@genex-ai/cli-demo 1.13.2-dev.559 → 1.14.0-dev.560

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.
@@ -1,13 +1,13 @@
1
1
  import {
2
- BLENDER_SETUP_HINT,
3
2
  SHEET_FORMATS,
4
3
  acquireSeat,
5
4
  blenderCall,
6
5
  blenderEndpoint,
6
+ blenderSetupHint,
7
7
  isRenderMode,
8
8
  sceneSummary,
9
9
  sheetOf
10
- } from "./chunk-WXHTNSFB.js";
10
+ } from "./chunk-2UJK5OCB.js";
11
11
  import {
12
12
  getCliVersion
13
13
  } from "./chunk-HYCSNWYX.js";
@@ -178,7 +178,7 @@ async function handle(msg) {
178
178
  if (!tool) return fail(id, -32602, `Unknown tool: ${name}`);
179
179
  const base = await mcpEndpoint();
180
180
  if (!base) {
181
- return reply(id, { content: [{ type: "text", text: BLENDER_SETUP_HINT }], isError: true });
181
+ return reply(id, { content: [{ type: "text", text: blenderSetupHint() }], isError: true });
182
182
  }
183
183
  try {
184
184
  return reply(id, await tool.run(args, base));
@@ -124,13 +124,20 @@ function blenderEndpoint() {
124
124
  const raw = process.env.GENEX_BLENDER_URL?.trim();
125
125
  return raw ? raw.replace(/\/+$/, "") : void 0;
126
126
  }
127
- var BLENDER_SETUP_HINT = [
128
- "GENEX_BLENDER_URL is not set \u2014 there is no hosted Blender service yet.",
129
- " Start one locally:",
130
- " docker build --platform=linux/amd64 -t genex-blender:m0 apps/blender-service",
131
- " docker run -d --platform=linux/amd64 -p 8099:8080 genex-blender:m0",
132
- " export GENEX_BLENDER_URL=http://localhost:8099"
133
- ].join("\n");
127
+ function blenderSetupHint() {
128
+ if (process.env.GENEX_HOSTED_SESSION === "1") {
129
+ return [
130
+ "The Blender lane is off on this stand \u2014 there is no seat to acquire.",
131
+ " Build the space in code instead ($genex-threejs-procedural-assets); do not wait for it."
132
+ ].join("\n");
133
+ }
134
+ return [
135
+ "No Blender endpoint. Run the service against your own Blender in another terminal:",
136
+ " npx genex blender serve",
137
+ " export GENEX_BLENDER_URL=http://localhost:8088",
138
+ " GENEX_BLENDER_URL may also point at any running genex-blender service."
139
+ ].join("\n");
140
+ }
134
141
  var SHEET_FORMATS = ["webp", "png"];
135
142
  function sheetOf(r) {
136
143
  if (r.contactSheet?.b64) return { b64: r.contactSheet.b64, mime: r.contactSheet.mime };
@@ -198,7 +205,8 @@ async function blenderCall(base, route, body) {
198
205
  seat ? `${route} refused the seat token (401) \u2014 the seat may have been closed; run the command again to acquire a new one` : `${route} refused the request (401) \u2014 set GENEX_BLENDER_SECRET to the service's secret`
199
206
  );
200
207
  }
201
- throw new Error(`${route} failed (${res.status}): ${json.error ?? text.slice(0, 200)}`);
208
+ const detail = typeof json.detail === "string" ? ` \u2014 ${json.detail}` : "";
209
+ throw new Error(`${route} failed (${res.status}): ${json.error ?? text.slice(0, 200)}${detail}`);
202
210
  }
203
211
  return json;
204
212
  } catch (err) {
@@ -431,7 +439,7 @@ export {
431
439
  RENDER_MODES,
432
440
  isRenderMode,
433
441
  blenderEndpoint,
434
- BLENDER_SETUP_HINT,
442
+ blenderSetupHint,
435
443
  SHEET_FORMATS,
436
444
  sheetOf,
437
445
  sheetExt,
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- BLENDER_SETUP_HINT,
4
3
  RENDER_MODES,
5
4
  SHEET_FORMATS,
6
5
  acquireSeat,
7
6
  apiFetch,
8
7
  blenderCall,
9
8
  blenderEndpoint,
9
+ blenderSetupHint,
10
10
  fetchSignedInEmail,
11
11
  getWorkspacePath,
12
12
  isRenderMode,
@@ -22,7 +22,7 @@ import {
22
22
  writeProject,
23
23
  writeUserToken,
24
24
  writeWorkspace
25
- } from "./chunk-WXHTNSFB.js";
25
+ } from "./chunk-2UJK5OCB.js";
26
26
  import {
27
27
  CLI_CHANNEL,
28
28
  DEFAULT_API_URL,
@@ -700,7 +700,7 @@ var CONTRACT_END = "<!-- genex:contract:end -->";
700
700
  var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
701
701
  # Genex build contract (always in effect for this game)
702
702
 
703
- Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` (\`--image\` from a reference image; \`model segment <id>\` into named parts; \`model rig <id>\` + \`model animate <rig-id> --preset walk\` for non-biped bodies \u2014 7 body plans) \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. There is NO sky generator: every sky, horizon, time of day and weather mood is built in code \u2014 your call how, from a background colour and fog to an atmospheric sky shader with a real sun. It costs nothing, weighs nothing on phones, and keeps the sun in sync with the scene's own lighting, which a painted panorama never can. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 load the owning row before the first file of each subsystem, and re-load the map after any compaction.
703
+ Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` (\`--image\` from a reference image; \`model segment <id>\` into named parts; \`model rig <id>\` + \`model animate <rig-id> --preset walk\` for non-biped bodies \u2014 7 body plans) \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 a live Blender for composing levels and interiors \u2014 \`blender scene|exec|snap|import|export\` (hosted sessions with the GPU lane on, or \`blender serve\` on your own machine; the \`genex-blender-scene\` skill owns it) \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. There is NO sky generator: every sky, horizon, time of day and weather mood is built in code \u2014 your call how, from a background colour and fog to an atmospheric sky shader with a real sun. It costs nothing, weighs nothing on phones, and keeps the sun in sync with the scene's own lighting, which a painted panorama never can. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 load the owning row before the first file of each subsystem, and re-load the map after any compaction.
704
704
 
705
705
  Important note: put soul into your creations, with many details and love. Aim to make them realistic and feel real. Reach for genex capabilities by default \u2014 they are how the world gets real assets (law 22) \u2014 but never limit your imagination \u2014 you are a powerful agent. Use your built-in sub-agents and iteration loops (\`/loop\` or your platform's equivalent) where you need them to reach outstanding results. Verify yourself. Anything you put in front of the player should already feel alive and impressive \u2014 never a bare scene waiting for "later". Build what's still missing before polishing what already works.
706
706
 
@@ -20613,7 +20613,7 @@ async function runMotion(opts) {
20613
20613
  // src/commands/blender.ts
20614
20614
  import fs23 from "fs/promises";
20615
20615
  import path22 from "path";
20616
- var SUBS2 = ["demo", "exec", "snap", "scene", "export", "reset", "mcp", "serve"];
20616
+ var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve"];
20617
20617
  var DEFAULT_OUT_DIR = "assets/blender";
20618
20618
  async function writeB64(dir, name, b64) {
20619
20619
  await fs23.mkdir(dir, { recursive: true });
@@ -20644,7 +20644,7 @@ async function runBlender(opts) {
20644
20644
  return serveLocalBlender({ port, log });
20645
20645
  }
20646
20646
  if (sub === "mcp") {
20647
- const { runBlenderMcp } = await import("./blender-mcp-LLN4FPA4.js");
20647
+ const { runBlenderMcp } = await import("./blender-mcp-3U5AG7WS.js");
20648
20648
  return runBlenderMcp();
20649
20649
  }
20650
20650
  let base = blenderEndpoint();
@@ -20659,8 +20659,8 @@ async function runBlender(opts) {
20659
20659
  return 1;
20660
20660
  }
20661
20661
  if (!base) {
20662
- const [first, ...rest] = BLENDER_SETUP_HINT.split("\n");
20663
- log.error(first ?? "GENEX_BLENDER_URL is not set");
20662
+ const [first, ...rest] = blenderSetupHint().split("\n");
20663
+ log.error(first ?? "No Blender endpoint");
20664
20664
  log.plain(rest.join("\n"));
20665
20665
  return 1;
20666
20666
  }
@@ -20706,12 +20706,31 @@ async function runBlender(opts) {
20706
20706
  case "export": {
20707
20707
  const target = opts.out ?? path22.join(outDir, "scene.glb");
20708
20708
  const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
20709
+ if (!r.glbBase64) {
20710
+ log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
20711
+ return 1;
20712
+ }
20709
20713
  await fs23.mkdir(path22.dirname(target), { recursive: true });
20710
- await fs23.writeFile(target, Buffer.from(r.glbBase64 ?? "", "base64"));
20714
+ await fs23.writeFile(target, Buffer.from(r.glbBase64, "base64"));
20711
20715
  log.success(`Exported ${r.bytes ?? 0} bytes`);
20712
20716
  log.plain(` ${c.cyan(target)}`);
20713
20717
  return 0;
20714
20718
  }
20719
+ case "import": {
20720
+ const url = opts.importUrl?.trim();
20721
+ if (!url || !/^https?:\/\//.test(url)) {
20722
+ log.error("genex blender import needs a GLB URL: genex blender import https://\u2026/model-glb");
20723
+ return 1;
20724
+ }
20725
+ const r = await blenderCall(base, "/import", { url });
20726
+ log.success(`imported ${r.count ?? 0}: ${(r.imported ?? []).join(", ") || "(nothing)"}`);
20727
+ const snap = await blenderCall(base, "/render", { ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
20728
+ const sheet = sheetOf(snap);
20729
+ if (sheet) log.plain(` ${c.cyan(await writeB64(outDir, `contact-sheet.${sheetExt(sheet.mime)}`, sheet.b64))}`);
20730
+ const info = await blenderCall(base, "/scene");
20731
+ reportScene(log, info);
20732
+ return 0;
20733
+ }
20715
20734
  case "exec":
20716
20735
  case "demo": {
20717
20736
  let script;
@@ -21734,6 +21753,13 @@ ${c.bold("Usage")}
21734
21753
  signature move. --lean keeps movement to forward
21735
21754
  walk + run (enemies follow a path).
21736
21755
  genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
21756
+ genex blender <verb> Compose a level or interior in a live Blender \u2014
21757
+ scene | exec --in <file.py> | snap [--mode \u2026] |
21758
+ import <glb-url> | export --out <file.glb> | reset.
21759
+ Hosted sessions get a GPU seat on first use; at a
21760
+ terminal run 'genex blender serve' against your
21761
+ own Blender. Every scene change writes
21762
+ ./assets/blender/contact-sheet.webp \u2014 look at it.
21737
21763
  genex wait <id> Attach to a generation enqueued with --no-wait and
21738
21764
  print its asset URL(s) when it finishes. Safe to
21739
21765
  re-run \u2014 it never creates a new generation.
@@ -22430,6 +22456,13 @@ function parseArgs(argv) {
22430
22456
  parsed.error = `Unexpected argument: ${arg}`;
22431
22457
  return parsed;
22432
22458
  }
22459
+ } else if (parsed.command === "blender") {
22460
+ if (parsed.options.name === "import" && parsed.options.importUrl === void 0) {
22461
+ parsed.options.importUrl = arg;
22462
+ } else {
22463
+ parsed.error = `Unexpected argument: ${arg}`;
22464
+ return parsed;
22465
+ }
22433
22466
  } else {
22434
22467
  parsed.error = `Unexpected argument: ${arg}`;
22435
22468
  return parsed;
@@ -22950,9 +22983,8 @@ async function main() {
22950
22983
  case "ui":
22951
22984
  await runUi({ ...parsed.options, sub: parsed.options.name });
22952
22985
  break;
22953
- // HIDDEN from --help: the M1 test surface for the Blender service spike
22954
- // (apps/blender-service). Gated on GENEX_BLENDER_URL, which no hosted
22955
- // stand sets, so this is a local-only verb until that service is real.
22986
+ // The Blender surface (apps/blender-service): a hosted seat, an explicit
22987
+ // GENEX_BLENDER_URL, or `blender serve` see commands/blender.ts.
22956
22988
  case "blender":
22957
22989
  process.exitCode = await runBlender({ ...parsed.options, sub: parsed.options.name });
22958
22990
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.13.2-dev.559",
3
+ "version": "1.14.0-dev.560",
4
4
  "description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,222 @@
1
+ ---
2
+ name: genex-blender-scene
3
+ description: Compose a level, arena, interior, building or map — a SPACE of rooms, lanes, walls and sites that must relate — in a live Blender with `npx genex blender`, then export one GLB the game walks through. Use when the request is a place to move through (from a description or a floor plan), not a single prop; probe the lane first and build the same space in code if it is off.
4
+ ---
5
+
6
+ # Genex Blender · Scene
7
+
8
+ Build the game's SPACE in a real Blender that answers over HTTP: write a Python
9
+ script, run it, read the numbers, look at the four-view sheet, repeat — then
10
+ export one GLB and load it like any model. The scene persists between steps, so a
11
+ level grows the way a level is actually built: shell, then zones, then dressing.
12
+
13
+ ## When this lane owns the work
14
+
15
+ - A **space**: a level, arena, interior, building, map, dungeon, station — several
16
+ rooms, lanes, walls or sites that must relate to each other, and a player who
17
+ moves through them.
18
+ - A **floor plan or reference** to trace (a sketch, an ASCII plan, a photo of a room).
19
+ - A **blockout**: the playable shape first, dressing later.
20
+
21
+ Not this lane:
22
+
23
+ - One recognisable object (a barrel, a cart, a statue) → `$genex-ai-model`.
24
+ - One parameterized object placed many times with variation, or a stylized shader
25
+ material → `$genex-threejs-procedural-assets`.
26
+ - The player's body, creatures, animation → `$genex-ai-character`,
27
+ `$genex-threejs-creatures`. Terrain from a heightmap → code.
28
+
29
+ Mixing all three in one scene is normal: the Blender level is the architecture, a
30
+ generated GLB is the hero piece, code is the repeated dressing.
31
+
32
+ ## Is the lane on? Probe by doing
33
+
34
+ ```bash
35
+ npx genex blender scene
36
+ ```
37
+
38
+ Read the answer literally — there are three:
39
+
40
+ 1. **A scene JSON prints** → the lane is on. Continue.
41
+ 2. `Warming up a Blender seat…` and then `The Blender seat is still warming up. Run
42
+ the same command again in a minute.` → **not done yet**, even though the command
43
+ exited cleanly. A GPU is booting for you (about 20 s on a warm pod, a few
44
+ minutes on a cold one). Write your first script meanwhile, then run the command
45
+ again. Never read this line as success, and never read it as failure.
46
+ 3. `The Blender lane is off on this stand` or `No Blender seat: fleet_full` → build
47
+ the same space **in code** with `$genex-threejs-procedural-assets`, say so in one
48
+ plain line to the player, and keep going. Do not wait, do not ask.
49
+
50
+ The seat, once granted, is remembered in `.genex/blender-seat.json` — later
51
+ commands are silent about it.
52
+
53
+ ## The loop
54
+
55
+ Scripts are the level's source. Keep them in the project, one per zone:
56
+
57
+ ```
58
+ blender/01-shell.py walls, floor, towers
59
+ blender/02-gatehouse.py
60
+ blender/03-courtyard.py
61
+
62
+ ```
63
+
64
+ Run one:
65
+
66
+ ```bash
67
+ npx genex blender exec --in blender/01-shell.py
68
+ ```
69
+
70
+ Every run prints two things and writes one file:
71
+
72
+ - **The numbers** — `objects N meshes N tris N materials N radius N`. This is
73
+ the gate. Before you run, know what the script should produce (twelve objects,
74
+ about 4k tris, radius ≈ 40); after, compare. A render judge is unreliable for
75
+ pass/fail; the numbers are not.
76
+ - **The sheet** — `assets/blender/contact-sheet.webp`, four fixed views (front,
77
+ side, TOP, hero) from cameras derived from the scene bounds, so two steps compare
78
+ pixel for pixel. **Open it with your file-reading tool after every step.** The
79
+ TOP view is the floor-plan check; the hero view is the mood check. Nothing shows
80
+ it to you unless you look.
81
+ - Script `print()` output comes back as stdout; a raised exception comes back with
82
+ its traceback AND the sheet of the half-built scene, which is usually where the
83
+ bug is visible.
84
+
85
+ The Python namespace **persists between runs** (`bpy`, `bmesh`, `mathutils`,
86
+ `math`, `random` are preloaded): define `mat()` and `box()` helpers once in the
87
+ first script and use them in every later one. If a run says the service restarted
88
+ and the scene was restored from a checkpoint, re-read the numbers — your last step
89
+ may be missing; re-run that one script.
90
+
91
+ Other verbs:
92
+
93
+ ```bash
94
+ npx genex blender snap --mode wireframe # topology; also normals | solid | lit
95
+ npx genex blender scene # the full graph as JSON, when a number surprises you
96
+ npx genex blender reset # empty scene (the namespace resets too)
97
+ ```
98
+
99
+ Keep each step under about 90 seconds of Blender time: a script that loops
100
+ hundreds of boolean cuts is killed at the deadline and the scene comes back from
101
+ the last checkpoint. Split heavy work across scripts. A level is 8–15 steps, not 50.
102
+
103
+ ## Craft rules that decide whether the export works
104
+
105
+ - **Metres.** Floor at `z = 0`, the map's centre at the origin. Blender is Z-up;
106
+ the export converts to Y-up, so the floor is the XZ plane in Three.js and a
107
+ Blender `(x, y, z)` lands at `(x, z, -y)`.
108
+ - **Name everything** `<zone>_<thing>` (`gate_portcullis`, `hall_table_1`). Names
109
+ survive the export and are how the game finds parts.
110
+ - **Materials: few and flat.** Six to eight for a whole level, one per surface
111
+ family (stone, dark stone, wood, roof, iron, glow). Set the Principled BSDF base
112
+ colour **and** `material.diffuse_color` — the sheet's fallback renderer reads the
113
+ second, and a scene that sets only the first exports with correct colours and
114
+ renders entirely grey. No image textures: they do not fit the export ceiling, and
115
+ an asset URL does not export.
116
+ - **Repetition by loop, never by an unapplied modifier.** Crenellations, columns,
117
+ pews: a Python `for`. An Array modifier that is not applied is not in the GLB.
118
+ - **Tri budget.** Keep the whole level under ~150k triangles and use 12–16 segments
119
+ on cylinders — phones load this file too. The export refuses above 6 MB; treat
120
+ that as "no longer a game asset", not as a limit to work around.
121
+ - **Markers.** Empties export as named nodes: `bpy.data.objects.new("spawn", None)`
122
+ placed and linked into the scene collection, likewise `key_1`, `door_crypt`,
123
+ `patrol_1`. The level tells the game where things go; the game reads them with
124
+ `scene.getObjectByName("spawn")`.
125
+ - **Lights stay in the game.** Blender lights do not export usefully; light the
126
+ level in Three.js (a hemisphere + a sun, point lights at torches and hearths).
127
+ - **Collision is the game's.** Static level geometry gets Rapier trimesh colliders
128
+ per mesh — `$genex-threejs-physics-rapier`. If a mesh is too dense to collide
129
+ against, add a simplified `col_<zone>` box in Blender and keep the visual one out
130
+ of physics.
131
+
132
+ A minimal first script, the shape every later one follows:
133
+
134
+ ```python
135
+ import bpy
136
+ bpy.ops.wm.read_factory_settings(use_empty=True)
137
+
138
+ def mat(name, rgb, rough=0.85):
139
+ m = bpy.data.materials.new(name)
140
+ m.use_nodes = True
141
+ b = m.node_tree.nodes["Principled BSDF"]
142
+ b.inputs["Base Color"].default_value = (*rgb, 1.0)
143
+ b.inputs["Roughness"].default_value = rough
144
+ m.diffuse_color = (*rgb, 1.0) # BOTH, or the sheet renders grey
145
+ return m
146
+
147
+ def box(name, loc, half, m):
148
+ bpy.ops.mesh.primitive_cube_add(size=2.0, location=loc)
149
+ o = bpy.context.active_object
150
+ o.name, o.scale = name, half
151
+ o.data.materials.append(m)
152
+ return o
153
+
154
+ STONE = mat("Stone", (0.46, 0.44, 0.41))
155
+ box("yard_floor", (0, 0, -0.25), (35, 35, 0.25), mat("Ground", (0.30, 0.28, 0.24)))
156
+ for sx, sy in ((1, 1), (1, -1), (-1, 1), (-1, -1)):
157
+ bpy.ops.mesh.primitive_cylinder_add(radius=3, depth=14, vertices=16,
158
+ location=(sx * 35, sy * 35, 7))
159
+ t = bpy.context.active_object
160
+ t.name = f"tower_{sx}_{sy}"
161
+ t.data.materials.append(STONE)
162
+ print("shell:", len(bpy.context.scene.objects), "objects")
163
+ ```
164
+
165
+ ## Hero pieces from `genex model`
166
+
167
+ ```bash
168
+ npx genex model "weathered stone knight effigy on a tomb, medieval" # prints a URL
169
+ npx genex blender import https://assets.genex.technology/generations/<id>/model-glb
170
+ ```
171
+
172
+ `import` prints the object names it added and renders a fresh sheet; place and
173
+ scale them in the next `exec` (`bpy.data.objects["…"].location = (…)`). Import
174
+ when the piece must be composed WITH the architecture (a gate in a wall, a statue
175
+ in a niche). When it is dressing, skip the import: drop a marker in Blender and
176
+ load the GLB in Three.js at that marker — it keeps the level file small.
177
+
178
+ ## Export and load
179
+
180
+ ```bash
181
+ npx genex blender export --out public/assets/level.glb
182
+ ```
183
+
184
+ Load it like any GLB. The quality kit's rung ladder applies only to generated
185
+ `/generations/` URLs, so this file loads as-is — the tri and material budgets
186
+ above are what keep it phone-safe.
187
+
188
+ ```ts
189
+ import { createGltfLoader } from "./controllers/quality/gltf-loader.ts";
190
+
191
+ const gltfLoader = createGltfLoader(renderer);
192
+ const level = await gltfLoader.loader.loadAsync("/assets/level.glb");
193
+ level.scene.traverse((o) => {
194
+ if ((o as THREE.Mesh).isMesh) { o.castShadow = true; o.receiveShadow = true; }
195
+ });
196
+ scene.add(level.scene);
197
+ const spawn = level.scene.getObjectByName("spawn");
198
+ ```
199
+
200
+ Then colliders (`$genex-threejs-physics-rapier`), the player
201
+ (`$genex-threejs-character-controller`), lights, and a real screenshot check
202
+ (`$genex-threejs-visual-validation`). Record the level in `DESIGN.md`'s Assets as
203
+ `local Blender build — scripts under blender/`, never a provider generation id.
204
+
205
+ ## Money
206
+
207
+ The seat bills a few credits **per minute while it is held, idle or not**, and it
208
+ ends with the session — there is no close command. So build the level in one
209
+ sitting: shell → zones → hero pieces → export, then move on to the game. Batch
210
+ changes into fewer, larger scripts; every step renders one sheet.
211
+
212
+ ## Troubleshooting
213
+
214
+ - `payload_too_large` on export → fewer cylinder segments, delete faces the player
215
+ never sees, or export two GLBs (`--out public/assets/level-a.glb`).
216
+ - The sheet is grey → `material.diffuse_color` was not set (see above).
217
+ - `refused the seat token (401)` → the seat was closed (the session rested, or the
218
+ pod was replaced); run the same command again — it re-acquires, and a replaced
219
+ pod restores your scene from its checkpoint.
220
+ - `seat_not_ready` → the pod is still booting; run again shortly.
221
+ - The numbers are right but the top view is wrong → coordinates: `y` in Blender is
222
+ depth, and `scale` on a 2-unit cube is the half-size.
@@ -132,6 +132,7 @@ vendored code from memory of another engine.
132
132
  | phone-survivable rendering: device tiers, DPR/shadow/post budgets, runtime governor, generated-asset rungs, Quality picker, dispose-on-swap | `$genex-threejs-adaptive-quality` |
133
133
  | anything falls, collides, gets pushed, or needs colliders/events | `$genex-threejs-physics-rapier` |
134
134
  | launch/docking timelines, authored transform phases, springs, convergence, deterministic prop/debris motion | `$genex-threejs-procedural-animation` |
135
+ | a level, arena, interior, building or map to walk through — a composed space of rooms, lanes, walls and sites, from a description or a floor plan; a blockout | `$genex-blender-scene` — probe the lane first (`npx genex blender scene`); when it is off, the same space is built as code via `$genex-threejs-procedural-assets` |
135
136
  | rebuild a reference prop, hard-surface object, modular decoration, or structural environment piece (kits, fences, walls, paving) as editable parameterized Three.js code — held to the asset bar at the end of this section | `$genex-threejs-procedural-assets` |
136
137
  | stylized/abstract shader-authored materials, the procedural/PBR material boundary | `$genex-threejs-procedural-assets` |
137
138
  | particles, trails, plasma, shockwaves, pooled bursts, and event effects | `$genex-threejs-procedural-vfx` |
@@ -164,8 +165,10 @@ honestly fit: a generated GLB built from the picture itself
164
165
  (`npx genex model --image <path>`), or editable parameterized Three.js code.
165
166
  If the request says procedural, parametric, code-built, reusable variations,
166
167
  or names a GLB, start the corresponding route directly. An image alone never
167
- auto-triggers the procedural lane. Player bodies, rigged characters/creatures,
168
- and animation stay with their protected owners.
168
+ auto-triggers the procedural lane. A floor plan, a room photo or a map sketch
169
+ is a SPACE, not an object: it goes to `$genex-blender-scene` with no route
170
+ question. Player bodies, rigged characters/creatures, and animation stay with
171
+ their protected owners.
169
172
 
170
173
  If a procedural request has a private reference, use it locally and do not
171
174
  upload, publish, or commit it without explicit permission. If no reference was
@@ -107,7 +107,10 @@ parametric, or code-built work. It is a first-class lane, not a fallback, held
107
107
  to the same bar as a generated GLB, and mixing it with generated GLBs in one
108
108
  scene is the normal way to build a detailed world. An image alone still doesn't decide the route (ask the one
109
109
  route question when both honestly fit), and never use it for player bodies,
110
- rigged characters/creatures, or animation.
110
+ rigged characters/creatures, or animation. An OBJECT is this lane's unit; a
111
+ SPACE the player moves through — a level, an interior, a whole building — is
112
+ `$genex-blender-scene`'s, which falls back to this lane when the Blender lane
113
+ is off on the stand.
111
114
 
112
115
  - Prefer a supplied reference. A private attachment stays local unless the
113
116
  player explicitly permits upload, publication, or commit.