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

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
@@ -3183,7 +3183,8 @@ var UI_NUMBER_FLAGS = {
3183
3183
  "--edge-flush-max": "edgeFlushMax",
3184
3184
  "--quantize": "quantize",
3185
3185
  "--ink-dist": "inkDist",
3186
- "--min-frac": "minFrac"
3186
+ "--min-frac": "minFrac",
3187
+ "--seam-tolerance": "seamTolerance"
3187
3188
  };
3188
3189
  var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or https URL>)")}
3189
3190
  ui extract --out-dir <dir> --names a,b,c (reading order) \u2014 split an
@@ -3199,7 +3200,10 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
3199
3200
  ink candidates as JSON. [--quantize 12] [--ink-dist 60]
3200
3201
  [--min-frac 0.004] [--out <json>] [--crop <png>]
3201
3202
  ui trim [--out <png>] \u2014 crop to the alpha bbox (default: overwrite
3202
- --in), print real dims + refresh the .bbox.json sidecar.`;
3203
+ --in), print real dims + refresh the .bbox.json sidecar.
3204
+ ui seams measure a tiling texture's wrap seam (opposite edges vs
3205
+ interior detail) \u2192 ratio + verdict as JSON; nonzero exit on
3206
+ a visible seam. [--seam-tolerance 3]`;
3203
3207
  async function runUi(opts) {
3204
3208
  const log = createLogger({ quiet: opts.quiet });
3205
3209
  try {
@@ -3216,9 +3220,12 @@ async function runUi(opts) {
3216
3220
  case "trim":
3217
3221
  await uiTrim(opts, log);
3218
3222
  return;
3223
+ case "seams":
3224
+ await uiSeams(opts, log);
3225
+ return;
3219
3226
  default:
3220
3227
  log.error(
3221
- opts.sub ? `Unknown ui tool "${opts.sub}". Tools: extract, masks, text-color, trim.` : "Missing ui tool. Tools: extract, masks, text-color, trim."
3228
+ opts.sub ? `Unknown ui tool "${opts.sub}". Tools: extract, masks, text-color, trim, seams.` : "Missing ui tool. Tools: extract, masks, text-color, trim, seams."
3222
3229
  );
3223
3230
  log.plain(UI_USAGE);
3224
3231
  process.exitCode = 1;
@@ -3925,6 +3932,78 @@ async function uiTrim(opts, log) {
3925
3932
  `
3926
3933
  );
3927
3934
  }
3935
+ async function uiSeams(opts, log) {
3936
+ const input = requireOpt(opts.input, "--in", "seams");
3937
+ const tol = opts.seamTolerance ?? 3;
3938
+ 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++;
3975
+ }
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;
3984
+ 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).`);
4001
+ return;
4002
+ }
4003
+ 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.`
4005
+ );
4006
+ }
3928
4007
 
3929
4008
  // src/index.ts
3930
4009
  var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "image", "video"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.51.0-dev.98",
3
+ "version": "0.51.0-dev.99",
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": {
@@ -114,6 +114,24 @@ The test: "is this fixed identity artwork, or should runtime code own it?"
114
114
  Baking a runtime value into a sprite is a failure; stripping a carved identity
115
115
  label flattens the art.
116
116
 
117
+ ## Icons & mechanics must match the game — diff before Stage 2
118
+
119
+ Words are only half of it. Every ICON, slot, gauge, and pictogram in the mockup
120
+ implies a MECHANIC: a grenade icon promises grenades, three ability slots
121
+ promise three abilities, an armor bar promises armor. Before Stage 2, walk each
122
+ one against the game contract — the mechanics the code actually has (or that you
123
+ are committing to build):
124
+
125
+ - **Backed by a real mechanic** → keep it and wire it to that state.
126
+ - **No backing mechanic** → it is a promise the player will notice is empty.
127
+ CUT it (prompt Stage 2, or a region re-edit, to omit that icon/slot) OR add
128
+ the mechanic to the game. Shipping a depicted control the game can't do — a
129
+ grenade slot on a rifle-only HUD, an armor bar with no armor, a minimap over
130
+ no map — is a failure, and "the model drew it so I kept it" is how it happens.
131
+
132
+ The mockup (and any concept frame) anchors STYLE; the game contract owns WHAT
133
+ EXISTS. When the picture and the mechanics disagree, the mechanics win.
134
+
117
135
  ## The pipeline
118
136
 
119
137
  Each step is one command; every `<...-url>` is the R2 URL the previous command
@@ -129,6 +129,18 @@ Gotchas (all bite in practice):
129
129
  moving or skinned target, use a small `PlaneGeometry` quad offset along the normal
130
130
  (`hit.point + n * 0.01`, `quad.lookAt(hit.point.clone().add(n))`) instead.
131
131
 
132
+ ### Impact marks (bullet holes, scorch, blood) — spawn on the hit, not a keypress
133
+
134
+ A game that shoots, throws, or explodes and leaves NO mark on what it hits reads
135
+ as unfinished — and it's the same recipe, driven by the weapon's raycast you
136
+ already have. Generate ONE small `--transparent` bullet-hole / scorch / impact
137
+ image, then on each confirmed **world** hit place a decal at `hit.point` with
138
+ `hit.face.normal` (exactly the `spray()` body above), capped in a FIFO ring
139
+ (~30–50) reusing one shared material. On enemy/skinned hits skip the decal and
140
+ use the quad fallback or a hit-spark VFX instead. Impact marks are a first-class
141
+ generated surface for any weapon/collision game — inventory them up front with
142
+ the rest of your art, don't discover the bare walls at the end.
143
+
132
144
  ## Multiplayer
133
145
 
134
146
  The asset URL is public, permanent, and CORS-open, so it is safe to broadcast the
@@ -67,6 +67,24 @@ map.repeat.set(64, 64);
67
67
  scene.add(ground);
68
68
  ```
69
69
 
70
+ ## Verify it actually tiles — don't eyeball it
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:
75
+
76
+ ```bash
77
+ npx genex ui seams --in <texture-url> # a local .png works too
78
+ ```
79
+
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.
87
+
70
88
  ## Publish checklist
71
89
 
72
90
  - Load it from the **URL** the command printed — absolute and permanent, so it resolves
@@ -92,6 +110,8 @@ scene.add(ground);
92
110
  - **"Email not verified" (`email_verification_required`)** — generation credits
93
111
  unlock after the account's email is verified. Give the user the verify link the
94
112
  CLI printed, wait for them to confirm, then re-run the command.
95
- - **Visible tiling seams** — use `--terrain` (tuned for seamless edges), lower the
96
- `repeat`, or blend two textures. Perfect seamlessness is a known v1 limitation.
113
+ - **Visible tiling seams** — measure first (`npx genex ui seams --in <url>`): a
114
+ high ratio is a defect to fix, not a limitation to accept. Regenerate with
115
+ `--terrain` / a "seamless tiling" prompt, lower the `repeat`, or blend two
116
+ textures until the check passes.
97
117
  - **Colors look washed/dark** — ensure `map.colorSpace = THREE.SRGBColorSpace`.
@@ -195,6 +195,48 @@ edits scattered through the code:
195
195
  HUD state come from `$genex-threejs-multiplayer`; render what the SDK
196
196
  reports, don't guess at it.
197
197
 
198
+ ## Escape must pause the game — not shrink the window
199
+
200
+ For a game that captures the mouse (first-person or pointer-lock aim — see the
201
+ pointer bucket in `$genex-threejs-camera-direction`), pressing Escape does two
202
+ **browser-reserved** things you CANNOT stop with `preventDefault`: it exits
203
+ fullscreen and releases pointer lock, and the player sees the window "shrink".
204
+ The real fix is the **Keyboard Lock API**, which routes Escape to your code
205
+ instead — Chromium only, and only in fullscreen. Enter fullscreen on a gesture
206
+ (the Deploy/Resume click), capture Escape, and own the pause:
207
+
208
+ ```ts
209
+ // call ONLY from a user gesture (the Deploy click, the Resume click) — never at boot
210
+ async function enterImmersive(): Promise<void> {
211
+ try {
212
+ if (!document.fullscreenElement)
213
+ await document.documentElement.requestFullscreen({ navigationUI: "hide" });
214
+ } catch { /* sandboxed iframe or no gesture — the pointer-lock path below still pauses */ }
215
+ // Chromium + fullscreen only: deliver Escape to us instead of exiting fullscreen.
216
+ try {
217
+ await (navigator as unknown as { keyboard?: { lock?: (k: string[]) => Promise<void> } })
218
+ .keyboard?.lock?.(["Escape"]);
219
+ } catch { /* Keyboard Lock unsupported — fine */ }
220
+ }
221
+
222
+ document.addEventListener("keydown", (e) => {
223
+ if (e.code !== "Escape") return;
224
+ e.preventDefault(); // stops any *other* default; the reserved ones the lock handles
225
+ if (phase === "playing") { document.exitPointerLock?.(); setPhase("paused"); } // free cursor for the menu
226
+ else if (phase === "paused") { void enterImmersive(); canvas.requestPointerLock?.(); } // Esc resumes
227
+ });
228
+ ```
229
+
230
+ - **Degradation is built in.** Where Keyboard Lock is absent (Safari, Firefox) or
231
+ the game isn't fullscreen, the browser still releases pointer lock on Escape —
232
+ so ALSO keep the pointer-lock-loss → pause path (`pointerlockchange`: if
233
+ unlocked while `playing`, `setPhase("paused")`). Escape then always pauses; it
234
+ only *also* drops fullscreen on browsers without the lock — unavoidable there.
235
+ - **Only for immersive/pointer-lock games.** A top-down or menu-driven game never
236
+ captures the mouse and has no window to shrink — skip all of this.
237
+ - **Dashboard embed:** full coverage needs the game iframe to permit keyboard
238
+ lock; standalone play (`<slug>.genex.technology`) works today regardless.
239
+
198
240
  ## The loader
199
241
 
200
242
  The loader is the first thing every player sees — a bare "Loading… 3/5" over