@genex-ai/cli-demo 0.51.0-dev.99 → 0.52.0-dev.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1652,7 +1652,13 @@ async function callPublish(ctx, commit, opts, log) {
1652
1652
  async function pushSource(cwd, ctx, log) {
1653
1653
  const target = await fetchPushUrl(ctx, log);
1654
1654
  if (!target) return false;
1655
- return pushWorktree(cwd, target.pushUrl, target.managed, log);
1655
+ if (await pushWorktree(cwd, target.pushUrl, target.managed, log)) return true;
1656
+ if (!target.managed) return false;
1657
+ log.info("Retrying the source push\u2026");
1658
+ await new Promise((r) => setTimeout(r, 2e3));
1659
+ const fresh = await fetchPushUrl(ctx, log);
1660
+ if (!fresh) return false;
1661
+ return pushWorktree(cwd, fresh.pushUrl, fresh.managed, log);
1656
1662
  }
1657
1663
  async function pushWorktree(cwd, pushUrl, managed, log) {
1658
1664
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
@@ -2119,6 +2125,104 @@ async function runPreview(opts) {
2119
2125
  }
2120
2126
  }
2121
2127
 
2128
+ // src/commands/generate.ts
2129
+ import { PNG } from "pngjs";
2130
+
2131
+ // src/lib/open.ts
2132
+ import { spawn as spawn4 } from "child_process";
2133
+ function tokenize(cmd) {
2134
+ return cmd.trim().split(/\s+/).filter(Boolean);
2135
+ }
2136
+ function openUrl(url) {
2137
+ let command;
2138
+ let args;
2139
+ const custom = (process.env.GENEX_BROWSER || process.env.BROWSER)?.trim();
2140
+ const customParts = custom ? tokenize(custom) : [];
2141
+ if (customParts.length > 0) {
2142
+ command = customParts[0];
2143
+ args = [...customParts.slice(1), url];
2144
+ } else {
2145
+ switch (process.platform) {
2146
+ case "darwin":
2147
+ command = "open";
2148
+ args = [url];
2149
+ break;
2150
+ case "win32":
2151
+ command = "cmd";
2152
+ args = ["/c", "start", "", url];
2153
+ break;
2154
+ default:
2155
+ command = "xdg-open";
2156
+ args = [url];
2157
+ break;
2158
+ }
2159
+ }
2160
+ try {
2161
+ const child = spawn4(command, args, { stdio: "ignore", detached: true });
2162
+ child.on("error", () => {
2163
+ });
2164
+ child.unref();
2165
+ } catch {
2166
+ }
2167
+ }
2168
+
2169
+ // src/lib/seams.ts
2170
+ var DEFAULT_SEAM_TOLERANCE = 3;
2171
+ function measureSeam(png, tolerance = DEFAULT_SEAM_TOLERANCE) {
2172
+ const { width: w, height: h, data } = png;
2173
+ const chan = (x, y, ch) => data[(y * w + x) * 4 + ch] ?? 0;
2174
+ const colDiff = (a, b) => {
2175
+ let s = 0;
2176
+ for (let y = 0; y < h; y++) {
2177
+ s += Math.abs(chan(a, y, 0) - chan(b, y, 0));
2178
+ s += Math.abs(chan(a, y, 1) - chan(b, y, 1));
2179
+ s += Math.abs(chan(a, y, 2) - chan(b, y, 2));
2180
+ }
2181
+ return s / (h * 3);
2182
+ };
2183
+ const rowDiff = (a, b) => {
2184
+ let s = 0;
2185
+ for (let x = 0; x < w; x++) {
2186
+ s += Math.abs(chan(x, a, 0) - chan(x, b, 0));
2187
+ s += Math.abs(chan(x, a, 1) - chan(x, b, 1));
2188
+ s += Math.abs(chan(x, a, 2) - chan(x, b, 2));
2189
+ }
2190
+ return s / (w * 3);
2191
+ };
2192
+ const seamH = colDiff(0, w - 1);
2193
+ const seamV = rowDiff(0, h - 1);
2194
+ const stepX = Math.max(1, Math.floor(w / 64));
2195
+ const stepY = Math.max(1, Math.floor(h / 64));
2196
+ let baseH = 0;
2197
+ let nH = 0;
2198
+ for (let x = 1; x < w - 1; x += stepX) {
2199
+ baseH += colDiff(x, x + 1);
2200
+ nH++;
2201
+ }
2202
+ let baseV = 0;
2203
+ let nV = 0;
2204
+ for (let y = 1; y < h - 1; y += stepY) {
2205
+ baseV += rowDiff(y, y + 1);
2206
+ nV++;
2207
+ }
2208
+ baseH = Math.max(baseH / Math.max(1, nH), 1e-6);
2209
+ baseV = Math.max(baseV / Math.max(1, nV), 1e-6);
2210
+ const round = (n) => Math.round(n * 100) / 100;
2211
+ const ratioHorizontal = round(seamH / baseH);
2212
+ const ratioVertical = round(seamV / baseV);
2213
+ const worstRatio = Math.max(ratioHorizontal, ratioVertical);
2214
+ return {
2215
+ width: w,
2216
+ height: h,
2217
+ ratioHorizontal,
2218
+ ratioVertical,
2219
+ worstRatio,
2220
+ worstAxis: ratioHorizontal >= ratioVertical ? "horizontal" : "vertical",
2221
+ tolerance,
2222
+ seamless: worstRatio <= tolerance
2223
+ };
2224
+ }
2225
+
2122
2226
  // src/lib/sse.ts
2123
2227
  async function* readSSE(body) {
2124
2228
  const decoder = new TextDecoder();
@@ -2255,9 +2359,9 @@ async function runGenerate(kind, opts) {
2255
2359
  log.dim(" (Re-running the generate command would start \u2014 and bill \u2014 a NEW generation.)");
2256
2360
  return;
2257
2361
  }
2258
- await awaitAndReport(apiUrl, token, id, kind, log);
2362
+ await awaitAndReport(apiUrl, token, id, kind, log, opts.open);
2259
2363
  }
2260
- async function awaitAndReport(apiUrl, token, id, kind, log) {
2364
+ async function awaitAndReport(apiUrl, token, id, kind, log, open = false) {
2261
2365
  log.step("Generating\u2026 (this can take up to a minute)");
2262
2366
  const onProgress = (p) => log.dim(` ${p}%`);
2263
2367
  const timeoutMs = waitTimeoutFor(kind);
@@ -2269,9 +2373,9 @@ async function awaitAndReport(apiUrl, token, id, kind, log) {
2269
2373
  process.exitCode = 1;
2270
2374
  return;
2271
2375
  }
2272
- await reportTerminal(kind, view, log);
2376
+ await reportTerminal(kind, view, log, open);
2273
2377
  }
2274
- async function reportTerminal(kind, view, log) {
2378
+ async function reportTerminal(kind, view, log, open = false) {
2275
2379
  if (view.status !== "completed") {
2276
2380
  log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
2277
2381
  process.exitCode = 1;
@@ -2290,6 +2394,15 @@ async function reportTerminal(kind, view, log) {
2290
2394
  log.plain(` ${label}${f.url}`);
2291
2395
  }
2292
2396
  log.plain("");
2397
+ if (kind === "texture" && files[0]) await reportTextureSeam(files[0].url, log);
2398
+ if (open && files[0]) {
2399
+ openUrl(files[0].url);
2400
+ log.plain(
2401
+ ` ${c.bold("\u{1F441} Show this to the user")} \u2014 it just opened in their browser; paste this link in chat too so they can open it themselves:`
2402
+ );
2403
+ log.plain(` ${files[0].url}`);
2404
+ log.plain("");
2405
+ }
2293
2406
  printHint(kind, files, log);
2294
2407
  await firstPreviewNudge(log);
2295
2408
  }
@@ -2375,6 +2488,27 @@ async function poll(apiUrl, token, id, onProgress, timeoutMs) {
2375
2488
  }
2376
2489
  return null;
2377
2490
  }
2491
+ async function reportTextureSeam(url, log) {
2492
+ try {
2493
+ const res = await fetch(url);
2494
+ if (!res.ok) return;
2495
+ const png = PNG.sync.read(Buffer.from(await res.arrayBuffer()));
2496
+ if (png.width < 4 || png.height < 4) return;
2497
+ const r = measureSeam(png);
2498
+ if (r.seamless) {
2499
+ log.dim(` Tiling check: seam ${r.worstRatio}\xD7 this texture's own detail \u2014 tiles cleanly.`);
2500
+ return;
2501
+ }
2502
+ log.error(
2503
+ `Visible ${r.worstAxis} tiling seam \u2014 the tile boundary jumps ${r.worstRatio}\xD7 this texture's own detail (want \u2264 ${r.tolerance}\xD7).`
2504
+ );
2505
+ log.plain(" It will read as a repeating grid on any large surface. Before wiring it in:");
2506
+ log.plain(' \u2022 regenerate with "seamless tiling, no visible edges" in the prompt (--terrain for ground), or');
2507
+ log.plain(" \u2022 lower the mesh's UV repeat so the seam falls off-camera.");
2508
+ log.plain(` Re-check any image any time: ${c.cyan("npx genex ui seams --in <url>")}`);
2509
+ } catch {
2510
+ }
2511
+ }
2378
2512
  function printHint(kind, files, log) {
2379
2513
  const url = files[0]?.url ?? "";
2380
2514
  const hint = {
@@ -2440,10 +2574,10 @@ async function runWait(opts) {
2440
2574
  log.dim(` ${kind} ${id}`);
2441
2575
  log.plain("");
2442
2576
  if (TERMINAL2.has(view.status)) {
2443
- await reportTerminal(kind, view, log);
2577
+ await reportTerminal(kind, view, log, opts.open);
2444
2578
  return;
2445
2579
  }
2446
- await awaitAndReport(apiUrl, token, id, kind, log);
2580
+ await awaitAndReport(apiUrl, token, id, kind, log, opts.open);
2447
2581
  }
2448
2582
 
2449
2583
  // src/commands/controller.ts
@@ -3029,11 +3163,11 @@ function rank(items, query) {
3029
3163
  // src/commands/ui.ts
3030
3164
  import fs14 from "fs/promises";
3031
3165
  import path14 from "path";
3032
- import { PNG as PNG2 } from "pngjs";
3166
+ import { PNG as PNG3 } from "pngjs";
3033
3167
 
3034
3168
  // src/lib/png-tools.ts
3035
3169
  import fs13 from "fs/promises";
3036
- import { PNG } from "pngjs";
3170
+ import { PNG as PNG2 } from "pngjs";
3037
3171
  var ALPHA_TRANSPARENT_MAX = 16;
3038
3172
  var isHttpUrl = (s) => /^https?:\/\//i.test(s);
3039
3173
  async function loadPng(input) {
@@ -3045,14 +3179,14 @@ async function loadPng(input) {
3045
3179
  } else {
3046
3180
  buf = await fs13.readFile(input);
3047
3181
  }
3048
- return PNG.sync.read(buf);
3182
+ return PNG2.sync.read(buf);
3049
3183
  }
3050
3184
  async function writePng(file, png) {
3051
- await fs13.writeFile(file, PNG.sync.write(png));
3185
+ await fs13.writeFile(file, PNG2.sync.write(png));
3052
3186
  }
3053
3187
  function cropPng(image, box) {
3054
- const out = new PNG({ width: box.w, height: box.h });
3055
- PNG.bitblt(image, out, box.x, box.y, box.w, box.h, 0, 0);
3188
+ const out = new PNG2({ width: box.w, height: box.h });
3189
+ PNG2.bitblt(image, out, box.x, box.y, box.w, box.h, 0, 0);
3056
3190
  return out;
3057
3191
  }
3058
3192
  function floodFill(width, height, visited, queue, startX, startY, inRegion, onPixel) {
@@ -3391,7 +3525,7 @@ async function uiExtract(opts, log) {
3391
3525
  log.warn(`Skipping "${name}" \u2014 crop too small (${cropW}x${cropH}).`);
3392
3526
  continue;
3393
3527
  }
3394
- const out = new PNG2({ width: cropW, height: cropH });
3528
+ const out = new PNG3({ width: cropW, height: cropH });
3395
3529
  for (let yo = 0; yo < cropH; yo++) {
3396
3530
  const sy = padY0 + yo;
3397
3531
  for (let xo = 0; xo < cropW; xo++) {
@@ -3552,7 +3686,7 @@ function greenAlpha(r, g, b, a) {
3552
3686
  return 0;
3553
3687
  }
3554
3688
  function convertGreenMask(annotatedCrop) {
3555
- const out = new PNG2({ width: annotatedCrop.width, height: annotatedCrop.height });
3689
+ const out = new PNG3({ width: annotatedCrop.width, height: annotatedCrop.height });
3556
3690
  let filled = 0;
3557
3691
  let minX = annotatedCrop.width;
3558
3692
  let minY = annotatedCrop.height;
@@ -3654,7 +3788,7 @@ function blendPixel(image, x, y, rgba) {
3654
3788
  image.data[idx + 3] = Math.max(image.data[idx + 3], rgba[3]);
3655
3789
  }
3656
3790
  function makeOverlay(clean2, mask) {
3657
- const out = new PNG2({ width: clean2.width, height: clean2.height });
3791
+ const out = new PNG3({ width: clean2.width, height: clean2.height });
3658
3792
  clean2.data.copy(out.data);
3659
3793
  for (let y = 0; y < mask.height; y += 1) {
3660
3794
  for (let x = 0; x < mask.width; x += 1) {
@@ -3934,74 +4068,23 @@ async function uiTrim(opts, log) {
3934
4068
  }
3935
4069
  async function uiSeams(opts, log) {
3936
4070
  const input = requireOpt(opts.input, "--in", "seams");
3937
- const tol = opts.seamTolerance ?? 3;
4071
+ const tol = opts.seamTolerance ?? DEFAULT_SEAM_TOLERANCE;
3938
4072
  const png = await loadPng(input);
3939
- const { width: w, height: h, data } = png;
3940
- if (w < 4 || h < 4) fail("Image is too small to test for tiling.", { width: w, height: h });
3941
- const chan = (x, y, ch) => data[(y * w + x) * 4 + ch] ?? 0;
3942
- const colDiff = (a, b) => {
3943
- let s = 0;
3944
- for (let y = 0; y < h; y++) {
3945
- s += Math.abs(chan(a, y, 0) - chan(b, y, 0));
3946
- s += Math.abs(chan(a, y, 1) - chan(b, y, 1));
3947
- s += Math.abs(chan(a, y, 2) - chan(b, y, 2));
3948
- }
3949
- return s / (h * 3);
3950
- };
3951
- const rowDiff = (a, b) => {
3952
- let s = 0;
3953
- for (let x = 0; x < w; x++) {
3954
- s += Math.abs(chan(x, a, 0) - chan(x, b, 0));
3955
- s += Math.abs(chan(x, a, 1) - chan(x, b, 1));
3956
- s += Math.abs(chan(x, a, 2) - chan(x, b, 2));
3957
- }
3958
- return s / (w * 3);
3959
- };
3960
- const seamH = colDiff(0, w - 1);
3961
- const seamV = rowDiff(0, h - 1);
3962
- const stepX = Math.max(1, Math.floor(w / 64));
3963
- const stepY = Math.max(1, Math.floor(h / 64));
3964
- let baseH = 0;
3965
- let nH = 0;
3966
- for (let x = 1; x < w - 1; x += stepX) {
3967
- baseH += colDiff(x, x + 1);
3968
- nH++;
3969
- }
3970
- let baseV = 0;
3971
- let nV = 0;
3972
- for (let y = 1; y < h - 1; y += stepY) {
3973
- baseV += rowDiff(y, y + 1);
3974
- nV++;
4073
+ if (png.width < 4 || png.height < 4) {
4074
+ fail("Image is too small to test for tiling.", { width: png.width, height: png.height });
3975
4075
  }
3976
- baseH = Math.max(baseH / Math.max(1, nH), 1e-6);
3977
- baseV = Math.max(baseV / Math.max(1, nV), 1e-6);
3978
- const round = (n) => Math.round(n * 100) / 100;
3979
- const ratioH = round(seamH / baseH);
3980
- const ratioV = round(seamV / baseV);
3981
- const worst = Math.max(ratioH, ratioV);
3982
- const axis = ratioH >= ratioV ? "horizontal" : "vertical";
3983
- const seamless = worst <= tol;
4076
+ const r = measureSeam(png, tol);
3984
4077
  const name = input.split("/").pop() ?? input;
3985
- process.stdout.write(
3986
- `${JSON.stringify({
3987
- input,
3988
- width: w,
3989
- height: h,
3990
- ratioHorizontal: ratioH,
3991
- ratioVertical: ratioV,
3992
- worstRatio: worst,
3993
- worstAxis: axis,
3994
- tolerance: tol,
3995
- seamless
3996
- })}
3997
- `
3998
- );
3999
- if (seamless) {
4000
- log.success(`${name}: tiles cleanly \u2014 worst seam ${worst}\xD7 the texture's own detail (\u2264 ${tol}\xD7).`);
4078
+ process.stdout.write(`${JSON.stringify({ input, ...r })}
4079
+ `);
4080
+ if (r.seamless) {
4081
+ log.success(
4082
+ `${name}: tiles cleanly \u2014 worst seam ${r.worstRatio}\xD7 the texture's own detail (\u2264 ${tol}\xD7).`
4083
+ );
4001
4084
  return;
4002
4085
  }
4003
4086
  fail(
4004
- `Visible ${axis} tiling seam in ${name}: the tile boundary jumps ${worst}\xD7 the texture's own detail (want \u2264 ${tol}\xD7). Regenerate it seamless/tileable, or lower the mesh's UV repeat so the seam falls off-camera.`
4087
+ `Visible ${r.worstAxis} tiling seam in ${name}: the tile boundary jumps ${r.worstRatio}\xD7 the texture's own detail (want \u2264 ${tol}\xD7). Regenerate it seamless/tileable, or lower the mesh's UV repeat so the seam falls off-camera.`
4005
4088
  );
4006
4089
  }
4007
4090
 
@@ -4048,6 +4131,8 @@ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture` `image` `
4048
4131
  --terrain (texture) seamless tiling surface for terrain/ground.
4049
4132
  --duration <sec> (sfx, video) target clip length in seconds.
4050
4133
  --transparent (image) transparent background (alpha) \u2014 for decals/stickers.
4134
+ --open (image/video) open the finished asset in the user's browser +
4135
+ print a link \u2014 show them the concept frame / anything they approve.
4051
4136
  --aspect <ratio> (image) aspect preset (e.g. square, landscape, portrait).
4052
4137
  --quality <q> (image) quality preset: low | medium | high.
4053
4138
  --candidates <n> (image) generate 2-4 variants in ONE call and print every
@@ -4232,6 +4317,9 @@ function parseArgs(argv) {
4232
4317
  case "--no-wait":
4233
4318
  parsed.options.noWait = true;
4234
4319
  break;
4320
+ case "--open":
4321
+ parsed.options.open = true;
4322
+ break;
4235
4323
  case "--terrain":
4236
4324
  parsed.options.terrain = true;
4237
4325
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.51.0-dev.99",
3
+ "version": "0.52.0-dev.111",
4
4
  "description": "Set up your ~/.claude workspace, authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "start": "node src/index.ts",
22
22
  "dev": "node --watch src/index.ts",
23
23
  "typecheck": "tsc --noEmit",
24
- "test": "node --test test/*.test.ts"
24
+ "test": "node --test --test-concurrency=1 test/*.test.ts"
25
25
  },
26
26
  "keywords": [
27
27
  "cli",
@@ -67,23 +67,38 @@ map.repeat.set(64, 64);
67
67
  scene.add(ground);
68
68
  ```
69
69
 
70
- ## Verify it actually tilesdon't eyeball it
70
+ ## Tiling is checked FOR you read the verdict
71
71
 
72
- A texture that isn't truly seamless draws a repeating seam grid the moment
73
- `repeat` is raised the single most common "asset-flip" tell, and `--terrain`
74
- asks for seamless but does not guarantee it. So measure the seam, don't guess:
72
+ `npx genex texture` measures the wrap seam of every texture it generates and
73
+ prints the verdict right under the URL. A bad one looks like this:
75
74
 
76
- ```bash
77
- npx genex ui seams --in <texture-url> # a local .png works too
75
+ ```
76
+ Visible vertical tiling seam the tile boundary jumps 6. this texture's own detail (want ≤ 3×).
77
+ ```
78
+
79
+ `--terrain` ASKS the model for seamless; it does **not** guarantee it — a shipped
80
+ game seamed at 6.6× with `--terrain` set. So when that line appears, fix it
81
+ BEFORE wiring the texture in: **regenerate** with "seamless tiling, no visible
82
+ edges" in the prompt, or **lower the `repeat`** so the seam falls off-camera. A
83
+ good texture prints `tiles cleanly` instead. To re-check any image (including one
84
+ you didn't just generate): `npx genex ui seams --in <png|url>`.
85
+
86
+ ## Scale by texel density — never hand-pick `repeat`
87
+
88
+ The other half of "the floor looks wrong" is a `repeat` guessed out of the air.
89
+ Keep texels SQUARE: derive BOTH axes from the surface's real world size and ONE
90
+ chosen tile size.
91
+
92
+ ```ts
93
+ const TILE_M = 4; // one tile covers 4×4 metres — choose once
94
+ map.repeat.set(width / TILE_M, depth / TILE_M); // a 40×400 m lane → (10, 100)
78
95
  ```
79
96
 
80
- It compares the texture's opposite edges against its own interior detail and
81
- prints a ratio + verdict (`seamless: false` and a nonzero exit on a visible
82
- seam). A fail is a real defect fix it in this order: **regenerate** (add
83
- "seamless tiling, no visible edges" to the prompt, or `--terrain`), then **lower
84
- `repeat`** so any residual seam falls off-camera. Run this on every tiled
85
- floor/wall texture before you call the scene done — a large flat surface with a
86
- high `repeat` and an unchecked texture is exactly how seams reach the player.
97
+ A non-uniform guess like `repeat.set(2, 40)` on a long lane stretches the texture
98
+ 20:1 along it: the pattern smears one way and crowds the other. That reads as
99
+ "wrong scale / glued together" even when the texture itself is flawless and it
100
+ is exactly what shipped in a real game. Same rule for walls and platforms: derive
101
+ from that surface's own width/height, not from the floor's numbers.
87
102
 
88
103
  ## Publish checklist
89
104
 
@@ -79,13 +79,25 @@ lap counter in a game without laps). `--aspect 16:9 --quality high
79
79
  --no-wait`, enqueued FIRST of all art; the URL goes into the style-brief
80
80
  comment.
81
81
 
82
- **Share it before building on it.** The moment the frame lands, show it to
83
- the user "this is roughly how the game will look: keep it, or change
84
- something?" as ONE structured question with 2–3 concrete adjustment
85
- options. Keep building gameplay-neutral work (scaffold, physics, netcode)
86
- while waiting, but do NOT anchor further art to an unapproved frame; if the
87
- user redirects, regenerate with their notes before the HUD mockup and menu
88
- frame go out.
82
+ **Share it before building on it actually SHOW the image, don't just ask.**
83
+ When the concept lands, pick it up with `genex wait <id> --open` (or generate it
84
+ with `--open`): it opens in the user's browser AND prints the link. Paste that
85
+ URL as a clickable link in your approval message — a URL is invisible in a
86
+ terminal, and "do you like it?" with no picture in front of the user is the #1
87
+ way this checkpoint fails (they end up digging logs for the file path). Then ask
88
+ ONE structured question — "this is roughly how the game will look: keep it, or
89
+ change something?" — with 2–3 concrete adjustment options. Keep building
90
+ gameplay-neutral work (scaffold, physics, netcode) while waiting, but do NOT
91
+ anchor further art to an unapproved frame. **Approval is a LOOP, not a
92
+ one-shot:** if the user dislikes ANYTHING, regenerate with their exact notes
93
+ (`--candidates 2–3` gives them options to choose from), open + link the new
94
+ frame, and ask again — repeat until they actually approve. Carry every note
95
+ forward so each round compounds; if two rounds don't converge, offer 2–3
96
+ distinct directions as a structured question instead of re-rolling blind. The
97
+ HUD mockup, the menu, and every downstream asset wait for a frame the user has
98
+ signed off — a single "meh" is never permission to move on. The same "open it +
99
+ paste the link" rule covers every image the user weighs in on — the menu still,
100
+ the HUD mockup candidates.
89
101
 
90
102
  **The concept anchors STYLE, not truth.** Palette, materials, light, and
91
103
  register come from the frame; CONTENT comes from the game contract. When
@@ -67,11 +67,21 @@ direction in the same plan block — the camera rig + pointer bucket
67
67
  (`$genex-threejs-camera-direction`), the renderer baseline (tone mapping,
68
68
  exposure, output color space — set deliberately at boot; stock three.js
69
69
  defaults are not a look, `$genex-threejs-exposure-color-grading`), the post
70
- stack this game ships (bloom/AO/grade at least ONE built render-pass effect
71
- for EVERY game, justified by the shared style brief; the UI vignette div or a
72
- CSS filter on the canvas does not count, and a plan that "deliberately skips
73
- everything" is the stock default, not a look; `$genex-threejs-image-pipeline`
74
- owns ordering when 2+ compose), one named **ambient-motion loop** that keeps
70
+ stack this game ships — read off the concept frame's OWN look (the grade, bloom
71
+ level, haze, grain it already shows) and which post the 2–3 AAA references lean
72
+ on, not a default single bloom; ONE built render-pass effect is the FLOOR
73
+ against no-post for EVERY game, never the target — ship the richness the concept
74
+ implies (the UI vignette div or a CSS canvas filter does not count;
75
+ `$genex-threejs-image-pipeline` owns ordering when 2+ compose), a decision for
76
+ **every primitive surface** the game builds — walls, barriers, kerbs and
77
+ platforms each get a real texture or a **shader** where that surface wants motion
78
+ or energy (a pulsing force barrier, an electric fence, a scrolling hazard strip),
79
+ judged one by one: a shader that gives a barrier life earns its place, the same
80
+ effect everywhere disfigures, deliberate flat black is valid if you say so — but
81
+ a flat-colour box beside textured geometry is the "stopped halfway" tell
82
+ (`$genex-threejs-procedural-materials` / `$genex-threejs-procedural-vfx`, and SEE
83
+ it in the running game), one named
84
+ **ambient-motion loop** that keeps
75
85
  the scene alive at rest (emissive pulse, shimmer, drifting dust — shader
76
86
  work, zero generations), and the lighting/atmosphere mood from that
77
87
  same brief. Planning is not building — effects still land last in the
@@ -41,13 +41,19 @@ Three.js release or branch, and do not blindly copy demo architecture.
41
41
  - **renderer baseline**: tone mapping, exposure, and output color space set
42
42
  deliberately at boot (`$genex-threejs-exposure-color-grading` owns the
43
43
  staging — stock three.js defaults are not a look);
44
- - **the post stack**: which image effects THIS game ships (bloom? AO?
45
- grade?) each named and justified by the shared style brief. The floor
46
- for EVERY game is the deliberate renderer baseline PLUS at least one
47
- scene-serving render-pass effect (bloom, AO, or a LUT/shader grade the
48
- UI vignette div or a CSS filter on the canvas does not count); "no post
49
- at all" is the stock default, not a plan, and "it's only a draft" is not
50
- a lower floor. When 2+ effects compose, `$genex-threejs-image-pipeline`
44
+ - **the post stack**: read the look off your evidence don't default to a
45
+ single bloom. TWO sources drive it: (a) the CONCEPT FRAME a rendered
46
+ image that already carries a grade, a bloom level, haze/DoF, maybe grain or
47
+ aberration; name what it actually shows and reproduce THAT; (b) the 2–3 AAA
48
+ references name which post each one leans on (racing: motion blur + heat
49
+ haze; grounded shooter: restrained bloom + film grain + faint aberration;
50
+ clean sci-fi: crisp bloom + strong grade). Ship the stack that evidence
51
+ calls for, each effect named and justified. ONE scene-serving render-pass
52
+ effect (bloom, AO, or a LUT/shader grade — a UI vignette div or a CSS
53
+ canvas filter does NOT count) is the FLOOR against "no post", never the
54
+ target: match the concept's richness, don't stop at one token pass. "No
55
+ post at all" is the stock default, not a plan, and "it's only a draft" is
56
+ not a lower floor. When 2+ effects compose, `$genex-threejs-image-pipeline`
51
57
  owns the pass ordering;
52
58
  - **references**: name 2–3 AAA games whose look this game borrows
53
59
  (conventions, lighting mood, palette, post — never trade dress); the
@@ -59,6 +65,21 @@ Three.js release or branch, and do not blindly copy demo architecture.
59
65
  drifting dust, a slowly flowing texture. Shader/procedural, zero
60
66
  generations, built with the scene — a world that is perfectly still
61
67
  reads as a screenshot, not a place;
68
+ - **every primitive surface — texture it or shade it, decided one by one**:
69
+ the ground is never the only surface. Walk the walls, barriers, kerbs,
70
+ platforms and props the game BUILDS out of primitives, and give each a real
71
+ material: a generated texture, or a **shader** where that surface genuinely
72
+ wants motion or energy — a force barrier that pulses and refracts, an
73
+ electric fence, a scrolling hazard strip, an emissive seam that breathes.
74
+ Judge surface by surface: a shader that gives a blocking barrier life earns
75
+ its place; the same effect smeared over everything disfigures the scene.
76
+ Deliberate flat black IS a valid answer when the look calls for it — say so
77
+ in one line. What is never valid is not deciding: a flat-colour box standing
78
+ next to textured geometry is the "stopped halfway" tell, and it is what
79
+ ships when this bullet is skipped. `$genex-threejs-procedural-materials`
80
+ and `$genex-threejs-procedural-vfx` own the craft. Whatever you apply, SEE
81
+ it in the running game before calling it done — an unverified shader
82
+ disfigures as easily as it delights;
62
83
  - **lighting/atmosphere mood** from the SAME shared style brief the UI gate
63
84
  wrote — one art direction across scene and UI.
64
85
  Planning is not building: effects still land LAST (steps 10–11); this step