@bitmagic/cli 0.1.53-dev.1 → 0.1.53-dev.11

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.
Files changed (111) hide show
  1. package/README.md +140 -36
  2. package/dist/assets/revoxelize.d.ts +61 -1
  3. package/dist/assets/revoxelize.js +157 -13
  4. package/dist/assets/revoxelize.js.map +1 -1
  5. package/dist/cli.d.ts +35 -1
  6. package/dist/cli.js +18 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/assets.js +3 -2
  9. package/dist/commands/assets.js.map +1 -1
  10. package/dist/commands/build.d.ts +4 -0
  11. package/dist/commands/build.js +7 -1
  12. package/dist/commands/build.js.map +1 -1
  13. package/dist/commands/cover.d.ts +5 -0
  14. package/dist/commands/cover.js +3 -0
  15. package/dist/commands/cover.js.map +1 -1
  16. package/dist/commands/dev.js +1 -2
  17. package/dist/commands/dev.js.map +1 -1
  18. package/dist/commands/forge.d.ts +4 -0
  19. package/dist/commands/forge.js +9 -0
  20. package/dist/commands/forge.js.map +1 -1
  21. package/dist/commands/generate.d.ts +25 -0
  22. package/dist/commands/generate.js +80 -8
  23. package/dist/commands/generate.js.map +1 -1
  24. package/dist/commands/init.d.ts +4 -0
  25. package/dist/commands/init.js +12 -1
  26. package/dist/commands/init.js.map +1 -1
  27. package/dist/commands/publish.d.ts +39 -5
  28. package/dist/commands/publish.js +102 -21
  29. package/dist/commands/publish.js.map +1 -1
  30. package/dist/commands/reload.js +1 -3
  31. package/dist/commands/reload.js.map +1 -1
  32. package/dist/commands/reset-account.js +25 -8
  33. package/dist/commands/reset-account.js.map +1 -1
  34. package/dist/commands/self-update.d.ts +67 -0
  35. package/dist/commands/self-update.js +196 -0
  36. package/dist/commands/self-update.js.map +1 -0
  37. package/dist/commands/upgrade.js +5 -2
  38. package/dist/commands/upgrade.js.map +1 -1
  39. package/dist/editor/asset-detail-panel.d.ts +2 -2
  40. package/dist/editor/asset-detail-panel.js +5 -2
  41. package/dist/editor/asset-detail-panel.js.map +1 -1
  42. package/dist/editor/cli-update.d.ts +32 -0
  43. package/dist/editor/cli-update.js +67 -0
  44. package/dist/editor/cli-update.js.map +1 -0
  45. package/dist/editor/publish-panel.d.ts +31 -0
  46. package/dist/editor/publish-panel.js +69 -0
  47. package/dist/editor/publish-panel.js.map +1 -0
  48. package/dist/editor/server.js +43 -1
  49. package/dist/editor/server.js.map +1 -1
  50. package/dist/editor/shell-page.js +249 -8
  51. package/dist/editor/shell-page.js.map +1 -1
  52. package/dist/forge/upload-proxy.d.ts +14 -0
  53. package/dist/forge/upload-proxy.js +28 -1
  54. package/dist/forge/upload-proxy.js.map +1 -1
  55. package/dist/generate/model.d.ts +7 -0
  56. package/dist/generate/model.js +13 -3
  57. package/dist/generate/model.js.map +1 -1
  58. package/dist/generate/prop.d.ts +20 -0
  59. package/dist/generate/prop.js +22 -2
  60. package/dist/generate/prop.js.map +1 -1
  61. package/dist/generate/vehicle.d.ts +2 -0
  62. package/dist/generate/vehicle.js +1 -0
  63. package/dist/generate/vehicle.js.map +1 -1
  64. package/dist/project/dev-handle.d.ts +28 -0
  65. package/dist/project/dev-handle.js +43 -0
  66. package/dist/project/dev-handle.js.map +1 -1
  67. package/dist/project/jobs.js +11 -9
  68. package/dist/project/jobs.js.map +1 -1
  69. package/dist/publish/bundle.d.ts +35 -8
  70. package/dist/publish/bundle.js +41 -13
  71. package/dist/publish/bundle.js.map +1 -1
  72. package/dist/publish/client.d.ts +13 -2
  73. package/dist/publish/client.js.map +1 -1
  74. package/dist/publish/record.d.ts +22 -0
  75. package/dist/publish/record.js +62 -0
  76. package/dist/publish/record.js.map +1 -0
  77. package/dist/render/qr-output.d.ts +44 -0
  78. package/dist/render/qr-output.js +71 -0
  79. package/dist/render/qr-output.js.map +1 -0
  80. package/dist/render/qr-png.d.ts +37 -0
  81. package/dist/render/qr-png.js +111 -0
  82. package/dist/render/qr-png.js.map +1 -0
  83. package/dist/render/qr-terminal.d.ts +63 -0
  84. package/dist/render/qr-terminal.js +142 -0
  85. package/dist/render/qr-terminal.js.map +1 -0
  86. package/dist/render/qr.d.ts +51 -0
  87. package/dist/render/qr.js +516 -0
  88. package/dist/render/qr.js.map +1 -0
  89. package/dist/scaffold/project-files.d.ts +42 -9
  90. package/dist/scaffold/project-files.js +242 -43
  91. package/dist/scaffold/project-files.js.map +1 -1
  92. package/dist/scaffold/project.js +2 -1
  93. package/dist/scaffold/project.js.map +1 -1
  94. package/dist/scaffold/suggest-template.d.ts +2 -0
  95. package/dist/scaffold/suggest-template.js +2 -1
  96. package/dist/scaffold/suggest-template.js.map +1 -1
  97. package/dist/telemetry/command-context.d.ts +12 -0
  98. package/dist/telemetry/command-context.js +7 -0
  99. package/dist/telemetry/command-context.js.map +1 -1
  100. package/dist/telemetry/creator-prompt.d.ts +35 -0
  101. package/dist/telemetry/creator-prompt.js +45 -0
  102. package/dist/telemetry/creator-prompt.js.map +1 -0
  103. package/dist/update/check.js +2 -1
  104. package/dist/update/check.js.map +1 -1
  105. package/dist/update/notice.d.ts +31 -1
  106. package/dist/update/notice.js +39 -5
  107. package/dist/update/notice.js.map +1 -1
  108. package/dist/update/self-install.d.ts +175 -0
  109. package/dist/update/self-install.js +335 -0
  110. package/dist/update/self-install.js.map +1 -0
  111. package/package.json +4 -4
package/README.md CHANGED
@@ -49,6 +49,9 @@ npm install -g @bitmagic/cli
49
49
  bitmagic --version
50
50
  ```
51
51
 
52
+ That is the only time you need npm. From then on, `bitmagic self-update` updates the CLI — see
53
+ [Staying current](#staying-current) for why the npm line is the wrong tool for the second install.
54
+
52
55
  ### The dev line
53
56
 
54
57
  ```bash
@@ -61,9 +64,10 @@ installs the unreleased build the Bitmagic team and external testers run — a p
61
64
  production, so a CLI that uses them has to be installable before it is released. Pair it with
62
65
  `bitmagic login --env dev`.
63
66
 
64
- `npm install -g @bitmagic/cli` puts you back on the release at any time. One global install is a
65
- single `bitmagic` on your PATH, so `bitmagic --version` is how you tell which line you are on: a
66
- `-dev.N` suffix means the dev line. To have both at once, see below.
67
+ `bitmagic self-update --tag latest` puts you back on the release at any time, and `--tag dev` takes
68
+ you the other way. One global install is a single `bitmagic` on your PATH, so `bitmagic --version`
69
+ is how you tell which line you are on: a `-dev.N` suffix means the dev line. To have both at once,
70
+ see below.
67
71
 
68
72
  ### Running both lines side by side
69
73
 
@@ -113,6 +117,11 @@ env=local
113
117
  apiUrl=http://localhost:3102
114
118
  ```
115
119
 
120
+ `bitmagic self-update` works inside this setup unchanged: it reinstalls into the root the
121
+ `bitmagic` you just ran came from, with the same `--global-dir` and `--global-bin-dir`, so updating
122
+ from `dev-games/` cannot disturb the prod root. (Running `pnpm add -g` by hand would, unless you
123
+ repeat both flags.)
124
+
116
125
  `direnv allow` each, and both halves are right in each tree:
117
126
 
118
127
  ```
@@ -184,11 +193,12 @@ browser reloads itself on every change and the assets panel shows each generatio
184
193
  | `bitmagic whoami [--env <env>]` | Show the identity the CLI is authenticated as — the account's email address alongside its user id — and which environment answered, including where that environment was chosen from. |
185
194
  | `bitmagic init [dir] [--template <id>] [--name <name>] [--idea "<pitch>"] [--env <env>] [--force]` | Scaffold a new project: mints a game, downloads the vendored engine, writes `AGENTS.md`, `GAME-DESIGN.md` and templates. Starts from `empty-3d` unless told otherwise. `--idea` seeds the design doc's pitch **and** picks the template whose camera and controls fit it — "a doom-like corridor shooter" scaffolds `first-person`, "jump between floating islands" scaffolds `sidescroller`. `--template` overrides both; a suggestion that fails for any reason falls back to `empty-3d` rather than failing the scaffold. Dependencies are installed with **pnpm** when your machine has it and npm otherwise — the lockfile that produces is what every later command reads, so it never tells you to run the wrong one. |
186
195
  | `bitmagic check` | Typecheck (`tsc --noEmit`) against the vendored engine. Fast; does not prove the game runs. |
187
- | `bitmagic dev [--port <port>] [--editor-port <port>] [--no-open]` | Build, then serve everything and keep it running: `tsc --watch`, `vite` on 3010, and a **Game / Editor** view on 3011 that reloads itself when the project changes. Opens the view in your browser; `--no-open` (or `BITMAGIC_NO_OPEN=1`) skips that. The one command to leave open. See **The dev view** below. |
196
+ | `bitmagic dev [--port <port>] [--editor-port <port>] [--no-open]` | Build, then serve everything and keep it running: `tsc --watch`, `vite` on 3010, and a **Game / Editor** view on 3011 that reloads itself when the project changes, and shows where the game is published (with a QR code) once it has been. Opens the view in your browser; `--no-open` (or `BITMAGIC_NO_OPEN=1`) skips that. The one command to leave open. See **The dev view** below. |
188
197
  | `bitmagic reload [--port <port>]` | Tell a running `bitmagic dev` to reload the browser now. For agents: run it when you finish a round of edits. Exits 0 and does nothing when no dev server is running. |
189
198
  | `bitmagic verify [--timeout <ms>] [--renderer webgpu\|webgl] [--platform desktop\|mobile\|both] [--fast] [--watch]` | Build, boot the game in a headless browser, press the engine's Play button like a player would, and report what broke. A game without the button passes as long as gameplay starts by itself (auto-starting genres, custom start UIs); a run where gameplay never starts fails. The settle is adaptive: the run ends as soon as the live engine proves stable gameplay (state, rendered frames, no fresh errors, player standing), with `--timeout` as the cap. The verdict includes live engine state — actual GPU backend, fps, player position, physics body count, gameplay events (see **Gameplay events** below) — recorded in `result.json`. `--renderer` pins the pipeline (default `webgpu`, what players run; a silent WebGPU→WebGL2 fallback **fails** the run, and `--renderer webgl` is the deliberate escape hatch). A run failing on a lost WebGL context retries once on software rendering automatically. `--fast` skips the screenshot pipeline (then there is no thumbnail candidate from this run). `--watch` keeps everything warm — incremental `tsc --watch`, one vite, one browser — and re-verifies on file change in seconds; `--fast` is the watch default (`--no-fast` restores the screenshot). `--platform` picks what the run emulates — see **Verifying the mobile path** below. Every run also reports the engine's mobile-parity check: a desktop action with no touch button is a warning, and a **failure** when `game.json` declares `primaryPlatform: "mobile"`. Writes the artifacts the publish gate reads (see below). |
190
- | `bitmagic build` | Bundle the game into one self-contained `.bitmagic/build/index.html`. Rarely needs to be run by hand — `publish` builds automatically when the project changed. |
191
- | `bitmagic publish [--visibility public\|private] [--name <name>] [--description <desc>] [--force]` | Verify-gate, build if needed, and upload straight to GCS. **Private unless `--visibility public` is passed.** |
199
+ | `bitmagic build [--single-file]` | Bundle the game into a single `.bitmagic/build/index.html`. The engine, your game and its data are inlined; three.js, Rapier and the other third-party packages load from a CDN at the versions your import map pins — the same shape a game published from bitmagic.ai has. `--single-file` inlines those too, for a game that has to run with no network (offline, `file://`, a packaged shell); the file is several megabytes bigger. Rarely needs to be run by hand — `publish` builds automatically when the project changed. |
200
+ | `bitmagic publish [--visibility public\|private] [--name <name>] [--description <desc>] [--force] [--single-file] [--no-qr]` | Verify-gate, build if needed, and upload straight to GCS. `--single-file` publishes the offline bundle described under `bitmagic build`; switching the flag between runs always rebuilds, since it changes nothing on disk for the reuse check to notice. **Keeps the game's current visibility**; a new game is private until published with `--visibility public`. Shows the published URL as a QR code so you can scan it onto a phone — drawn in the terminal, or written to `.bitmagic/publish-qr.png` when it cannot be. `--no-qr` skips both. |
201
+ | `bitmagic self-update [--tag latest\|dev]` | Update the **CLI itself** to the newest build of its line, installing into the place this CLI actually lives rather than wherever the `npm` on your `PATH` would put it — which under nvm is routinely somewhere else. Reads the project's line from `bitmagic.json` when there is one, so it also fixes a wrong-line install; `--tag` overrides. Verifies the version on disk moved, and refuses (naming the path) for an `npx` run, a source checkout, or an install shape it cannot recognise. Not to be confused with `upgrade`, which is about the project's engine. |
192
202
  | `bitmagic upgrade [--engine <version>] [--force]` | Refresh vendored platform files and the CLI's own skills to the currently published engine. Refuses a dirty git tree unless `--force`. Also records the project's `environment` in `bitmagic.json` if it has none yet — the engine it just vendored came from that api-server — and says so when it does. |
193
203
  | `bitmagic generate <skybox\|background\|block-type\|sound\|image\|character\|animation\|model\|prop\|vehicle> ...` | Generate one asset directly into `src/work/world.json`. Costs sparks. `model` mints a **new** 3D object from a prompt, `prop` upgrades an existing placeholder **in place**, `vehicle` adds a new drivable one, `animation` takes a parameter spec rather than a prompt, `block-type` registers a custom voxel block — see below. `skybox` and `background` write the **same** field: pick one. |
194
204
  | `bitmagic assets add <file> [--name <n>] [--asset-id <id>] [--keep-glb] [--voxel-size <m>] [--max-voxel-size <m>] [--height <m>] [--hollow\|--solid] [--force]` | Upload **your own** file — `.glb`, `.vxl`, `.png/.jpg/.webp`, `.opus/.ogg/.m4a/.mp3`, `.json` — into the game's assets and write its entry into `src/work/world.json`. A `.glb` is voxelized in a headless browser (Chrome + a free port in 3000–3199, like `prop`) unless `--keep-glb`. Free — no sparks. See **Adding your own assets** below. |
@@ -411,6 +421,21 @@ While the game boots, the mark hops over the stage — the first time only. Late
411
421
  still, and only if the reload is slow enough to be worth explaining. If a boot fails it stays put,
412
422
  says what went wrong and offers to try again, instead of leaving you a black rectangle.
413
423
 
424
+ ### Where your game is published
425
+
426
+ Once the project has been published, a **Published** pill appears in the top bar. It opens the
427
+ game's address, the visibility it went out with, which publish this is and how long ago — and the
428
+ same QR code the terminal draws, so the phone in your hand can reach the build without you typing
429
+ anything.
430
+
431
+ The pill lights up on its own within a second or two of a publish finishing, with no reload: the
432
+ page polls, and the publish it should show almost always happens in another terminal — often your
433
+ agent's, which you never look at. Until the first publish the pill is there but dead, because a
434
+ control that opens an empty dialog is worse than one that is plainly not ready.
435
+
436
+ It works even when the game will not boot, which is deliberate: that is the moment the link matters
437
+ most, since the last good build is still live at that address.
438
+
414
439
  Put the page beside your agent's window. It is one page with
415
440
  two tabs over the same running game, so switching costs nothing and does not restart anything:
416
441
 
@@ -727,12 +752,18 @@ manage the registry, not gameplay.
727
752
  publish rebuilds automatically; a prior manual `bitmagic build` is never required, only useful
728
753
  if you want to inspect `.bitmagic/build/index.html` before it ships.
729
754
 
730
- **Publishing does not make a game public.** Every `bitmagic publish` uploads and registers the
731
- game, but by default `visibility` is `private` reachable by URL, not listed anywhere on
732
- bitmagic.ai. You must pass `--visibility public` explicitly, every time you want the game listed;
733
- omitting it on a later publish makes it private again — and takes the game's `/play/` page out of
734
- service, so a previously-public game re-published bare is gone from bitmagic.ai until you publish
735
- publicly again.
755
+ **Publishing does not make a game public.** A new game starts `private` reachable by URL, not
756
+ listed anywhere on bitmagic.ai. Pass `--visibility public` when you want it listed.
757
+
758
+ **Visibility is sticky.** `bitmagic publish` with no `--visibility` keeps the game exactly as it
759
+ is, so shipping an update to a public game leaves it public. You only need `--visibility` when you
760
+ want to *change* something: `public` to list it, `private` to take it out of service.
761
+
762
+ **Managing a published game** — changing its visibility, or taking it down for good — happens on
763
+ the web, at the **My games** page `publish` prints a link to (`bitmagic.ai/my-games/`). It is
764
+ the same page web-lane creators use, so there is one place to manage a game regardless of how it
765
+ was built. Withdrawing a game there unlists it and frees any custom `/play/` address; it does not
766
+ touch your project, and you can publish it again afterwards.
736
767
 
737
768
  `--force` overrides a **stale or failed** verify verdict. It does **not** override a missing
738
769
  verify record — a project with no cover art has only the verify screenshot to publish a thumbnail
@@ -744,6 +775,33 @@ from, so `bitmagic verify` must have produced at least one screenshot — `scree
744
775
  stick — and note that writing them into `game.json` yourself would change the fingerprint and
745
776
  invalidate the verify you just checked.
746
777
 
778
+ ### Testing it on a phone
779
+
780
+ Publish draws the URL it just printed as a QR code, so you can point a phone at the terminal
781
+ instead of retyping a twelve-character game id into a mobile browser. Scan it and the game opens.
782
+
783
+ This is the pro lane's "try it on my phone" loop, and `--visibility private` is the whole of it: a
784
+ private game is *unlisted*, not access-controlled, so its URL works from any device while the game
785
+ stays off bitmagic.ai. Publish as often as you like and scan the code each time.
786
+
787
+ **When the code cannot be drawn, publish writes it as a file instead** — `.bitmagic/publish-qr.png`
788
+ — and names the path (`qrPath` in the `--json` result). That is the normal case for an agent-driven
789
+ publish: your agent's stdout is a pipe, not a terminal, so there is nothing to paint a code onto,
790
+ and the person who would scan it is on the other side of that pipe. The image is how it reaches
791
+ them. The same happens under `--json`, for `TERM=dumb`, and when the terminal is narrower than the
792
+ code (37 columns for a typical publish URL), since a wrapped code cannot be scanned anyway. A
793
+ terminal that draws the code successfully writes no file.
794
+
795
+ `.bitmagic/` is excluded from the publish fingerprint, so the image cannot invalidate a verify.
796
+
797
+ The code is extra, never a substitute — the URL is always printed as a plain line above it, and
798
+ everything still works if neither a drawing nor a file appears. `--no-qr` and `BITMAGIC_NO_QR`
799
+ suppress both.
800
+
801
+ `bitmagic dev` deliberately has no equivalent. Its servers bind `127.0.0.1`, and the asset CDN
802
+ only serves assets to `localhost` origins — a phone pointed at your machine's LAN address would
803
+ load the game with no terrain and drop the player through the floor. Publish privately instead.
804
+
747
805
  ### Which platform the game is catalogued as
748
806
 
749
807
  Publish syncs two indicators from your project files, so bitmagic.ai lists the game as what it
@@ -792,7 +850,7 @@ refused is a bundle that is not a Bitmagic engine build at all (exit 1).
792
850
 
793
851
  | Code | Meaning |
794
852
  |---|---|
795
- | 1 | Build failed, or the uploaded bundle was not a Bitmagic engine build (check `vite.publish.config.js`). Not retryable — republishing the same bytes gets the same refusal. |
853
+ | 1 | Build failed, or the uploaded bundle was not a Bitmagic engine build (check `vite.publish.config.js`, or `vite.publish-standalone.config.js` for a `--single-file` build). Not retryable — republishing the same bytes gets the same refusal. |
796
854
  | 2 | Not logged in |
797
855
  | 3 | Verify missing, stale, or failed |
798
856
  | 4 | `bitmagic judge --min-score`: the judge ran, but the overall score is below the bar. The scorecard is in `.bitmagic/judge.json`. |
@@ -849,10 +907,12 @@ exact install command to run, using your project's own package manager (detected
849
907
  added needs that command run once before `bitmagic build` (and therefore `bitmagic publish`) will
850
908
  work.
851
909
 
852
- A project scaffolded before `vite.publish.config.js` existed cannot be bundled at all: `bitmagic
910
+ A project scaffolded before the bundle config it needs existed cannot be bundled at all: `bitmagic
853
911
  build` stops with a message naming `bitmagic upgrade`, which vendors the file without touching
854
- your game code. Run the printed install command afterwards if `upgrade` reports missing
855
- dependencies too.
912
+ your game code. This applies to `vite.publish-standalone.config.js` too, which arrived later than
913
+ `vite.publish.config.js` — so a project can have one and not the other, and the message names
914
+ whichever one your build actually needs. Run the printed install command afterwards if `upgrade`
915
+ reports missing dependencies too.
856
916
 
857
917
  ## Machine-readable output
858
918
 
@@ -867,7 +927,12 @@ parses one stream whether the command succeeded or not. The exit code remains au
867
927
 
868
928
  Without `--json` nothing changes: prose goes to stdout as before and no JSON is emitted.
869
929
 
870
- The result shape is per-command `publish` reports `url`/`visibility`/`publishVersion`, `init`
930
+ `--json` suppresses `publish`'s *drawn* QR code, which uses terminal escape sequences: under
931
+ `--json` stdout carries the result document and nothing else. The image is still written, and
932
+ `qrPath` in the result names it — that is the only way an agent learns the file exists.
933
+
934
+ The result shape is per-command — `publish` reports
935
+ `url`/`visibility`/`publishVersion`/`manageUrl`, plus `qrPath` when it wrote a QR image, `init`
871
936
  reports `gameId`/`root`/`dependenciesInstalled`/`packageManager`, `upgrade` reports `replaced` and any
872
937
  `missingDependencies` as structured entries rather than only as a printed install command, plus
873
938
  `cliUpdate` (`{ current, latest, updateAvailable }`, or `null` when the engine did not move and so
@@ -877,7 +942,39 @@ no check was made — which is not the same as "no update"), and `legacyAssetsRe
877
942
 
878
943
  ## Staying current
879
944
 
880
- Three nudges, all advisory — nothing is ever blocked because something is out of date.
945
+ ```bash
946
+ bitmagic self-update
947
+ ```
948
+
949
+ updates the CLI itself. Use it rather than `npm install -g @bitmagic/cli`, which is only for the
950
+ first install — see [Why self-update and not npm](#why-self-update-and-not-npm) below.
951
+
952
+ It installs into the exact place this CLI is already installed, on the line it is already on. Inside
953
+ a project it uses that project's line instead (`environment` in `bitmagic.json`), so running it in a
954
+ `dev` project from a release build crosses you over; `--tag dev` / `--tag latest` overrides both. It
955
+ reads back the version on disk afterwards and fails if it did not move, so a successful-looking
956
+ update that changed nothing is reported rather than believed.
957
+
958
+ It refuses, with the path it looked at, when there is nothing for it to do: a run through `npx`
959
+ (nothing is installed), a source checkout (`git pull` and rebuild), or an install shape it does not
960
+ recognise — it will not guess at where to write.
961
+
962
+ ### Why self-update and not npm
963
+
964
+ `npm install -g @bitmagic/cli` installs into whichever Node the `npm` on your `PATH` belongs to,
965
+ which is not necessarily the one this CLI is installed in. Under a version manager — nvm, fnm,
966
+ Volta — those two are routinely different, and a coding agent's non-interactive shell makes it
967
+ worse: it sources no profile, so nvm never loads and `npm` is whatever else is on the system path.
968
+ The install then succeeds, into a prefix nothing on your `PATH` points at, and `bitmagic --version`
969
+ never moves. The same applies to the two-root pnpm setup below, where a bare `npm i -g` writes to a
970
+ third place entirely.
971
+
972
+ `self-update` resolves its own install from the Node binary running it, so it cannot be aimed
973
+ anywhere else.
974
+
975
+ ### The nudges
976
+
977
+ Three, all advisory — nothing is ever blocked because something is out of date.
881
978
 
882
979
  - **The CLI itself.** Commands print a line when a newer `@bitmagic/cli` is on npm. The check reads
883
980
  a cache written by an earlier run, so it costs no time; the cache refreshes in the background at
@@ -885,15 +982,17 @@ Three nudges, all advisory — nothing is ever blocked because something is out
885
982
  every merge to `main` and can move several times an hour, so an older answer there is usually
886
983
  about a build several versions back. It asks about the line you are actually on — a build
887
984
  installed from `@dev` asks the `dev` dist-tag and hears about the next dev build, rather than
888
- being told the production release is "newer" and it names the install command for that line. An
889
- unreachable registry, a first-ever run, and a version with no recognisable numeric core all stay
890
- quiet.
985
+ being told the production release is "newer". An unreachable registry, a first-ever run, and a
986
+ version with no recognisable numeric core all stay quiet. `bitmagic dev` also shows it as a
987
+ banner in the browser, because that command runs for hours and the terminal line scrolls away.
891
988
  - **The CLI, after an engine bump.** When `bitmagic upgrade` actually moves the engine version, it
892
989
  asks npm there and then instead of trusting that cache — which can be a whole interval stale, and
893
990
  so silent about the CLI published alongside the engine you just vendored. The CLI and the engine
894
991
  ship together, so a newer engine can need build config or a command only the newer CLI has, and
895
- finding that out as a failed `bitmagic build` is worse than being told here. No request is made when the
896
- engine version did not change, and an unreachable registry costs the hint and nothing else.
992
+ finding that out as a failed `bitmagic build` is worse than being told here. It names the line the
993
+ *project* is on rather than the one you happen to be running, so it says `bitmagic self-update
994
+ --tag dev` where that is the crossing you need. No request is made when the engine version did not
995
+ change, and an unreachable registry costs the hint and nothing else.
897
996
  - **The vendored engine.** `bitmagic dev` and `bitmagic publish` say when a newer engine exists and
898
997
  suggest `bitmagic upgrade`. Publishing is never refused for an old engine — the prompt belongs
899
998
  where rebuilding is cheap, not at the moment you ship.
@@ -916,19 +1015,23 @@ and where the generators fall short. It is the same thing the web editor already
916
1015
  typed there, and it goes to the same place. None of it reaches Google Analytics, which only ever
917
1016
  receives counts, outcomes and timings.
918
1017
 
919
- If your agent passes it, the wording *you* used when you asked it is kept alongside the command's
920
- own text. That is optional and most agents will not send it.
921
-
922
- This does not include what you type into Claude Code, Codex or any other agent. Bitmagic never sees
923
- that the CLI only ever receives the arguments your agent chooses to pass it.
1018
+ **What you asked your agent for is kept too, when it passes it on.** The scaffolded project docs
1019
+ tell your agent to add `--original-prompt "<your request, verbatim>"`, so on those commands the
1020
+ wording *you* used — typically what you typed into Claude Code or Codex — is stored beside the
1021
+ command's own text and as a record of its own. The CLI has no other view of your agent's
1022
+ conversation: it sees only the arguments your agent chooses to pass it, and this is the one
1023
+ argument that is your words rather than its paraphrase. It is optional, never required, and never
1024
+ influences what gets generated.
924
1025
 
925
1026
  Set `DO_NOT_TRACK=1` in your environment to switch the end-of-command report off. Nothing is
926
- collected for it in that case — not even the file reads it would have needed.
1027
+ collected for it in that case — not even the file reads it would have needed. It also stops
1028
+ `--original-prompt` being forwarded: the CLI drops it at the point every command reads its flags,
1029
+ so no request carries it, whether or not your agent passed it.
927
1030
 
928
- It does **not** switch off the paragraph above. The text you pass to `forge`, `generate`, `cover`
929
- and `init --idea` is not telemetry: it is the request, and the server cannot generate anything
930
- without it. If you would rather it were not kept, the honest answer is that today there is no flag
931
- for that — tell us and we will add one.
1031
+ It does **not** switch off the paragraph before that one. The text you pass to `forge`, `generate`,
1032
+ `cover` and `init --idea` is not telemetry: it is the request, and the server cannot generate
1033
+ anything without it. If you would rather it were not kept, the honest answer is that today there is
1034
+ no flag for that — tell us and we will add one.
932
1035
 
933
1036
  The report is deliberately unobtrusive but it is not free: it is capped at 1.5 seconds per request
934
1037
  (two at worst, when your access token is due for a refresh first), it never prints, and it can never
@@ -953,9 +1056,10 @@ it, modified or not, free or paid, as a game engine, framework, SDK, library, st
953
1056
  game-creation tool.
954
1057
 
955
1058
  One practical consequence: a published game contains compiled engine code, so the licence requires
956
- the notice to travel with it. `vite.publish.config.js` carries a `banner` that does this
957
- automatically, along with the attribution three.js, Rapier and the other bundled libraries require.
958
- Leave it in place. `engine/THIRD-PARTY-NOTICES.md` has the full texts.
1059
+ the notice to travel with it. Both bundle configs carry a `banner` that does this automatically,
1060
+ along with the attribution three.js, Rapier and the other libraries require — which a
1061
+ `--single-file` build inlines outright, and a default build still ships loaders for. Leave it in
1062
+ place. `engine/THIRD-PARTY-NOTICES.md` has the full texts.
959
1063
 
960
1064
  ## Development
961
1065
 
@@ -45,12 +45,72 @@ export declare function findRevoxelizableAsset(world: Record<string, unknown>, a
45
45
  asset: Record<string, unknown>;
46
46
  source: RevoxelizeSource;
47
47
  };
48
+ /**
49
+ * A voxel size fit to show a person and to store.
50
+ *
51
+ * The `.vxl` header holds float32, so the size read back out of it is `0.019999999552965164` where
52
+ * the creator asked for `0.01` and got a doubling. Six significant figures is far beyond any
53
+ * resolution this means anything at, and it puts the number back in the vocabulary the flag uses —
54
+ * both in the note and in world.json, which would otherwise carry the artifact into every diff.
55
+ */
56
+ export declare function tidyVoxelSize(value: number): number;
57
+ /**
58
+ * What the bake actually produced, when that is not what was asked for.
59
+ *
60
+ * The engine's compiler halves resolution until the model fits a leaf budget
61
+ * (`compileVoxelModelToVxlAsset`: `while (cells.size > maxLeaves) { downsample; vs *= 2 }`, budget
62
+ * 600k for a working asset). It says so — but only as a `console.warn` inside the headless Chrome
63
+ * the CLI drives, which reaches nobody, and `REVOXELIZE_FROM_VXL_MASTER_RESULT` did not carry it.
64
+ * So a request just under the knee came back at exactly twice the size, silently, with a zero exit
65
+ * code.
66
+ *
67
+ * The effect is worse than "not quite what you asked for", which is why this is said out loud
68
+ * rather than left to whoever compares two numbers in world.json: the result is NON-MONOTONIC.
69
+ * One step below the knee the asset is coarser than a COARSER request would have produced —
70
+ * measured on a 2.78 m stall with a 512³ master, 0.011 m gave 556k voxels and 0.0105 m gave 141k.
71
+ * Nothing about "I asked for finer voxels and got a blockier model" suggests looking at a budget.
72
+ *
73
+ * Detected from the .vxl header rather than from anything the engine says, deliberately: the
74
+ * header is what was actually written, and the CLI drives whatever engine the project vendored —
75
+ * including ones that predate any reply field carrying this.
76
+ */
77
+ export declare function describeBudgetCoarsening(requested: number, achieved: number): {
78
+ coarsened: boolean;
79
+ factor: number;
80
+ note: string;
81
+ };
82
+ /**
83
+ * The height a master re-bake will be resampled to.
84
+ *
85
+ * `--height` wins. Otherwise the height the asset was BAKED at, which the master path records in
86
+ * `voxelizeSettings` for exactly this. Falling through to neither is the case worth naming: a
87
+ * master is a grid, not a size, so the engine has nothing to derive a height from and lands on its
88
+ * own `DEFAULT_TARGET_HEIGHT` of 2 m — silently returning a 10 m building as a 2 m one. The mesh
89
+ * branch has no such gap, which is why `--height`'s "omit to keep the source's own size" only ever
90
+ * described half of this command.
91
+ */
92
+ export declare function resolveMasterTargetHeight(asset: Record<string, unknown>, requested: number | undefined): number | undefined;
48
93
  /**
49
94
  * Refuse a master re-bake that asks for more detail than the master holds.
50
95
  *
51
96
  * The engine would refuse it too — `resampleMaster` throws "re-forge at a higher resolution rather
52
97
  * than upsampling" — but only after the browser, vite and the page have started. Saying it here
53
98
  * costs nothing and names the thing to do instead, which the engine's message cannot know.
99
+ *
100
+ * The floor is derived from the MASTER, not from the previous bake. Those are very different
101
+ * numbers: a 512³ master baked once at 0.1 m holds detail down to about `targetHeight / 512`, and
102
+ * refusing everything below 0.1 m would make that headroom unreachable from the CLI — which is the
103
+ * whole reason the master is stored rather than the working asset.
104
+ *
105
+ * Deliberately PERMISSIVE. The engine's real limit is the master's occupied span along the up
106
+ * axis, which is at most its resolution and usually less, so a request between
107
+ * `targetHeight / resolution` and `targetHeight / spanUp` still throws in the browser. That is the
108
+ * right way round: this pre-flight exists to avoid paying for a browser on a request that cannot
109
+ * work, never to refuse a size the master can serve. The engine stays the authority.
110
+ *
111
+ * Falls back to the old "no finer than the last bake" rule when either field is missing, so assets
112
+ * written before `sourceVxlMasterResolution` existed — or hand-edited ones — keep a guard rather
113
+ * than losing it.
54
114
  */
55
- export declare function assertMasterCanGoFiner(asset: Record<string, unknown>, requestedVoxelSize: number, name: string): void;
115
+ export declare function assertMasterCanGoFiner(asset: Record<string, unknown>, requestedVoxelSize: number, targetHeight: number | undefined, name: string): void;
56
116
  export declare function revoxelizeAsset(options: RevoxelizeOptions): Promise<RevoxelizeResult>;
@@ -3,11 +3,13 @@
3
3
  *
4
4
  * Every voxelized asset keeps what it was made from, and either source can be baked again:
5
5
  *
6
- * - `sourceVxlMasterUrl` — the voxel master, written by the direct-voxel path. Cheap (about a
7
- * megabyte) and already occupancy, so the engine only resamples and recompiles it.
6
+ * - `sourceVxlMasterUrl` — the voxel master, written by the direct-voxel path. Cheap (a few
7
+ * megabytes) and already occupancy, so the engine only resamples and recompiles it. It can go
8
+ * finer than a previous bake, but never finer than the grid it was forged at — masters are
9
+ * resampled by merging cells, never by inventing them.
8
10
  * - `sourceGlbUrl` — the mesh, written by the GLB path. Heavier (50-100 MB to refetch) and a full
9
- * re-voxelization, but the mesh is resolution-independent, so it is the only source that can go
10
- * FINER than any previous bake.
11
+ * re-voxelization, but the mesh is resolution-independent, so it is the only source with no
12
+ * ceiling at all.
11
13
  *
12
14
  * One command over both, because "re-bake this at a different size" is one thing a creator wants
13
15
  * and which pipeline produced the asset months ago is not something they should have to remember.
@@ -19,6 +21,7 @@
19
21
  * Writes under the SAME id, so every placed instance changes with it — the same upgrade-in-place
20
22
  * contract `generate prop` makes, and the reason this is not "add a second asset at a new size".
21
23
  */
24
+ import { propVoxelGrids } from '@bitmagic/asset-core';
22
25
  import { CliError } from '../errors.js';
23
26
  import { VOXELIZE_TIMEOUT_MS, withVoxelizeSession } from '../forge/voxelize-session.js';
24
27
  import { applyModificationsToWorld } from '../forge/apply-modifications.js';
@@ -65,20 +68,138 @@ function countInstances(world, assetId) {
65
68
  const objects = Array.isArray(world.environmentObjects) ? world.environmentObjects : [];
66
69
  return objects.filter((item) => isJsonObject(item) && item.assetId === assetId).length;
67
70
  }
71
+ /** A positive number from an unknown field, or undefined. */
72
+ function positiveNumber(value) {
73
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
74
+ }
75
+ /** The finest grid the generator will forge a master at. */
76
+ const MAX_MASTER_GRID = Math.max(...propVoxelGrids);
77
+ /**
78
+ * Floating-point slack for "the engine baked what I asked for".
79
+ *
80
+ * The requested size round-trips through a float32 header, so an exact `===` would report a
81
+ * coarsening on every bake. A real one is a DOUBLING, so anything under a few percent is noise.
82
+ */
83
+ const VOXEL_SIZE_EPSILON = 0.02;
84
+ /**
85
+ * A voxel size fit to show a person and to store.
86
+ *
87
+ * The `.vxl` header holds float32, so the size read back out of it is `0.019999999552965164` where
88
+ * the creator asked for `0.01` and got a doubling. Six significant figures is far beyond any
89
+ * resolution this means anything at, and it puts the number back in the vocabulary the flag uses —
90
+ * both in the note and in world.json, which would otherwise carry the artifact into every diff.
91
+ */
92
+ export function tidyVoxelSize(value) {
93
+ return Number(value.toPrecision(6));
94
+ }
95
+ /**
96
+ * What the bake actually produced, when that is not what was asked for.
97
+ *
98
+ * The engine's compiler halves resolution until the model fits a leaf budget
99
+ * (`compileVoxelModelToVxlAsset`: `while (cells.size > maxLeaves) { downsample; vs *= 2 }`, budget
100
+ * 600k for a working asset). It says so — but only as a `console.warn` inside the headless Chrome
101
+ * the CLI drives, which reaches nobody, and `REVOXELIZE_FROM_VXL_MASTER_RESULT` did not carry it.
102
+ * So a request just under the knee came back at exactly twice the size, silently, with a zero exit
103
+ * code.
104
+ *
105
+ * The effect is worse than "not quite what you asked for", which is why this is said out loud
106
+ * rather than left to whoever compares two numbers in world.json: the result is NON-MONOTONIC.
107
+ * One step below the knee the asset is coarser than a COARSER request would have produced —
108
+ * measured on a 2.78 m stall with a 512³ master, 0.011 m gave 556k voxels and 0.0105 m gave 141k.
109
+ * Nothing about "I asked for finer voxels and got a blockier model" suggests looking at a budget.
110
+ *
111
+ * Detected from the .vxl header rather than from anything the engine says, deliberately: the
112
+ * header is what was actually written, and the CLI drives whatever engine the project vendored —
113
+ * including ones that predate any reply field carrying this.
114
+ */
115
+ export function describeBudgetCoarsening(requested, achieved) {
116
+ const factor = achieved / requested;
117
+ if (!(factor > 1 + VOXEL_SIZE_EPSILON))
118
+ return { coarsened: false, factor: 1, note: '' };
119
+ return {
120
+ coarsened: true,
121
+ factor,
122
+ note: `note: baked at ${tidyVoxelSize(achieved)} m, not the ${requested} m requested — the model did not fit `
123
+ + 'the renderer\'s 600,000-leaf budget, so the engine halved the resolution to make it fit. '
124
+ + 'A slightly LARGER --voxel-size will come back finer than this one did; there is a knee '
125
+ + 'just above the size you asked for.',
126
+ };
127
+ }
128
+ /**
129
+ * What to actually do about a master that cannot go finer — which is NOT the same advice at every
130
+ * resolution.
131
+ *
132
+ * "Re-generate at a higher --voxel-grid" is a dead end once the master is already at
133
+ * {@link MAX_MASTER_GRID}: that is the top of the generator's allowlist, so there is no higher
134
+ * value to pass and the reader is sent to look for one that does not exist. At the ceiling the two
135
+ * things that DO still work are a resolution-independent source (the mesh path keeps a GLB, which
136
+ * has no ceiling at all) and a shorter bake — the floor is `targetHeight / resolution`, so the same
137
+ * cells spread over less world buy proportionally finer voxels.
138
+ */
139
+ function wayPastTheFloor(resolution) {
140
+ return resolution >= MAX_MASTER_GRID
141
+ ? `${MAX_MASTER_GRID} is the finest grid the generator offers, so there is no higher `
142
+ + '--voxel-grid to ask for: either bake it shorter (--height, which spreads the same cells '
143
+ + 'over less world) or re-generate WITHOUT --direct-voxels, whose mesh source has no '
144
+ + 'resolution ceiling.'
145
+ : `Re-generate it at a higher --voxel-grid (up to ${MAX_MASTER_GRID}) for more detail.`;
146
+ }
147
+ /**
148
+ * The height a master re-bake will be resampled to.
149
+ *
150
+ * `--height` wins. Otherwise the height the asset was BAKED at, which the master path records in
151
+ * `voxelizeSettings` for exactly this. Falling through to neither is the case worth naming: a
152
+ * master is a grid, not a size, so the engine has nothing to derive a height from and lands on its
153
+ * own `DEFAULT_TARGET_HEIGHT` of 2 m — silently returning a 10 m building as a 2 m one. The mesh
154
+ * branch has no such gap, which is why `--height`'s "omit to keep the source's own size" only ever
155
+ * described half of this command.
156
+ */
157
+ export function resolveMasterTargetHeight(asset, requested) {
158
+ if (requested !== undefined)
159
+ return requested;
160
+ const settings = asset.voxelizeSettings;
161
+ return isJsonObject(settings) ? positiveNumber(settings.targetHeight) : undefined;
162
+ }
68
163
  /**
69
164
  * Refuse a master re-bake that asks for more detail than the master holds.
70
165
  *
71
166
  * The engine would refuse it too — `resampleMaster` throws "re-forge at a higher resolution rather
72
167
  * than upsampling" — but only after the browser, vite and the page have started. Saying it here
73
168
  * costs nothing and names the thing to do instead, which the engine's message cannot know.
169
+ *
170
+ * The floor is derived from the MASTER, not from the previous bake. Those are very different
171
+ * numbers: a 512³ master baked once at 0.1 m holds detail down to about `targetHeight / 512`, and
172
+ * refusing everything below 0.1 m would make that headroom unreachable from the CLI — which is the
173
+ * whole reason the master is stored rather than the working asset.
174
+ *
175
+ * Deliberately PERMISSIVE. The engine's real limit is the master's occupied span along the up
176
+ * axis, which is at most its resolution and usually less, so a request between
177
+ * `targetHeight / resolution` and `targetHeight / spanUp` still throws in the browser. That is the
178
+ * right way round: this pre-flight exists to avoid paying for a browser on a request that cannot
179
+ * work, never to refuse a size the master can serve. The engine stays the authority.
180
+ *
181
+ * Falls back to the old "no finer than the last bake" rule when either field is missing, so assets
182
+ * written before `sourceVxlMasterResolution` existed — or hand-edited ones — keep a guard rather
183
+ * than losing it.
74
184
  */
75
- export function assertMasterCanGoFiner(asset, requestedVoxelSize, name) {
76
- const existing = asset.voxelSize;
77
- if (typeof existing !== 'number' || requestedVoxelSize >= existing)
185
+ export function assertMasterCanGoFiner(asset, requestedVoxelSize, targetHeight, name) {
186
+ const resolution = positiveNumber(asset.sourceVxlMasterResolution);
187
+ if (resolution !== undefined && targetHeight !== undefined) {
188
+ const floor = targetHeight / resolution;
189
+ if (requestedVoxelSize >= floor)
190
+ return;
191
+ throw new CliError(`"${name}" has a ${resolution}³ voxel master, which at ${targetHeight} m holds detail down `
192
+ + `to about ${floor.toPrecision(3)} m — ${requestedVoxelSize} m would be upsampling, and a `
193
+ + `master is only ever resampled coarser. ${wayPastTheFloor(resolution)} `
194
+ + 'world.json is unchanged.');
195
+ }
196
+ const existing = positiveNumber(asset.voxelSize);
197
+ if (existing === undefined || requestedVoxelSize >= existing)
78
198
  return;
79
- throw new CliError(`"${name}" was baked at ${existing} m and its voxel master cannot be resampled finer than `
80
- + `itself, so ${requestedVoxelSize} m would be upsampling. Re-generate it insteador use a `
81
- + 'voxel size at or above the current one. world.json is unchanged.');
199
+ throw new CliError(`"${name}" was baked at ${existing} m and records no master resolution, so there is nothing `
200
+ + `saying its master can be resampled finer than that${requestedVoxelSize} m may be `
201
+ + 'upsampling. Re-generate it instead, or use a voxel size at or above the current one. '
202
+ + 'world.json is unchanged.');
82
203
  }
83
204
  export async function revoxelizeAsset(options) {
84
205
  const { context, assetId } = options;
@@ -115,7 +236,12 @@ export async function revoxelizeAsset(options) {
115
236
  */
116
237
  async function rebakeFromMaster(options) {
117
238
  const { context, environment, token, assetId, voxel, assetName, source, log } = options;
118
- assertMasterCanGoFiner(options.asset, voxel.minVoxelSize, assetName);
239
+ const targetHeight = resolveMasterTargetHeight(options.asset, voxel.targetHeight);
240
+ assertMasterCanGoFiner(options.asset, voxel.minVoxelSize, targetHeight, assetName);
241
+ if (targetHeight === undefined) {
242
+ log(`note: "${assetName}" records no baked height, so the engine's 2 m default applies. `
243
+ + 'Pass --height to keep it the size it is.');
244
+ }
119
245
  const runSession = options.session ?? withVoxelizeSession;
120
246
  const retryHint = `Nothing was spent — the master is still at ${source.masterUrl}, so this is `
121
247
  + 'safe to retry.';
@@ -130,7 +256,9 @@ async function rebakeFromMaster(options) {
130
256
  options: {
131
257
  minVoxelSize: voxel.minVoxelSize,
132
258
  maxVoxelSize: voxel.maxVoxelSize,
133
- ...(voxel.targetHeight !== undefined ? { targetHeight: voxel.targetHeight } : {}),
259
+ // The RESOLVED height, not `voxel.targetHeight`: omitting it lands on the engine's 2 m
260
+ // default, which resizes every asset that is not 2 m tall.
261
+ ...(targetHeight !== undefined ? { targetHeight } : {}),
134
262
  },
135
263
  }, 'REVOXELIZE_FROM_VXL_MASTER_RESULT', VOXELIZE_TIMEOUT_MS, { maxRetries: 1 });
136
264
  if (!result)
@@ -155,6 +283,9 @@ async function rebakeFromMaster(options) {
155
283
  if (summary === null) {
156
284
  throw new CliError(`The engine returned bytes that are not a readable .vxl for "${assetName}". ${retryHint}`);
157
285
  }
286
+ const achieved = describeBudgetCoarsening(voxel.minVoxelSize, summary.minVoxelSize);
287
+ if (achieved.coarsened)
288
+ log(achieved.note);
158
289
  const timestamp = Date.now();
159
290
  const upload = await uploadProjectFile(environment, token, {
160
291
  gameId: context.metadata.gameId,
@@ -168,9 +299,22 @@ async function rebakeFromMaster(options) {
168
299
  size: bytes.byteLength,
169
300
  boundingBox: summary.boundingBox,
170
301
  boundingBoxInMeters: true,
171
- voxelSize: summary.minVoxelSize,
302
+ voxelSize: tidyVoxelSize(summary.minVoxelSize),
172
303
  fragmentCount: summary.fragmentCount,
173
304
  ...(summary.voxelCount !== undefined ? { voxelCount: summary.voxelCount } : {}),
305
+ // Re-recorded from what the bake ACHIEVED, never from what was asked for. A `--height` re-bake
306
+ // that left the old height here would leave the next run computing its master floor against a
307
+ // size that no longer matches the asset — and recording the requested voxel size next to a
308
+ // `voxelSize` the compiler coarsened leaves world.json asserting two resolutions for one file.
309
+ // `fillInterior` is meaningless on this path: a master already carries its own occupancy.
310
+ voxelizeSettings: {
311
+ minVoxelSize: tidyVoxelSize(summary.minVoxelSize),
312
+ // Scaled by the same factor, so the ladder the creator asked for (a 5x spread, say) survives
313
+ // a coarsening rather than silently narrowing to 2.5x.
314
+ maxVoxelSize: tidyVoxelSize(voxel.maxVoxelSize * achieved.factor),
315
+ ...(targetHeight !== undefined ? { targetHeight } : {}),
316
+ fillInterior: false,
317
+ },
174
318
  };
175
319
  }
176
320
  /**