@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
|
@@ -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** —
|
|
96
|
-
|
|
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
|