tosijs-3d 0.7.0-beta.6 → 0.7.0

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 (215) hide show
  1. package/CHANGELOG.md +1213 -0
  2. package/LICENSE +190 -0
  3. package/Migration.md +102 -0
  4. package/README.md +56 -0
  5. package/dist/ambient-budget.d.ts +32 -0
  6. package/dist/ambient-budget.d.ts.map +1 -1
  7. package/dist/ambient-budget.js +39 -1
  8. package/dist/ambient-budget.js.map +1 -1
  9. package/dist/ambient-leaves.js +1 -1
  10. package/dist/ambient-leaves.js.map +1 -1
  11. package/dist/asset-url.js +1 -1
  12. package/dist/asset-url.js.map +1 -1
  13. package/dist/atmosphere.js +1 -1
  14. package/dist/atmosphere.js.map +1 -1
  15. package/dist/b3d-aircraft.d.ts +81 -0
  16. package/dist/b3d-aircraft.d.ts.map +1 -1
  17. package/dist/b3d-aircraft.js +215 -26
  18. package/dist/b3d-aircraft.js.map +1 -1
  19. package/dist/b3d-ambient.d.ts.map +1 -1
  20. package/dist/b3d-ambient.js +14 -6
  21. package/dist/b3d-ambient.js.map +1 -1
  22. package/dist/b3d-clouds.d.ts.map +1 -1
  23. package/dist/b3d-clouds.js +24 -5
  24. package/dist/b3d-clouds.js.map +1 -1
  25. package/dist/b3d-collisions.d.ts.map +1 -1
  26. package/dist/b3d-collisions.js +138 -0
  27. package/dist/b3d-collisions.js.map +1 -1
  28. package/dist/b3d-controllable.d.ts.map +1 -1
  29. package/dist/b3d-controllable.js +21 -4
  30. package/dist/b3d-controllable.js.map +1 -1
  31. package/dist/b3d-death.d.ts.map +1 -1
  32. package/dist/b3d-death.js +5 -0
  33. package/dist/b3d-death.js.map +1 -1
  34. package/dist/b3d-destroyable.js +1 -1
  35. package/dist/b3d-destroyable.js.map +1 -1
  36. package/dist/b3d-launcher.d.ts +46 -1
  37. package/dist/b3d-launcher.d.ts.map +1 -1
  38. package/dist/b3d-launcher.js +118 -10
  39. package/dist/b3d-launcher.js.map +1 -1
  40. package/dist/b3d-library.d.ts +39 -0
  41. package/dist/b3d-library.d.ts.map +1 -1
  42. package/dist/b3d-library.js +98 -18
  43. package/dist/b3d-library.js.map +1 -1
  44. package/dist/b3d-panel.d.ts.map +1 -1
  45. package/dist/b3d-panel.js +9 -1
  46. package/dist/b3d-panel.js.map +1 -1
  47. package/dist/b3d-primitives.d.ts +41 -0
  48. package/dist/b3d-primitives.d.ts.map +1 -1
  49. package/dist/b3d-primitives.js +53 -24
  50. package/dist/b3d-primitives.js.map +1 -1
  51. package/dist/b3d-probe.js +1 -1
  52. package/dist/b3d-probe.js.map +1 -1
  53. package/dist/b3d-quality.js +1 -1
  54. package/dist/b3d-quality.js.map +1 -1
  55. package/dist/b3d-radar-blip.d.ts.map +1 -1
  56. package/dist/b3d-radar-blip.js +5 -0
  57. package/dist/b3d-radar-blip.js.map +1 -1
  58. package/dist/b3d-radar.d.ts.map +1 -1
  59. package/dist/b3d-radar.js +76 -0
  60. package/dist/b3d-radar.js.map +1 -1
  61. package/dist/b3d-shadows.d.ts +18 -0
  62. package/dist/b3d-shadows.d.ts.map +1 -1
  63. package/dist/b3d-shadows.js +31 -2
  64. package/dist/b3d-shadows.js.map +1 -1
  65. package/dist/b3d-skybox.d.ts.map +1 -1
  66. package/dist/b3d-skybox.js +7 -1
  67. package/dist/b3d-skybox.js.map +1 -1
  68. package/dist/b3d-star-system.d.ts.map +1 -1
  69. package/dist/b3d-star-system.js +12 -0
  70. package/dist/b3d-star-system.js.map +1 -1
  71. package/dist/b3d-svg-plane.d.ts +40 -0
  72. package/dist/b3d-svg-plane.d.ts.map +1 -1
  73. package/dist/b3d-svg-plane.js +224 -19
  74. package/dist/b3d-svg-plane.js.map +1 -1
  75. package/dist/b3d-terrain.d.ts +1 -0
  76. package/dist/b3d-terrain.d.ts.map +1 -1
  77. package/dist/b3d-terrain.js +41 -1
  78. package/dist/b3d-terrain.js.map +1 -1
  79. package/dist/b3d-utils.d.ts +101 -0
  80. package/dist/b3d-utils.d.ts.map +1 -1
  81. package/dist/b3d-utils.js +141 -4
  82. package/dist/b3d-utils.js.map +1 -1
  83. package/dist/b3d-warhead.d.ts.map +1 -1
  84. package/dist/b3d-warhead.js +39 -14
  85. package/dist/b3d-warhead.js.map +1 -1
  86. package/dist/b3d-water.d.ts.map +1 -1
  87. package/dist/b3d-water.js +16 -0
  88. package/dist/b3d-water.js.map +1 -1
  89. package/dist/biome-chart.js +1 -1
  90. package/dist/biome-chart.js.map +1 -1
  91. package/dist/biome-plugin.js +1 -1
  92. package/dist/biome-plugin.js.map +1 -1
  93. package/dist/box.js +1 -1
  94. package/dist/box.js.map +1 -1
  95. package/dist/carve.js +1 -1
  96. package/dist/cloud-shadows.d.ts.map +1 -1
  97. package/dist/cloud-shadows.js +14 -7
  98. package/dist/cloud-shadows.js.map +1 -1
  99. package/dist/control-input.js +1 -1
  100. package/dist/control-input.js.map +1 -1
  101. package/dist/destroyable-behavior.js +1 -1
  102. package/dist/destroyable-behavior.js.map +1 -1
  103. package/dist/flow-layout.js +1 -1
  104. package/dist/flow-layout.js.map +1 -1
  105. package/dist/formations.js +1 -1
  106. package/dist/formations.js.map +1 -1
  107. package/dist/frame-panel.d.ts.map +1 -1
  108. package/dist/frame-panel.js +5 -0
  109. package/dist/frame-panel.js.map +1 -1
  110. package/dist/galaxy-data.js +1 -1
  111. package/dist/galaxy-data.js.map +1 -1
  112. package/dist/gamepad-focus.js +2 -2
  113. package/dist/gamepad-focus.js.map +1 -1
  114. package/dist/gamepad-svg.js +1 -1
  115. package/dist/gamepad-svg.js.map +1 -1
  116. package/dist/glass-gamepad.d.ts +17 -4
  117. package/dist/glass-gamepad.d.ts.map +1 -1
  118. package/dist/glass-gamepad.js +56 -16
  119. package/dist/glass-gamepad.js.map +1 -1
  120. package/dist/hardware-gamepad.js +1 -1
  121. package/dist/hardware-gamepad.js.map +1 -1
  122. package/dist/hud-math.js +1 -1
  123. package/dist/hud-math.js.map +1 -1
  124. package/dist/icon-data.d.ts.map +1 -1
  125. package/dist/icon-data.js +6 -0
  126. package/dist/icon-data.js.map +1 -1
  127. package/dist/icon-name.js +1 -1
  128. package/dist/icon-name.js.map +1 -1
  129. package/dist/index.d.ts +11 -4
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +22 -2
  132. package/dist/index.js.map +1 -1
  133. package/dist/key-layout.js +1 -1
  134. package/dist/key-layout.js.map +1 -1
  135. package/dist/landform.d.ts +3 -1
  136. package/dist/landform.d.ts.map +1 -1
  137. package/dist/landform.js +4 -2
  138. package/dist/landform.js.map +1 -1
  139. package/dist/make-mesh.d.ts +109 -0
  140. package/dist/make-mesh.d.ts.map +1 -0
  141. package/dist/make-mesh.js +229 -0
  142. package/dist/make-mesh.js.map +1 -0
  143. package/dist/medium.js +1 -1
  144. package/dist/mersenne-twister.js +1 -1
  145. package/dist/mersenne-twister.js.map +1 -1
  146. package/dist/min-sim-conformance.js +1 -1
  147. package/dist/min-sim-conformance.js.map +1 -1
  148. package/dist/model-transform.d.ts.map +1 -1
  149. package/dist/model-transform.js +58 -0
  150. package/dist/model-transform.js.map +1 -1
  151. package/dist/patch-field.js +1 -1
  152. package/dist/patch-field.js.map +1 -1
  153. package/dist/perf-probe.js +1 -1
  154. package/dist/perf-probe.js.map +1 -1
  155. package/dist/popup-chrome.d.ts +45 -0
  156. package/dist/popup-chrome.d.ts.map +1 -0
  157. package/dist/popup-chrome.js +85 -0
  158. package/dist/popup-chrome.js.map +1 -0
  159. package/dist/popup-surface.d.ts +119 -0
  160. package/dist/popup-surface.d.ts.map +1 -0
  161. package/dist/popup-surface.js +634 -0
  162. package/dist/popup-surface.js.map +1 -0
  163. package/dist/portal-transform.js +1 -1
  164. package/dist/prefab.js +1 -1
  165. package/dist/prefab.js.map +1 -1
  166. package/dist/radar.js +1 -1
  167. package/dist/radar.js.map +1 -1
  168. package/dist/rounded-rect.d.ts +37 -0
  169. package/dist/rounded-rect.d.ts.map +1 -0
  170. package/dist/rounded-rect.js +159 -0
  171. package/dist/rounded-rect.js.map +1 -0
  172. package/dist/shadow-decal.d.ts.map +1 -1
  173. package/dist/shadow-decal.js +3 -5
  174. package/dist/shadow-decal.js.map +1 -1
  175. package/dist/slope-profile.js +1 -1
  176. package/dist/slope-profile.js.map +1 -1
  177. package/dist/spatial-transform.js +1 -1
  178. package/dist/spatial-transform.js.map +1 -1
  179. package/dist/surface.js +1 -1
  180. package/dist/surface.js.map +1 -1
  181. package/dist/table-layout.d.ts.map +1 -1
  182. package/dist/table-layout.js +11 -1
  183. package/dist/table-layout.js.map +1 -1
  184. package/dist/text-edit.js +1 -1
  185. package/dist/text-edit.js.map +1 -1
  186. package/dist/tosi-b3d.d.ts +132 -5
  187. package/dist/tosi-b3d.d.ts.map +1 -1
  188. package/dist/tosi-b3d.js +977 -113
  189. package/dist/tosi-b3d.js.map +1 -1
  190. package/dist/touch-gamepad.d.ts.map +1 -1
  191. package/dist/touch-gamepad.js +5 -1
  192. package/dist/touch-gamepad.js.map +1 -1
  193. package/dist/virtual-gamepad.d.ts +1 -1
  194. package/dist/virtual-gamepad.js +2 -2
  195. package/dist/virtual-gamepad.js.map +1 -1
  196. package/dist/w3d-theme.d.ts +3 -0
  197. package/dist/w3d-theme.d.ts.map +1 -1
  198. package/dist/w3d-theme.js +18 -1
  199. package/dist/w3d-theme.js.map +1 -1
  200. package/dist/widget-box.js +1 -1
  201. package/dist/widget-box.js.map +1 -1
  202. package/dist/widgets3d-layout.js +1 -1
  203. package/dist/widgets3d-layout.js.map +1 -1
  204. package/dist/widgets3d.d.ts.map +1 -1
  205. package/dist/widgets3d.js +35 -2
  206. package/dist/widgets3d.js.map +1 -1
  207. package/dist/world-contract.js +1 -1
  208. package/dist/world-contract.js.map +1 -1
  209. package/dist/world-store.js +1 -1
  210. package/dist/world-store.js.map +1 -1
  211. package/dist/world-topology.js +1 -1
  212. package/dist/world-topology.js.map +1 -1
  213. package/dist/xr-gamepad.js +1 -1
  214. package/dist/xr-gamepad.js.map +1 -1
  215. package/package.json +10 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,1213 @@
1
+ # Changelog
2
+
3
+ <!--{ "order": 3 }-->
4
+
5
+ All notable changes to **tosijs-3d**. This project is pre-1.0 (`0.x`), so minor
6
+ versions may carry breaking peer-dependency changes — each is called out in a
7
+ **⚠️ Breaking** block in its version section below, with what a consumer must do.
8
+
9
+ ## 0.7.0
10
+
11
+ > **Prereleases, and why the numbering looks backwards.** `0.7.0-rc.1` was cut
12
+ > first, then work continued as `-beta.1…beta.6`. Semver sorts beta BELOW rc, so
13
+ > `bun add tosijs-3d@next` wrote `^0.7.0-beta.6` and RESOLVED BACKWARDS to the
14
+ > older rc.1 — silently, and `bun update` even reported the downgrade as an
15
+ > upgrade. **Publishing 0.7.0 is what un-inverts the channel**, since it sorts
16
+ > above every prerelease.
17
+ >
18
+ > The rule taken from it: **a prerelease channel never moves backwards** — the
19
+ > successor to `-rc.N` is `-rc.N+1`, never a beta. If you pinned any
20
+ > `^0.7.0-beta.*` or `0.7.0-rc.1`, move to `0.7.0`.
21
+ >
22
+ > - **0.7.0-beta.6** (2026-08-17) — carved landforms (the sdf-lattice page, the
23
+ > volcano cutaway, strata, 3D volcanic veins), portal math, and four
24
+ > manta-reported fixes: the explode+library crash that seized the scene (#24),
25
+ > mutual death eating the panel (#25), blasts blind to library destroyables
26
+ > (#28), and pitch authority becoming configurable and asymmetric (#26).
27
+ > - **0.7.0-beta.5** (2026-08-15) — `MediumOptics` + `fogLayerFor`
28
+ > (⚠️ experimental, not load-bearing: water publishes its optics but still
29
+ > computes its own fog layer). Otherwise identical to beta.4 — the rest is
30
+ > design notes.
31
+ > - **0.7.0-beta.4** (2026-08-15) — the `medium` primitive (space/air/water/
32
+ > mercury, plane or sphere), medium-aware projectiles closing #13, and pause
33
+ > marked EXPERIMENTAL pending a headset.
34
+ > - **0.7.0-beta.3** (2026-08-15) — pause (backgrounding, start-paused, the VR
35
+ > entry gesture, a centred panel), the stick-sign contract test + `kind`, and
36
+ > four more manta reports: #17/#18 ambient spawn bias, #19 authored projectile
37
+ > meshes, #22 destroyable library models. Plus the HUD circle's portrait
38
+ > sizing and both panels' camera/viewport fixes.
39
+ > - **0.7.0-beta.2** (2026-08-15) — adds #13's missile-guide composition, the
40
+ > gear gadget for the glass gamepad, and the live `fade`. #14 investigated and
41
+ > NOT changed: the glass source measures correct on desktop from four
42
+ > independent angles, so no sign was flipped (see the issue).
43
+ > - **0.7.0-beta.1** (2026-08-14) — everything below, including the four
44
+ > manta-recon issues fixed that day: #9 (crashed aircraft eats input) and the
45
+ > `b3d-death` latching bug behind it, #10 (`aircraftMapping` ignored its
46
+ > config), #11 (probe and ambient watchdog measuring during load), #12 (the sky
47
+ > ignored the water you were under).
48
+
49
+ **Aircraft that fly like aircraft, and the substrate for volumetric terrain.**
50
+ No peer-dependency changes, but this is a **behaviour** release: the throttle,
51
+ the right stick and the on-screen gamepad all work differently, and one
52
+ attribute was renamed. Read the ⚠️ Breaking block before upgrading a scene that
53
+ was tuned around the old feel.
54
+
55
+ ### ⚠️ Breaking
56
+
57
+ - **`canonicalize` now drops the CONTENT node's scene transform, so a model
58
+ placed away from the origin in its source file lands differently.** The
59
+ contract always said it "drops the node's SCENE transform" and it only ever
60
+ zeroed the node it was handed — on the glTF path that is `__root__`, the
61
+ loader's own wrapper — while the authored object underneath kept its scene
62
+ placement.
63
+
64
+ **Who is affected:** anyone whose model sits somewhere other than the origin
65
+ inside its `.glb` AND who compensated for the resulting offset. The
66
+ compensation is now doubled.
67
+
68
+ **How it shows up:** the control node used to sit a fixed distance from the
69
+ airframe it steers (1.7 m for our own scout), so collision rays started beside
70
+ the model and a `_centerOfGravity` marker measured against the control node
71
+ folded that offset into the PIVOT. The perverse consequence: a marker authored
72
+ dead on the centreline — the documented way — installed a pivot 1.75 m out,
73
+ while an odd-looking hand-tuned marker silently netted out correct. Our scout
74
+ was re-exported "properly" and started crashing on every bank.
75
+
76
+ **Migration:** author content in the model's LOCAL frame, as
77
+ `CLAUDE.md` has always said, and delete compensating offsets. If a vehicle
78
+ now flies or pivots oddly, look for a marker or a placement that was tuned
79
+ against the old behaviour before assuming a regression. Models authored at the
80
+ origin (the common case) are unaffected.
81
+
82
+ - **Angles in the AUTHORING surface are DEGREES.** "We should use degrees
83
+ everywhere. Not even mathematicians visualize radians." Two APIs took radians
84
+ and now take degrees: `landform`'s `gulley`/`cover` `heading`, and the library
85
+ rotation below. **Migration:** multiply by `180 / Math.PI`.
86
+ Pure math modules (`spatial-transform`, `guidance`, `portal-transform`,
87
+ `fly-by-wire`'s internals) keep radians deliberately — that is mathematics,
88
+ not a dial an author turns. And `b3d-skybox`'s `azimuth` is neither: it is
89
+ Babylon's 0–1 fraction of a turn, passed straight through, now documented as
90
+ such rather than left to be guessed at.
91
+ - **⚠️ `library.instantiate()` / `library.make.*` rotation: WAS IGNORED, now
92
+ works — and is DEGREES.** The single most disruptive change in this release,
93
+ because it is the only one that can move a scene which previously looked
94
+ **correct**. Everything else here either errors or looks obviously wrong.
95
+
96
+ Two things happened to one option:
97
+
98
+ 1. **It never applied on the default path.** `instantiate()` wrote
99
+ `result.rotation`, but Babylon's glTF loader always assigns a
100
+ `rotationQuaternion`, and a `TransformNode` ignores `.rotation` while one is
101
+ present. Measured on a real model: `ry: 0`, `ry: 140` and `ry: -90` gave
102
+ **bit-identical** orientations — you got the GLB's own baked rotation
103
+ whatever you asked for. `position` worked, which is precisely what made it
104
+ look wired up. (`canonical: true` was unaffected throughout.)
105
+ 2. **It is now degrees**, matching `AbstractMesh`'s `rx`/`ry`/`rz` and
106
+ `el.make.*`. The library was the only surface in the framework that
107
+ disagreed, and a bare number is valid in either unit — so it failed by
108
+ producing a different orientation rather than an error. It got past us in
109
+ this repo's own collision demo, where `ry: 140` meant 140 radians.
110
+
111
+ **Migration.** If you passed rotation and it _appeared_ to work, it was not
112
+ your value doing it — something else was (baked rotation in the model, a parent
113
+ transform, a compensating tweak downstream), and that compensation is now
114
+ **doubled up**. Check every placement before calling it a regression. If you
115
+ passed rotation and gave up because nothing happened, it works now: your values
116
+ are radians, so multiply by `180 / Math.PI`. If you never passed rotation,
117
+ nothing changes. Quickest triage: grep for `instantiate(` and `make.` with an
118
+ `r[xyz]` key — no hits means this does not apply to you.
119
+
120
+ See `Migration.md`, which ships inside the package.
121
+
122
+ - **`b3dPatch` / `B3dPatch` are removed.** The volumetric-patch element stitched
123
+ an SDF extraction into the heightfield and never worked: two surfaces meeting
124
+ at a grazing angle have no well-defined boundary, so every fix aimed at one
125
+ crossing created another elsewhere. It shipped EXPERIMENTAL and undemoed, and
126
+ the design that replaces it (TUNNEL-DESIGN.md) removes the boundary rather
127
+ than reconciling it. The pure modules it was built on — `carve.*`,
128
+ `sdf-lattice`, `patch-field` — are **kept**: they are the substrate the
129
+ replacement uses.
130
+
131
+ - **`hudChase` → `hudChaseOff`, with the polarity inverted.** The old attribute
132
+ is gone (no alias — HTML boolean semantics can't express a default-true flag,
133
+ which is why the negative name exists). It shipped in 0.6.2's `dist`, and
134
+ because tosijs props end in an index signature a stale `hudChase` **compiles
135
+ and is silently ignored**, so this will not error for you.
136
+ - `hudChase: true` → delete it; the chase HUD is now the default.
137
+ - `hudChase: false` → `hudChaseOff: true`.
138
+ - Chase view now shows the HUD **without the artificial horizon** (which
139
+ only tells the truth from inside the cockpit).
140
+ - **The glass gamepad hides itself** once a **keyboard or hardware gamepad** is
141
+ used, returning after `idleSeconds` (10) of silence. The POINTER never fades
142
+ it — not touch, not mouse, not trackpad: the pad is operable by pointer, so
143
+ fading on pointer movement made it vanish exactly as you reached for it.
144
+ Set `fade="off"` to pin it — worth knowing if you screenshot or demo.
145
+ - **The right stick is the camera**, not aux roll. `strafe` still sums into
146
+ roll, so a custom mapping can restore a dedicated roll axis.
147
+ - **`carve` is a NAMESPACE, not 13 bare exports.** New in this cycle, so this
148
+ affects only the handful of people who took `0.7.0-rc.1` off the `next` tag:
149
+ that tarball exported `applyCarve`, `sphere`, `capsule`, `tube`, `box`,
150
+ `union`, `smoothUnion`, `flange`, `subtract`, `intersect`, `roughen`, `warp`
151
+ and `shaft` at top level; they now live on `carve.*`. Coming from the rc you
152
+ get a link error, which is the intended loud failure — `box` in particular
153
+ shadowed the UI container of the same name.
154
+ - `import { sphere, tube } from 'tosijs-3d'` → `import { carve } from
155
+ 'tosijs-3d'`, then `carve.sphere(…)`, `carve.tube(…)`.
156
+ - Types stay top-level: `Carve`, `NoiseOptions`, `Vec3Like`.
157
+ - **`getNames()` returns different strings.** Behaviour suffixes no longer
158
+ leak into the public name, so `Hull_collideMesh.model` lists as `Hull` and
159
+ `instantiate('Hull')` resolves it (fixes #7, reported by manta-recon).
160
+ Exact and `.model` matches still win first, so existing calls that used the
161
+ raw name keep working — but code that PARSED the returned string, or
162
+ compared it to an authored name with its suffix, needs updating.
163
+
164
+ ### Added
165
+
166
+ - **Re-seat is now a two-step, confirmed gesture — and it finally works.**
167
+ Re-seating takes your CURRENT head yaw as forward, but you had to LOOK AT the
168
+ button to press it, so a panel that had drifted left re-seated you facing
169
+ left: the gesture defeated itself exactly when you needed it. Pressing
170
+ **Re-seat** now raises a prompt pinned to the **face frame** (head-locked, so
171
+ it stays readable WHILE you turn your head — the one case that earns
172
+ face-locking): _1. Look comfortably ahead. 2. Pull right trigger to reseat.
173
+ Or, pull left trigger to cancel._ The yaw is captured on the trigger, from
174
+ wherever you are looking THEN.
175
+ - **`reseatFreeze`** (new `<tosi-b3d>` attribute, `'on' | 'off'`, default
176
+ `'on'`) freezes the clock while the prompt is up, so you are not shot mid
177
+ re-seat. **Set `'off'` for multiplayer** — freezing is a decision a local
178
+ world can make and a networked one cannot honour. The dialog always gates
179
+ YOUR input (`b3d.suppressInput`), which is the half that works in any
180
+ topology, so the trigger that confirms never also fires your gun.
181
+ - `b3d.freeze(on)` and `b3d.suppressInput(on)` are public: a modal that
182
+ borrows a control you also play with can use them. `freeze()` stops the
183
+ clock without raising the pause panel and without pause's resume semantics
184
+ — a scene paused underneath stays paused.
185
+ - **The package is licensed** — **Apache-2.0**, with a LICENSE file, plus
186
+ `description`, `homepage` and `bugs`. Every release since 0.2.1 shipped with
187
+ no licence at all, which legally means all-rights-reserved: SCA tooling flags
188
+ an unknown-licence dependency, and adopters consuming this as a `file:` dep
189
+ had nothing to point at. Apache-2.0 rather than MIT/BSD for the explicit
190
+ patent grant and retaliation clause.
191
+ - **A `startPaused` scene renders its own fog, not Babylon's default** (#31). A
192
+ paused frame still RENDERS — that is the point, the panel has to be visible —
193
+ so everything the render reads has to be correct. `_updateFog` sat past the
194
+ paused branch's early return and `startPaused` pauses in the same synchronous
195
+ setup block, so a scene that booted paused had never run it once.
196
+ - **Entering VR unpauses a paused scene.** Putting the headset on is the resume
197
+ — and a world you deliberately stepped into has no reason to be frozen. It
198
+ also removed a broken state: entering while paused showed no pause panel
199
+ in-session, so you arrived in a stopped world with no visible way out. You can
200
+ still pause in VR from the scene panel's transport toggle.
201
+ - **Camera-relative dialogs are no longer buried by the world.** The respawn and
202
+ pause panels could end up inside terrain — you die on a hillside and the panel
203
+ offering you a way out is in the hill. They now render in group 1 (drawn after
204
+ the scene, depth auto-cleared between groups), so a modal is always readable.
205
+ World-anchored panels and popups keep normal depth sorting.
206
+ - **VR altitude on the BUMPERS** — left down, right up, analog (squeeze
207
+ pressure, so you can ease onto a height). Same hand logic as brake/accelerate
208
+ on the triggers. The right stick's vertical still flies, but it doubles as
209
+ panel scroll, so in any scene with a scrollable panel open — most demos — the
210
+ stick scrolled and the rig did not move, and altitude appeared to be missing
211
+ entirely. Two dedicated buttons cannot be stolen by a panel.
212
+ - **Pause/resume in the standard scene panel.** Pause was a one-way door: the
213
+ pause panel offers _Continue_, so it was a state you could LEAVE and never
214
+ ENTER — nothing in the panel stopped a running scene, and `startPaused` is an
215
+ initial condition, not a control. The icon bar now carries a transport toggle
216
+ (`play` when paused, `pause` when running — it shows what pressing it DOES,
217
+ not what the scene currently is). It is a panel GADGET, so it appears in the
218
+ flat overlay **and** the in-VR panel from one definition, and in a headset it
219
+ is the only way to stop the world at all.
220
+ - **Status surfaces in the theme** — `--w3d-info`, `--w3d-warning`,
221
+ `--w3d-error` (`w3dTheme.info` / `.warning` / `.error`). Backgrounds, not text
222
+ colours: a status panel in a dark theme is a tinted SURFACE that `text` still
223
+ reads on, and the bright hue you would put on a label is unreadable behind
224
+ one. Opaque on purpose, so the panel underneath cannot show through the
225
+ message interrupting you.
226
+ - **`markUiMesh()` / `isNoCollide()`** (`b3d-utils`) — mark a mesh as UI:
227
+ pointer-pickable, but invisible to COLLISION. See the phantom-collision fix
228
+ below for why those are different questions. The first of the collision
229
+ GROUPS described in the new `COLLISION-DESIGN.md`.
230
+ - **`xrFrame`** on `<tosi-b3d-svg-plane>` — when `cameraRelative` and in a
231
+ headset, ride an XR reference frame instead of the head camera. `'body'`
232
+ (damped yaw) keeps a panel in front of you without jittering on every head
233
+ movement; leave it unset for a HUD that should stay head-locked.
234
+ - **A diagnostic ring buffer** — `b3d.logDebug(tag, event)`,
235
+ `b3d.debugCapture(tag)`, `b3d.debugLog`. There is no console in a headset and
236
+ `window.rAF` is suspended in-session, so this records structured events IN the
237
+ page for readback afterwards. Plus always-on Perf-panel rows: **aircraft
238
+ ground** (what the ground ray is calling ground, and a full crash report) and
239
+ **xr input** (per-hand thumbstick axes, rig pose, movement scale).
240
+
241
+ - **Popups are their own SURFACES — `el.openPopup()`** (`popup-surface`). A menu,
242
+ a dropdown's list or a debug readout spawns a second plane above its opener
243
+ rather than being laid out inside its SVG. Owned popups travel and die with
244
+ their opener; `tearOff()` promotes one to world space preserving pose, after
245
+ which it stays where you put it — and **dragging an owned popup tears it off**,
246
+ like pulling a tab out of a window. Click-to-front, `modal` (blocks pointer
247
+ access to what's behind it, never the camera), a title-bar grip so the rest of
248
+ the panel can stay interactive, and injected move/close glyphs.
249
+
250
+ This retired a constraint we had recorded as absolute: a dropdown "MUST grow
251
+ the panel's layout … because a popover won't rasterize into the VR texture".
252
+ True of a popover inside ONE texture; the escape is another texture on another
253
+ plane. See `UI-DESIGN-NOTES.md`.
254
+
255
+ - **`rounded-rect` + `cornerRadius` / `transparent` on `b3d-svg-plane`** — rounded
256
+ corners as GEOMETRY so a UI panel can be **opaque**. A transparent mesh is not
257
+ depth-written, so Babylon re-sorts it per frame by camera distance and
258
+ near-coplanar panels flip order as you orbit: it looks exactly like z-fighting
259
+ and no amount of correct depth ordering fixes it. Three quads plus four
260
+ quarter-disc fans, 22 triangles at 4 segments. **The general rule: in 3D,
261
+ express shape as geometry and reserve alpha for things that are genuinely
262
+ see-through.**
263
+
264
+ - **`el.make.*`** (`make-mesh`) — Babylon primitives with the forgettable parts
265
+ done: material from `color`/`glow`, `register()` so the sun and reflection
266
+ probes see it, and `computeWorldMatrix` (a mesh positioned but never rendered
267
+ has no world matrix, so a ray cast this frame finds it AT THE ORIGIN and
268
+ answers confidently and wrongly). A Proxy forwarding to `MeshBuilder`, so all
269
+ 26 shapes are reachable plus whatever Babylon adds later.
270
+
271
+ - **`library.make.<name>()`** — a library's contents as callable names,
272
+ `lib.make.scout({ y: 1 })`. Same option vocabulary as `el.make.*`.
273
+
274
+ - **`glow` / `glowColor` on `b3dBox` / `b3dSphere`** — self-illumination as a
275
+ fraction of `color`. Both primitives now share one `primitiveMaterial`, which
276
+ is how the sphere came to be missing `glow` ninety seconds after the box got
277
+ it.
278
+
279
+ - **`B3d.BABYLON` (static) and `BABYLON` re-exported from the barrel.** Babylon
280
+ is a PEER dependency, so a consumer importing `@babylonjs/core` separately can
281
+ end up with a second copy — two `Vector3` classes that fail `instanceof`
282
+ against each other. Taking it from the library that already holds one is the
283
+ guarantee.
284
+
285
+ - **`shadowNormalBias` / `shadowBias` on `b3dSun`**, defaulting to `0.05`.
286
+ Babylon's CSM ships `normalBias: 0`, which is not a tuned value for a large
287
+ receiver: a 40×40 ground self-shadowed into a dense stipple across its whole
288
+ surface. Measured at 0 / 0.02 / 0.05 on a live scene.
289
+
290
+ - **`b3d-collisions` gains its first live demo** — hail on the scout, impacts
291
+ found by a SWEPT ray, each marked with a disc lying on the surface plus a
292
+ stalk along the normal. Ground hits count too, in a cooler colour.
293
+
294
+ - **`b3d-aircraft` gains `chasePitchFollow` / `chasePitchLag`** — how much of the
295
+ nose's pitch the chase camera inherits. `0` (default) keeps the level pivot;
296
+ `1` is as if the camera were bolted to the airframe. A dial rather than a
297
+ reparent, because the flat pivot is what fixed the jittery chase, and the lag
298
+ term is what passes the intention while dropping the per-frame wobble.
299
+
300
+ - **`b3d-aircraft` gains `maxPitch` / `maxDive`** (#26). Pitch authority was a
301
+ module constant at a symmetric 35° — a gentle airliner descent, unreachable by
302
+ any adopter, and unable to express a craft that should fall out of the sky
303
+ readily and climb reluctantly. `maxDive` defaults to `maxPitch`, so nothing
304
+ changes unless asked.
305
+
306
+ - **⚠️ EXPERIMENTAL — `portalTransform`**: see-through portal math (virtual
307
+ camera pose, the oblique clip plane, a step-test crossing, and recursion
308
+ budgeted by a falloff CURVE rather than a hard depth limit — linear, geometric
309
+ or accelerating, so first-bounce quality and pass count stop being welded
310
+ together).
311
+ - **Sedimentary strata** in the volcanic rock (`strata`, `strataScale`,
312
+ `strataTilt`), visible on any cut face — only possible because the plate noise
313
+ is now sampled in 3D, since bedding means nothing to a shader that knows only
314
+ x/z.
315
+ - **Carved landforms** — the `sdf-lattice` page is now a real doc page with the
316
+ theory (SDF composition, marching cubes vs dual contouring vs surface nets, and
317
+ why this uses surface nets), a **volcano in cross-section** with lava tubes and
318
+ a sweeping cutaway, and a **volumetric-vs-heightfield comparison** with a bore
319
+ punched through an authored ridge. `demo-utils.volumetricDemo` is the shared
320
+ fixture behind both.
321
+
322
+ - **⚠️ EXPERIMENTAL — `portalTransform`**, the pure math for see-through
323
+ portals: `portalCamera` (where the render-target camera goes so a doorway
324
+ shows another place), `clipPlaneFor` (the oblique near plane, without which
325
+ the far room leaks into the doorway), `crossedPortal` (a _step_ test — a jeep
326
+ is past a doorway within one frame, and a context swap one frame late is one
327
+ frame of the wrong world), and `depthLimit`/`attenuationAt` for recursion.
328
+
329
+ Recursion terminates the way real mirrors do — each traversal is slightly
330
+ lossy, so the tunnel darkens instead of hitting a hard cutoff that pops.
331
+ Calibration worth knowing: **real glass is ~95% transmissive**, which is why
332
+ mirror corridors look infinite — at 0.95 it takes 77 bounces to fall below 2%.
333
+ So the physics gives the right look and a useless budget; the device cap is
334
+ load-bearing, and a portal that should visibly fade out has to be markedly
335
+ dirtier than glass (~0.5–0.7). That trade is documented rather than hidden
336
+ behind a physical-sounding parameter.
337
+
338
+ The falloff is a **curve, not a ratio** — `geometricFalloff`, `linearFalloff`
339
+ (state the level count instead of solving for it: `0.1` means exactly ten) and
340
+ `acceleratingFalloff` (the loss grows with depth — dirtier where nobody
341
+ looks). That decoupling is the point: geometric-at-0.95 and
342
+ accelerating-from-0.05 keep an identical 95% at the first bounce, the one
343
+ anyone actually inspects, but cost **77 levels versus 5**.
344
+
345
+ No renderer yet: applying the clip plane, the passes and stencil framing are
346
+ deliberately out of scope for a pure module.
347
+
348
+ - **⚠️ EXPERIMENTAL — `MediumOptics` + `fogLayerFor`** (MEDIUM-DESIGN.md §3).
349
+ A medium can describe how it LOOKS (colour, density, murk-with-depth,
350
+ visibility floor), and one derivation turns that into a fog layer for the
351
+ compositor that already exists. Weight is `submergence`, so the fog and
352
+ anything else keyed off the same medium cannot disagree about where the
353
+ surface is — that disagreement is what produced the fogged-sky vs
354
+ transparent-window conflict.
355
+
356
+ **Not load-bearing yet, on purpose.** `b3d-water` publishes its optics but
357
+ still computes its own fog layer, because that path is verified and swapping
358
+ it is a visual change that wants an eye on it. What's proven so far is the
359
+ falsifier from the design doc: one derivation reproduces water's numbers
360
+ (including thickening with depth), a cloud bank's soft edge, and vacuum
361
+ contributing nothing. Three hand-rolled contributions collapse onto it _in
362
+ the tests_; collapsing them in the code is the next step.
363
+
364
+ - **Projectiles understand medium** (#13 — ⚠️ EXPERIMENTAL, unflown). A round
365
+ now picks up drag from whatever it is passing through (`dragAt`, blended
366
+ across the band so entry is a ramp), and gains three options that turn the
367
+ same round into four weapons:
368
+
369
+ - `whenCrossing(kind, medium, at)` — the entry splash, the breakout plume,
370
+ the audio. Yours to dress; the engine only tells you it happened.
371
+ - `detonateDepth` — a **depth charge**: a fuse on depth rather than impact.
372
+ - `stayIn: 'water'` — a **torpedo**, for which the surface is a ceiling. Held
373
+ just inside rather than reflected: a round that bounces off water reads as a
374
+ skipping stone, which is a different weapon.
375
+
376
+ A **sub-launched missile** needs no option at all — it is a `whenCrossing`
377
+ handler that swaps its own behaviour on `'exited'`. That the four fall out of
378
+ three small options is the argument for the primitive being shared rather than
379
+ per-weapon.
380
+
381
+ - **[medium](https://3d.tosijs.net/medium/) — the substance you're moving
382
+ through**, pure and Babylon-free (#13, and the seam #3/#15/#16 were all
383
+ converging on). It answers three questions and owns nothing else: how deep am
384
+ I (`depthIn`, signed — depth and altitude are one measurement), how much of me
385
+ is in it (`submergence`, smoothed across a band), and did I just cross the
386
+ surface (`crossing`, a _step_ test, because at 200 m/s a round is above the
387
+ water one frame and 3 m under it the next).
388
+
389
+ - **Two geometries**: `plane` (a sea, a fog bank, the top of a mercury vat)
390
+ and `sphere` (a planet's ocean or atmosphere, the edge of space). A plane is
391
+ NOT a very large sphere — at a 6371 km radius the arithmetic loses the
392
+ centimetres that decide whether you're above or below a wave.
393
+ - `innermost` resolves space → air → water the way you'd say it aloud, and
394
+ vacuum is simply the absence of a match. `dragAt` blends a projectile's drag
395
+ coefficient across the band, which is why depth charges and torpedoes need
396
+ no new integrator.
397
+ - **`b3d.media` / `b3d.addMedium()`**, and `b3d-water` registers itself — so
398
+ nothing has to re-derive where the surface is. Two subsystems deriving that
399
+ separately is exactly how the fogged sky and the transparent window ended up
400
+ disagreeing (#12/#15).
401
+
402
+ - **`b3d-ambient` spawns where you're LOOKING and where you're GOING** (#17,
403
+ #18): `lookAhead` (0.35 of `radius`, along the view) and `lead` (0.25s of
404
+ travel, along motion), capped by `speedCap`. A camera sees a frustum, not a
405
+ sphere, so a box centred on the eye births most of its particles behind and
406
+ beside you — they live and are culled unseen, paying full budget for a
407
+ fraction of the result — and at speed you outrun the box entirely, emptying
408
+ the view ahead exactly when you want it fullest. Velocity is measured from the
409
+ camera's own displacement, so nothing has to be plumbed in; in a chase view
410
+ that's the right signal anyway. Set either to 0 for the old centred box.
411
+
412
+ - **`b3d-destroyable` takes `library` + `meshName`** (#22) — a static,
413
+ destroyable thing that uses a library mesh is most of what populates a level,
414
+ and there was no route to it: `b3d-loader` takes a `url` (which also loses the
415
+ canonical frame), `b3d-aircraft` gets the frame right but flies. Absent
416
+ `library`, you still get the placeholder cube, so nothing existing changes.
417
+ The model is left **pickable** on purpose — damage resolves from the warhead
418
+ gathering destroyables near the detonation, not from which mesh the ray hit,
419
+ so the model can simply be the target and the hidden-cube-plus-skin dance
420
+ isn't needed.
421
+ - **`spawnProjectile`/`spawnMissile` take a `mesh`** (#19) — draw the round as
422
+ an authored model, with the engine still owning motion, collision, lifetime
423
+ and disposal. It is also **oriented along velocity**, which a sphere never
424
+ needed: the engine moved a sphere, a sphere has no facing, so a modelled
425
+ missile flew sideways. Yaw and pitch only — a round has no reason to bank.
426
+ Supplied meshes and their children are made non-pickable, like the default.
427
+
428
+ - **`panelFitWidth(fov, aspect, z, want, fill)`** — how wide a camera-relative
429
+ panel may be and stay on screen. Shared because there are two such panels and
430
+ a constant copied into both is a constant that will disagree with itself.
431
+
432
+ - **PAUSE — ⚠️ EXPERIMENTAL.** The flat path is confirmed on a real browser
433
+ (start paused, continue, background, return, repeat) but the VR path — the
434
+ Continue tap carrying the user gesture into `enterXRAsync`, and removing the
435
+ headset pausing — is **unvalidated**, and it is the reason the feature exists.
436
+ Treat the API as unsettled until it's been through a headset; nothing else
437
+ depends on it.
438
+
439
+ `b3d.pause(reason)` / `.resume()` / `.togglePause()` / `.paused`,
440
+ a centred in-scene pause panel, and the attributes around them:
441
+
442
+ - `pauseWhenHidden` (default **on**) — backgrounding the tab holds the scene,
443
+ so a player returns to a held frame and a panel rather than to a world that
444
+ carried on without them. Suppressed while immersive, since some browsers
445
+ report the page hidden during an XR session.
446
+ - `startPaused` — come up paused: the "press Start" shape.
447
+ - `enterXrOnResume` — resume enters VR, and leaving VR pauses. **This is why
448
+ starting paused matters**: `enterXRAsync` requires a user gesture, so a
449
+ scene that tried to enter XR on load would simply be refused. The Continue
450
+ tap is the gesture, which makes "leave it paused, put the headset on, press
451
+ Continue" the only shape that reliably works.
452
+ - `pausePanel(host, resume)` — replace the default rows (a title and Continue)
453
+ with your own: a title screen, settings, a "you were away" summary. `resume`
454
+ is handed in, because a pause panel with no way out is the failure this
455
+ feature exists to prevent.
456
+ - `pause` / `resume` / `orientation` events. `pause` carries a `reason`
457
+ (`'user' | 'hidden' | 'xr' | 'start'`) so a game can tell "the player asked"
458
+ from "the tab went away". `orientation` is derived from the **viewport**
459
+ (`innerWidth`/`innerHeight` swapping), not from `screen.orientation` — that
460
+ API is patchy on iOS, so a scene depending on it hears nothing on the
461
+ devices that rotate most; it's still read for `type`/`angle` when present,
462
+ as detail rather than trigger. Fires only when the classification actually
463
+ flips, so dragging a desktop window edge isn't a rotation.
464
+
465
+ Rendering continues while paused — the panel has to be drawn and picked —
466
+ but nothing advances: no combat tick, no fog, no `update` hook, and
467
+ controllables read empty input, so a held stick can't steer a stopped world.
468
+ Orientation changes are **reported, not acted on**: a rotating phone is
469
+ sometimes a pause and sometimes nothing, and only the game knows which. (Tilt
470
+ is a separate, permissioned API on iOS and deliberately untouched.)
471
+
472
+ - **The stick sign contract is now a test** (`stick-sign.test.ts`) — every
473
+ source must report **up/forward as positive**, and all of them must agree.
474
+ This was documented in three places and enforced in none, which is what let
475
+ #14 run for a day: a source that disagrees is internally consistent, so only
476
+ a player with two devices notices, and what they report is "the framework is
477
+ broken". Verified by deliberately flipping the touch and hardware signs in
478
+ turn and watching it fail. `STICK_UP_IS_POSITIVE` names the convention.
479
+ - **`GamepadSource.kind`** — a stable `'keyboard' | 'hardware' | 'touch' | 'xr'
480
+ | 'glass'` marker, because the obvious alternative fails silently: an adopter
481
+ matched our glass pad with `constructor.name`, their bundler mangled it, the
482
+ lookup never matched, and every experiment built on it was a no-op that
483
+ looked like a result. Class names are not API.
484
+
485
+ - **A gamepad gadget in the gear panel** — one tap to pin the glass pad visible
486
+ or hand it back to the auto-hide. The fade is production-correct and
487
+ development-hostile: once a mouse or trackpad is present the pad goes away and
488
+ doesn't come back, so checking it on a laptop meant switching Chrome into
489
+ responsive mode. It sits in the same icon bar as the debug tools, so it works
490
+ in a headset too. `B3dGamepad.setFade()` / `.hidden` are the API behind it.
491
+
492
+ - **`b3d.sceneBusy`** and **`terrain.busy`** — is the world still building? Any
493
+ frame-rate judgement taken while these are true is a measurement of the loading,
494
+ not of the device.
495
+ - **`b3d.ambientPoolScale` is now readable AND settable**, so a game can say "that
496
+ was the loading screen, try again" instead of reaching into two privates (#11).
497
+ - **`aircraftMapping` honours its config** — it accepted one and had zero
498
+ references to it. `AircraftMappingConfig` adds `invertPitch` (the arcade pitch
499
+ convention), `invertRoll` and `invertCameraY`. Meaning belongs in the mapping:
500
+ every source passes through it, so one setting can't leave the keyboard inverted
501
+ and the glass gamepad not (#10, manta-recon).
502
+
503
+ - **The pure flight model is exported**: `flyByWireStep`, `regime`,
504
+ `targetVelocity`, `chaseVelocity`, **`equilibriumSpeed`** and the
505
+ `FlyByWireConfig`/`Command`/`State` types. `equilibriumSpeed` is what the
506
+ HUD's new set-point mark is drawn from — a mission planner wants the same
507
+ number, and until now it was unreachable from the package.
508
+ - **`isIgnored(name)`** — one matcher for the `-ignore` convention, used by the
509
+ loader, the library and `publicName`.
510
+
511
+ - **[carve](https://3d.tosijs.net/carve/)** — the cave vocabulary, exported as
512
+ the **`carve.*`** namespace (see ⚠️ Breaking): `carve.sphere`,
513
+ `carve.capsule`, `carve.tube`, `carve.box`, `carve.smoothUnion` (fillets a
514
+ junction so a passage flares into a chamber), `carve.subtract`,
515
+ `carve.intersect`, plus the two perturbations that stop a carve looking
516
+ carved: `carve.roughen` (texture the wall, silhouette stays) and
517
+ `carve.warp` (bend the space so nothing reads as a primitive).
518
+ - **[sdf-lattice](https://3d.tosijs.net/sdf-lattice/)** — surface-nets
519
+ extraction over ONE global hash-jittered lattice, so chunks weld
520
+ bit-identically and cross-tile/cross-LOD seams are unrepresentable rather
521
+ than stitched. Includes the chunk-weld proof as a test.
522
+ - **[patch-field](https://3d.tosijs.net/patch-field/)** — `landform`'s
523
+ volumetric sibling: `(x, y, z, d) => d'` carving composed against the
524
+ terrain's own hooked height sampler.
525
+ - **`landform`** gains `gulley` (a height FORCING function ending in a
526
+ predictable cliff), `cover` (forced ground over a tunnel so it stays
527
+ buried), and `pad` from 0.6.2's family.
528
+ - **`b3d-terrain`**: `patchMask`/`patches` hooks, `biomeLapseRate` (the lapse
529
+ is coupled to vertical scale — a 340m world on the small-world default
530
+ renders entirely as snow), and a public `heightSampler()`.
531
+ - **Landing gear, found by NAME** — AnimationGroups mentioning "gear" and
532
+ "retract" are cycled from height above ground, with hysteresis and an
533
+ optional `gearSound`. The animation is SCRUBBED, not played, because glTF
534
+ animations arrive with a cyclic loop mode and a played group can snap back
535
+ to frame 0.
536
+ - **`arcDashArray`** + `hud.setMeterMarks` — reference marks on any meter,
537
+ drawn on the meter's own arc. Used for the throttle set point, sea level,
538
+ and the ground beneath you.
539
+ - **`equilibriumSpeed(cfg, throttle, afterburner)`** — where a lever will
540
+ settle.
541
+ - **`sceneDelta(scene)` is now actually exported.** 0.6.2 announced it and
542
+ shipped it unreachable — `dist/index.d.ts` had no such symbol — so anyone
543
+ following those notes hit a link error. It's the correct frame delta for
544
+ anything ticking inside a scene observer (see 0.6.2's note on
545
+ `getDeltaTime`), and the six doc demos that still taught the broken idiom
546
+ now use it.
547
+
548
+ ### Changed
549
+
550
+ - **tosijs-ui devDependency → ^1.9.8**, for serve-time gzip on the dev server:
551
+ it was sending the 10.7 MB bundle uncompressed even to clients asking for
552
+ gzip, so over a thin link a doc page showed its pre-rendered HTML and then
553
+ never hydrated — the bundle never finished arriving. 2.56 MB now. Doc-site
554
+ tooling only; nothing in the published library changes.
555
+
556
+ - **⚠️ The aircraft throttle is a LEVER, not a speed setpoint.** It commands
557
+ an equilibrium: a climb settles at a new lower speed instead of stalling,
558
+ and lowering the nose returns you to the speed you had, untouched. Full
559
+ lever is MILITARY thrust; afterburner lights only while the trigger is held
560
+ past a detent and drops back when released. Releasing the trigger holds the
561
+ setting, so speed no longer self-sustains at idle — set the lever where you
562
+ want to cruise.
563
+ - **⚠️ The right stick is the CAMERA**, not aux roll: it orbits the chase
564
+ camera around the aircraft and turns the pilot's head in the cockpit,
565
+ springing back on release. (`strafe` still sums into roll for anyone who
566
+ maps a dedicated axis.)
567
+ - Hover gained a brake (the trigger's negative half sheds speed to a stop)
568
+ and slow reverse (nose-up lean), with the trigger staying purely vertical
569
+ so stopping never fights altitude. `hoverCeiling` 50 → 140.
570
+ - Speed gauges read 0..100% of the FULL envelope, afterburner included.
571
+ - Terrain defaults: bigger gross features carrying less gross amplitude, with
572
+ the detail layer doing real work — the old defaults read as pudding.
573
+ - `worldV` defaults to 0.25: `CylinderSampler` reflects v, so `worldV = 0`
574
+ put every scene ON a mirror plane with a seam running to the horizon.
575
+
576
+ ### Fixed
577
+
578
+ - **Phantom collisions: an aircraft "collided with something it was nowhere
579
+ near".** Flying high, banking in cockpit view, you died instantly — in any
580
+ scene, terrain or not. The impact sweep was hitting the **spatial UI panel**
581
+ floating in front of the cockpit and treating it as terrain, and that sweep
582
+ crashes on ANY hit above `crashSpeed` with no slope test. It only bit in
583
+ cockpit view (that is where the camera, and the panels riding its reference
584
+ frames, sit inside a ~2m sweep) and fired on bank (which swings the velocity
585
+ vector into the panel). A panel must stay `isPickable` — that is how a
586
+ controller ray targets it — so the fix separates the two questions:
587
+ `markUiMesh` marks it collision-invisible, and both aircraft predicates filter
588
+ it.
589
+ - **Collision rays fired from `node.position`.** Under a `_centerOfGravity`
590
+ pivot that is the STANCE ORIGIN, not where the airframe is — the model swings
591
+ about the CoG under attitude, so banking moved the aircraft out from under its
592
+ own rays. Both rays now go through the world matrix, which is bit-identical to
593
+ `node.position` when there is no pivot.
594
+ - **`canonicalize` did not drop the CONTENT node's scene transform** — see
595
+ ⚠️ Breaking above. This is also why the scout's centre of gravity appeared
596
+ wrong.
597
+ - **Ambient effects could never recover from a pool of zero.** The watchdog
598
+ bailed one line before the recovery path written for exactly that state, so a
599
+ scene that shed its garnish to nothing stayed dead for the session — no
600
+ leaves, bubbles or motes on hardware that had been drawing them a second
601
+ earlier.
602
+ - **VR: exiting dropped the flat camera's altitude.** Babylon carries the walked
603
+ headset pose back into the non-XR camera, which for an orbit camera recomputes
604
+ a low angle — you left VR looking at the scene from the floor. The orbit state
605
+ is now snapshotted on entry and restored on exit; free/walkable cameras keep
606
+ the carry-back.
607
+ - **VR: orbit demos dropped you in facing a random direction.** Both piloted
608
+ paths seeded the rig's yaw against your head; free locomotion never did, so a
609
+ demo with no piloted entity left the panel behind you and the subject out of
610
+ sight. Now seeded on the first frame with a real viewer pose, facing what the
611
+ flat camera was looking at.
612
+ - **VR: re-seat did nothing in an aircraft's chase view.** Three camera paths
613
+ capture yaw separately and re-seat only re-armed two; toggling cockpit↔chase
614
+ appeared to "fix" it because the view-change path re-armed the third.
615
+ - **VR: locomotion was scale-blind.** Free-fly walked at a fixed 2.5 m/s, which
616
+ in a 512-unit landscape is ~0.5% of the scene per second — indistinguishable
617
+ from a broken stick, while turning (angular, therefore scale-free) felt fine.
618
+ Speed now scales with the orbit camera's radius; small demos are unchanged.
619
+ - **VR: the grid floor sat at the world origin**, so in any scene whose subject
620
+ is elsewhere it read as a mystery grid off in the distance, visible only in
621
+ VR. It now sits under the subject.
622
+ - **VR: the respawn panel was pinned to your face and jittered** with every head
623
+ movement. It rides the `body` frame now.
624
+ - **`bun run typecheck` was red at the release commit** — a one-argument call to
625
+ a three-argument `canonicalize` in a test, invisible to `bun test` (which
626
+ strips types) and to the build tsconfig (which excludes test files).
627
+
628
+ - **Shadow acne on large receivers** — see `shadowNormalBias` above. It was also
629
+ why scenes looked dull: the stipple darkened roughly half the ground.
630
+
631
+ - **`b3d-svg-plane` cleared a parent it never set.** Its `cameraRelative` sync ran
632
+ `mesh.parent = null` unconditionally whenever the flag was off, so anything
633
+ parented to a plane was silently torn off a frame later, with no error. Same
634
+ shape as the pause bug below: **a guard must record what you DID, not observe
635
+ what state the object is in.**
636
+
637
+ - **The glass gamepad no longer fades when you move the mouse.** It is operable
638
+ by pointer — `pointerdown` drives the sticks — so it vanished exactly as you
639
+ reached for it. Only a keypress or a physical gamepad fades it now.
640
+
641
+ - **`publicName` leaked loader and Blender artifacts.** The library picker showed
642
+ `building_collideCylinder_primitive0`; `_primitiveN` is the glTF loader
643
+ splitting a multi-material mesh, and Blender's `.001` sat AFTER the behaviour
644
+ suffix so no `endsWith` matched it. `.001` is preserved (it is a different
645
+ object) while the annotation is removed.
646
+
647
+ - **`ry` is DEGREES, and now says so.** `AbstractMesh.render()` multiplies
648
+ rx/ry/rz by `DEG_TO_RAD`; nothing documented it, and the b3d pause demo had
649
+ been adding radians per second for its whole life — about one revolution per
650
+ ten minutes, which read as "the cube doesn't spin".
651
+
652
+ - **Impacts now report the surface NORMAL, not just the point** (#29) —
653
+ `spawnProjectile`/`spawnMissile` take `whenImpact({ point, normal, mesh })`.
654
+ The swept ray already computed the normal and the launcher was throwing it
655
+ away, so anything that wanted an oriented effect (a scorch mark, a dent, a
656
+ ricochet) had to re-cast the same ray to get it back. `normal`/`mesh` are
657
+ `null` when there is genuinely no surface — a depth fuse in open water, a
658
+ timed round in mid-air. The old `onImpact(point)` still fires with a one-shot
659
+ deprecation warning; it was renamed off the `on*` prefix because such a key
660
+ becomes an `addEventListener` call the moment the shape is lifted onto an
661
+ element, and then silently never runs.
662
+ - **Pause didn't pause** (#30) — an adopter measured a player travelling 66.58 m
663
+ during a 3-second pause with `paused === true` throughout. Two clocks leaked,
664
+ not one. `sceneDelta` accepted a published frame delta only when `> 0`, so the
665
+ one value meaning "stopped" was the one value it discarded, falling back to
666
+ the live engine clock — everything on the render observable (projectiles,
667
+ water, ambient) kept running. And `B3dControllable` runs its own `Date.now`
668
+ clock that a stopped scene delta cannot reach: it was fed empty input, but
669
+ with no input an aircraft COASTS, which is indistinguishable from cruising. A
670
+ paused scene now publishes a delta of zero and controllables halt outright.
671
+ - **Blasts could not see library-backed destroyables** (#28) — a third
672
+ consequence of `library`. `detonateWarhead` resolved targets with
673
+ `scene.getMeshByName`, which searches meshes only, so a `library`
674
+ destroyable's TransformNode root was filtered out of every AOE. manta measured
675
+ it at 4 of 4 targets. It now uses the element's own node, which was always
676
+ right there.
677
+
678
+ - **`library` + `explode` on a destroyable was a guaranteed crash** (#24) — and a
679
+ nasty one. The exploder read vertex buffers off whatever it was handed, and a
680
+ library instance's root is a geometry-less transform node. Worse than a throw:
681
+ it happened inside a render observer, so every observer after it was skipped —
682
+ camera follow, flight integration, all of it — and the scene stopped advancing
683
+ while input kept responding. It presents as "the game seized but the controls
684
+ still work". `explodeMesh` now takes a `TransformNode` and shatters the
685
+ descendants that have geometry (a multi-part model comes apart properly instead
686
+ of one piece shattering while the rest hangs in the air), warns once rather than
687
+ throwing when there is nothing to shatter, and the death path wraps the call
688
+ because a cosmetic must never be able to stop the frame loop.
689
+ - **Mutual death produced no death panel** (#25). The already-dying _recovery_
690
+ added in 0.7.0 ran before the "only OUR death matters" filter, so any other
691
+ entity dying while the player was dead tore the panel down — ram an enemy and
692
+ you both die, and you got nothing. Relevance is checked first now.
693
+
694
+ - **Volcanic veins were absent from every vertical surface** (3D Worley), the
695
+ **rock stopped warming** at dark brown so a fully volcanic face read as black,
696
+ **verticals could never exceed stage 2** so the top of the ladder was
697
+ unreachable on a cliff or cut face, and the **glow pulse touched only the
698
+ molten channel** — a fraction of a fraction, which is invisible in practice.
699
+
700
+ - **Volcanic veins were missing from every vertical surface.** The plate pattern
701
+ was sampled from world **XZ only**, so a cliff or a cut face barely moves
702
+ through the noise — its xz coordinate is near-constant across the surface —
703
+ and you got vertical streaks instead of a vein web. Now sampled with a true
704
+ **3D Worley**, which costs the same neighbourhood as blending three 2D
705
+ projections (27 cells either way, a 3-wide hash instead of 2-wide) and is
706
+ strictly better: no projection seams, no ghosting where planes blend, and a
707
+ cut face shows the veins' real cross-sections because the veins are genuinely
708
+ three-dimensional. Only the volcanism branch reaches it, so non-volcanic
709
+ surfaces pay nothing.
710
+
711
+ - **The HUD circle dominated a portrait viewport.** It was already sized off the
712
+ _smaller_ dimension — that part was right — but a single percentage of the
713
+ small side means 36% of the width in landscape and **70% of it in portrait**,
714
+ the same rule with twice the visual weight. It's now capped at half that
715
+ fraction of the long side as well, so turning the device doesn't change how
716
+ much of the screen the circle eats: measured 490px (was 499) at 1400x713, and
717
+ 265px (was 350) at 500x757. `hudSizePx` in `hud-math.ts`, unit-tested.
718
+
719
+ - **`b3d-death`'s Respawn panel had both of the pause panel's problems**, and at
720
+ a worse moment. Its spectator camera was attached to the canvas, so a tap
721
+ meant for Respawn also drove the camera (Tonio, on a phone: _"the view
722
+ pivots"_), and it used the same fixed 1.1-unit width that overflows a portrait
723
+ viewport. The camera is now set with `{ attach: false }` — the orbit still
724
+ runs, it's driven by an observable, you just can't wrestle it — and the width
725
+ comes from the camera's FOV and aspect via the shared `panelFitWidth`.
726
+
727
+ - **The pause panel fought the camera for your tap.** It lives in the scene, so
728
+ a press on it is also a press on the canvas, and the camera's input got it
729
+ too: on a phone the first attempt to press Continue read as a pinch-zoom,
730
+ which moved the camera through the panel and hid it, so the button had to be
731
+ un-zoomed back into reach. The camera is now detached while the panel is up
732
+ (which is also what "paused" ought to mean) and restored on resume.
733
+ - **The pause panel was wider than a phone's viewport.** Sized at a fixed 1.1
734
+ world units, it fits a 16:9 monitor and overflows a portrait phone — at the
735
+ default FOV only ~0.86 units are visible across, so the edges and the button
736
+ sat off-screen. It now derives its width from the camera's real FOV and
737
+ aspect. `panel-fit.test.ts` pins it, including the portrait case that broke.
738
+
739
+ - **`B3dGamepad.fade` was read once at connect**, so toggling it at runtime did
740
+ nothing in either direction — including from a `scenePanel` toggle, which is
741
+ this repo's own recommended way to expose a tweakable. A settings control that
742
+ silently does nothing is worse than no control. Now evaluated live, and
743
+ `setFade()` reconciles on the tap rather than on the next pointer move.
744
+
745
+ - **A guided missile consumed the caller's `guide` hook** (#13), so a game could
746
+ give medium behaviour — water drag, a depth floor — to a dumb shell but not to
747
+ a missile. The seeker is now a named function the caller's hook composes with,
748
+ and the caller runs **last** so it can constrain what homing asked for: the
749
+ seeker wants to go fast, the water says no. `MissileOpts.guide` is new.
750
+
751
+ - **The sky ignored the water you were under** (#12, manta-recon: _"can't see
752
+ anything underwater except for the skybox"_). A skybox is built
753
+ `applyFog = false` with `infiniteDistance = true` — right in air, where a long
754
+ fog layer would swallow it — so submerged, the fog worked on everything except
755
+ the thing filling most of the screen. `b3d-water` now turns the sky's fog on
756
+ while the camera is under it, keyed off the same band weight as the fog layer
757
+ so the two can't disagree about where the surface is, and hands it back on the
758
+ way out. Note the trap it set: turning `underwaterFog` DOWN made it worse,
759
+ because the murk had been the only thing disguising an unfogged sky.
760
+
761
+ - **The device probe measured the machine while it was still loading, then cached
762
+ the verdict for 30 days** (#11, manta-recon). It went out on `setTimeout(…, 0)`
763
+ — one task, not one idle moment — so the benchmark ran during terrain build and
764
+ shader compile. It times work against a fixed reference and calls anything under
765
+ ~1.67× the medium baseline **low**, so the machine that loads the biggest scene
766
+ contends the most and tiers worst: an M5 Max holding 120fps was being given
767
+ low-tier budgets for terrain, shadows, reflections and ambient at once. It now
768
+ waits for ~1s of settled frames (`sceneBusy`), gives up after 30s, and caches a
769
+ measurement it had to take under load with a short life so the next visit
770
+ re-measures.
771
+ - **Ambient was shed during the loading screen and never came back** (#11). The
772
+ watchdog's 10s warmup was a fixed timer where the question is "has the scene
773
+ settled" — a streaming world loads for longer, so the one-way ratchet fired on
774
+ frames that said nothing about the hardware and ambient stayed at zero for the
775
+ session. The warmup clock now runs only while the scene is quiet.
776
+
777
+ - **`b3d-death` latched after the first death if the game respawned by its own
778
+ route.** `_dying` is cleared only by `resume()`, which only the panel's
779
+ Respawn button calls — and that button doesn't exist without a `respawn`
780
+ callback. So a scene that respawns from its own `crash` listener (a
781
+ documented pattern) left the component stuck, and it then swallowed every
782
+ later death: no panel, no focus release, welded to the wreck. That is the
783
+ exact failure this component exists to prevent, one level up. A death from an
784
+ entity that isn't the wreck it's holding now tears down and handles the new
785
+ one, and a panel built with no way out says so in the console. (Found by
786
+ manta-recon crashing a respawned aircraft.)
787
+
788
+ - **`Foo_ignore` was half-honoured**: the loader disposed only the hyphen form
789
+ while `publicName` stripped only the underscore one, so an underscore-form
790
+ node survived the load AND collapsed to `Foo` — colliding with a real `Foo`,
791
+ which made `instantiate('Foo')` resolve to whichever came first. Both
792
+ separators now go through `isIgnored`.
793
+ - **`spinner()` in the demo helpers** still used `engine.getDeltaTime()`, so on
794
+ any page importing it (the water demo among them) crates spun ~4× slow at the
795
+ default `frameRate: 30` — the one call site the `sceneDelta` sweep missed.
796
+
797
+ - **A crashed aircraft releases input focus so a respawn can take over** (#9). A wreck keeps input
798
+ focus and ignores it, so with no `<tosi-b3d-death>` in the scene the player
799
+ held a dead controller — which reads as broken controls rather than as
800
+ dying. The crash now releases focus if nothing else handled the event.
801
+ - **`recenter()` reset `worldV` to 0**, which is the `CylinderSampler` mirror
802
+ plane — undoing the new default and putting a seam through the world, hours
803
+ into a session where nobody would connect the two.
804
+
805
+ - **Tile skirts hung 344m through tunnels** — depth came from world amplitude
806
+ rather than the tile's own relief.
807
+ - `_centerOfGravity` markers, cave shading (`interior` as a depth ramp,
808
+ flooding by `waterTable`, `noWater`), volcanism confined to its caldera.
809
+
810
+ ## 0.7.0-rc.1
811
+
812
+ Published to npm under the `next` tag on 2026-08-13, superseded by 0.7.0.
813
+ Everything in 0.7.0 above applies, with two differences that matter if you
814
+ installed it:
815
+
816
+ - It exported the 13 `carve` functions as **bare top-level names**; 0.7.0 moves
817
+ them to `carve.*` (see ⚠️ Breaking).
818
+ - It did **not** export `sceneDelta`, despite the notes promising it — the
819
+ `getDeltaTime`-in-a-scene-observer fix was unusable from the package.
820
+
821
+ It was tagged **before** the pre-release review ran, deliberately, to get it
822
+ into an adopter's hands sooner. The review found no blocker in the shipped
823
+ behaviour; what it found were the two release-artifact gaps above.
824
+
825
+ ## 0.6.2
826
+
827
+ Terrain gets **volcanism** and **authored landforms**, clouds get real shapes, and a
828
+ systemic timing bug that made half the engine run in slow motion is fixed. Additive
829
+ only — no API removals, no peer-dependency changes. (The full terrain-system upgrade
830
+ lands as 0.7.0 once its remaining pieces are in.)
831
+
832
+ ### Added
833
+
834
+ - **Volcanism intensity ladder** ([biome-plugin](https://3d.tosijs.net/biome-plugin/)) —
835
+ the `volcanism` dial now climbs stages rather than scaling one look: near-black basalt
836
+ with dark-brown voronoi seams → dark-brown rock with glowing seams → patchy open lava.
837
+ Verticals lag one stage behind horizontals (lava pools flat; cliff faces drain and
838
+ crust over), veins widen as the ladder climbs so pools form by veins merging, and the
839
+ glow is subtly animated (spatially-phased pulse + drifting 3D-noise churn — never a
840
+ global blink). Water pushes the ladder down half a stage and mutes the glow with depth
841
+ instead of cancelling it: smouldering veins under shallow water.
842
+ - **`volcanicPalette`** + `LAVA_PALETTE` / `CRYOVOLCANIC_PALETTE` — the seven ladder
843
+ colours are data, so a frozen world venting molten _water_ (pale ice rock, teal veins,
844
+ glowing blue-white melt) is a palette swap, not a new shader.
845
+ - **Local volcanic provinces** — `b3dTerrain.provinceField = (x, z) => 0..1` marks
846
+ _this_ island volcanic independently of the global dial. Sampled per tile vertex in
847
+ origin-stable coordinates and carried to the shader in the colour buffer's (visually
848
+ inert) alpha channel.
849
+ - **[landform](https://3d.tosijs.net/landform/)** — authored landforms FORCED through
850
+ the terrain noise via the new `b3dTerrain.landform` hook, where
851
+ [slope-profile](https://3d.tosijs.net/slope-profile/)s only _remap_ it. `volcano()`
852
+ and `impactCrater()` each return a matched pair — the height shape **and** the
853
+ volcanism province that makes it glow — so a volcano goes anywhere and an explosion
854
+ leaves a glowing crater. `pad()` claims flat ground for cities and bases;
855
+ `composeLandforms` / `mergeProvinces` chain them. Pure, deterministic, unit-tested.
856
+ - **`_centerOfGravity` model convention** — a child node with that suffix declares where
857
+ a vehicle pivots in flight, while the root origin stays its on-ground stance point, so
858
+ one model conveys both how it flies and how it plants.
859
+ `findCenterOfGravity`/`applyCenterOfGravity` are exported; `b3d-aircraft` applies it
860
+ on both load paths.
861
+ - **`b3dClouds({ model })`** — an authored GLB mesh becomes the cloud lobe (normalized
862
+ on load, bottom-aligned), and clouds are now built as **clusters** of 2–4 overlapping
863
+ lobes laid out by size rank (biggest low, smallest on top straddling the pair) with
864
+ aligned bases, since cumulus condense at a level.
865
+ - **`B3d.frameDelta`** + **`sceneDelta(scene)`** — the authoritative inter-render delta
866
+ (see Fixed).
867
+
868
+ ### Fixed
869
+
870
+ - **Everything ticking in a scene observer ran in slow motion.** Babylon's
871
+ `getDeltaTime()` measures the engine's rAF tick, but scene observers only fire when
872
+ `scene.render()` runs — and `<tosi-b3d>` throttles rendering to `frameRate`. At
873
+ `frameRate: 60` on a 120Hz display that's **half speed**; at the default 30, a
874
+ **quarter**. Measured live: a dropped bomb inherited exactly 50% of the aircraft's
875
+ velocity. B3d now publishes the real delta and 14 call sites use it — projectiles,
876
+ missiles, turret, radar, warhead FX, spawner, clouds, ambient, death orbit, exploder,
877
+ star / star-system / planet / black-hole.
878
+ - **`canonicalize` applied a spurious 180° yaw**, flying correctly-authored (Blender
879
+ −Y-forward) models backwards. Babylon reads node-local data raw — the handedness flip
880
+ lives only on the discarded `__root__` — so the collapse now applies **no rotation**,
881
+ it only cleans. Pinned against real content in `model-frame.test.ts`.
882
+ - **Weapons inherited a phantom velocity.** `this.velocity` is only the aircraft's
883
+ hover/ground integrator and reads zero in wing-borne flight; weapons now inherit the
884
+ true world velocity, so bombs fall with you and cannon rounds lead correctly.
885
+ - **Weapon muzzles are computed through the world matrix**, so shots leave the visible
886
+ airframe under any attitude or centre-of-gravity pivot.
887
+ - **Flying into a cliff face passed through it** — the ground check was a single
888
+ downward ray, which reports the valley floor while a wall fills the windscreen.
889
+ Airborne frames now also sweep along the velocity; steep contacts crash.
890
+ - **Death could strand you in the wreck.** Charring the wreck cloned a material carrying
891
+ an unregistered plugin, Babylon threw, and the sequence aborted _after_ the explosion
892
+ but _before_ releasing input focus — no spectate camera, no respawn panel. Material
893
+ plugins are now registered for cloning, and death's exit is shielded so cosmetics can
894
+ never block it again.
895
+ - Volcano calderas are flat-floored basins with the melt confined to the floor and a
896
+ crusted rim (a smoothed shading normal can't make the rim read as open lava).
897
+ - Library instances carry their animations, retargeted onto the instance.
898
+
899
+ ## 0.6.1
900
+
901
+ First consumer-reported fixes — both filed by **manta-recon** (tosijs-3d's first
902
+ external adopter) with root-cause analyses read from the 0.6.0 dist. No API changes.
903
+
904
+ ### Fixed
905
+
906
+ - **Aircraft chase camera never created when focus adoption lands after a fast
907
+ mesh load** ([#1](https://github.com/tonioloewald/tosijs-3d/issues/1)). Both
908
+ sides deferred to the other: `setupMesh` skipped the camera when
909
+ `inputProvider` wasn't set yet, and `inputFocus` early-returned for any
910
+ self-managed entity — an ordering hole between them left the player flying
911
+ blind on `default-camera`. The focus side now nudges entities using the
912
+ deferral pattern (idempotent `setupFollowCamera`), so whichever side runs
913
+ last completes the setup.
914
+ - **fly-by-wire: zero-speed deadlock above `hoverCeiling`**
915
+ ([#2](https://github.com/tonioloewald/tosijs-3d/issues/2)). At full nose-up,
916
+ pitch decay (`diveBoost·sin(pitch)`) could exceed thrust, pinning speed at
917
+ the 0 clamp with the throttle held — a craft frozen mid-air. Held thrust is
918
+ now floored to make progress below `vtolSpeed` (a nose-high climb-out labors
919
+ instead of freezing); the throttle-released zoom-climb stall — the deliberate
920
+ "hard way into high-altitude hover" — is untouched.
921
+
922
+ ## 0.6.0
923
+
924
+ Cut as rc.1 → rc.3 (each reviewed; rc consumers see the per-rc sections in git
925
+ history), finalized after the nine-lens review's follow-up fixes landed.
926
+
927
+ The **SVG UI surface** release: a first-class, VR-ready UI substrate — container,
928
+ overlay, widgets seam, table, keyboard — plus the interaction contracts that make
929
+ the same surface work identically in the DOM, on a 3D plane, and under an XR
930
+ controller ray. No peer-dependency changes.
931
+
932
+ ### Added
933
+
934
+ - **`box`** — the flow container: blocks + inline items + wrapped text, background/
935
+ border/radius, resize-with-reflow, scroll regions, a coordinate-based event model
936
+ (press/activate, raw capturing `handlePointer` for drags), and D-pad focus
937
+ traversal with a visible ring.
938
+ - **`surface`** — content box + overlay layer: cascade **menus** and persistent
939
+ draggable/closable **panels**, `placePopup` flip/clamp positioning.
940
+ - **`widget-box`** — the seam that lets `widgets3d` controls (sliders, toggles…)
941
+ live inside a `box`/`surface` unchanged.
942
+ - **`table` / `table-layout`** — sticky header, virtualized body, drag-to-scroll,
943
+ icon **selection** (see `selection`), D-pad row traversal with the escape
944
+ contract.
945
+ - **`keyboard` / `key-layout` / `text-edit`** — the on-screen typing surface for a
946
+ headset: long-press accent strips (sticky on lift, generous ray-tolerant taps),
947
+ spacebar caret-trackpad (slow taps still type), pressed-key tint, hold-signifier
948
+ glyphs (▾ accents, ↔ caret drag), per-key D-pad focus; `inputField` with
949
+ code-point-correct editing and the **receiver-caret** model (lit = where text
950
+ lands, dim = not; `setActive`/`onFocus` for host exclusivity). `key-layout` grew
951
+ `numpad`/`dial`/`email`/`url` modes, gap-absorbing multi-unit keys (equal units ⇒
952
+ equal width), and vertical key spans (the numpad's tall enter).
953
+ - **`gamepad-focus`** — the D-pad → focus wire: edge-triggered with typematic
954
+ repeat (pure, testable `createFocusPulse`), menu/A activates, and `claim` scopes
955
+ one physical pad to the last-touched UI when several live on a page.
956
+ - **Inner-focus protocol** on `BoxChild`/`Widget3d` — `focusMove(dx, dy) → boolean`
957
+ (false = focus escaped, host moves on), `focusActivate`, `focusClear`, `setState`
958
+ reflection — so a composite widget (the keyboard) is one child but many focus
959
+ stops.
960
+ - **`panelScene`** — dual-presentation in two lines: textured plane + orbit camera
961
+ - pick routing (mouse AND XR-controller ray) → `handlePointer`, with one gesture
962
+ contract: a `claim(x, y)` policy for orbit-vs-UI, gestures collected on a stable
963
+ catcher quad via the **pick ray** (screen coordinates don't exist in a headset),
964
+ mapped through the gesture-start frame so a target that rescales its own plane
965
+ stays stable, camera-yield during claimed presses, off-plane release ends the
966
+ gesture.
967
+ - **`svgIcons.resize`** glyph (from tosijs-ui), used by the resizable-box demo's
968
+ in-corner grip.
969
+
970
+ ### Changed
971
+
972
+ - **`b3dSvgPlane`** routes the texture's alpha to the mesh (`opacityTexture`) —
973
+ transparent svg regions (outside rounded corners) are transparent on the plane,
974
+ not an opaque black substrate.
975
+ - Pick-forwarded events (`pointerEvents: 'on'`) carry a **synthetic pointerId**,
976
+ never the physical pointer's — a listener's `setPointerCapture` on a forwarded
977
+ event can no longer hijack the real mouse/ray stream.
978
+ - Panel demos are side-by-side DOM + 3D (50/50) throughout, built on `panelScene`.
979
+
980
+ ### Fixed
981
+
982
+ - **The lost-pointerup chain**: demos passing `pointerEvents: false` were silently
983
+ running with forwarding ON (tosijs discards wrong-typed prop writes — filed as
984
+ tosijs#24); the forwarded events carried the real pointerId, so the flat mirror
985
+ captured the physical mouse and the canvas stopped receiving pointerups. Fixed at
986
+ every layer, and routing is now **self-healing**: a fresh down while a gesture is
987
+ outstanding flushes the stale gesture (keyboard/box/surface), so a lost up costs
988
+ nothing instead of wedging the session.
989
+ - **`SvgTexture`**: a failed rasterize no longer freezes the plane for the session
990
+ — the busy latch always releases and the frame retries on the next tick.
991
+ - Keyboard: accent strips stay inside the keyboard's rect (top row opens below the
992
+ key); a strip tap can't fall through to the key behind it; releasing on the held
993
+ key with jitter goes sticky instead of inserting a random accent; focus follows
994
+ the click (ring on the tapped key); Space presses the focused key.
995
+ - Arrow keys no longer orbit demo cameras (`ArcRotateCameraKeyboardMoveInput`
996
+ removed — arrows drive the UI).
997
+
998
+ ### Added (post-rc, from the review follow-ups)
999
+
1000
+ - **`ui.*` namespace** — the SVG UI family's value exports live on one
1001
+ container (`ui.box`, `ui.table`, `ui.keyboard`, …): common nouns don't leak
1002
+ from the barrel. Types stay top-level. Migration from any rc:
1003
+ `import { ui } from 'tosijs-3d'` and destructure.
1004
+ - `Box.interactiveAt` / `Surface.interactiveAt`, and `panelScene`'s **default
1005
+ claim policy** asks them — pressing a button/panel claims the gesture,
1006
+ pressing static prose orbits the camera, consistently across every demo.
1007
+ - `panelGesture` + `uvToViewBox` + `planeLocalToViewBox` exported pure (the
1008
+ gesture contract is unit-pinned); named option types (`TableOptions`,
1009
+ `KeyboardOptions`, `InputFieldOptions`, `PanelSceneOptions`,
1010
+ `GamepadFocusOptions`); `panelScene` takes `updateInterval`; `w3dTheme`
1011
+ (one place reads the `--w3d-*` variables).
1012
+
1013
+ ### Fixed (post-rc)
1014
+
1015
+ - `table.focusMove` speaks the protocol's `(dx, dy)` (rc.1 trapped D-pad focus
1016
+ in a hosted table); menu leaf-select no longer destroys persistent panels
1017
+ (`Surface.closeMenus`).
1018
+ - box hover no longer sticks on raw children; a claimless `gamepadFocus`
1019
+ instance is no longer starved (click-away releases the claim).
1020
+ - Table scrolling within the window is transform-only and hover restyles
1021
+ surgically (the virtualized body no longer rebuilds per drag-move);
1022
+ `textBlock` caches its wrap per width.
1023
+ - `SvgTexture` warns once per instance when rasterizes fail (self-healing had
1024
+ made it silent); deploy/tunnel host reads `TOSIJS_DEPLOY_HOST` instead of a
1025
+ committed address.
1026
+
1027
+ ### ⚠️ Changed
1028
+
1029
+ - **`B3d.onResize()` / `B3dHud.onResize()` → `handleResize()`** (the `on*`
1030
+ prefix is reserved by the element-creator's listener sugar). A 0.5.x
1031
+ consumer calling `onResize()` must call `handleResize()`.
1032
+
1033
+ ## 0.5.2
1034
+
1035
+ ### ⚠️ Breaking — peer dependency (`tosijs` ≥ 1.7.8)
1036
+
1037
+ - **`tosijs` peer range `^1.6.9` → `^1.7.8`.** Consumers must be on `tosijs`
1038
+ **≥ 1.7.8**; a project on `1.6.x` will hit `ERESOLVE`. (1.7.6/1.7.7 are
1039
+ **deprecated on npm** — a `parts`-resolution regression; 1.7.8 fixes it, and
1040
+ tosijs-ui 1.7.4 requires it.) As with 0.5.1, this ships under a **patch**
1041
+ deliberately — `0.x` narrative versioning, with `0.5.0` reserved for the
1042
+ playable-game cut — so it's flagged here rather than forced to a minor.
1043
+ - _Dev/build only, no consumer action:_ `tosijs-ui` `1.6.23` → `1.7.4`, and
1044
+ `chokidar` added to `devDependencies` (tosijs-ui 1.7.x's shipped doc-site
1045
+ pipeline imports it at runtime but declares it dev-only — filed upstream).
1046
+
1047
+ ### Added
1048
+
1049
+ - **`svgIcons`** (`svg-icons.ts`) — an SVG **icon proxy** (`svgIcons.<name>()` →
1050
+ an SVG `ElementCreator`) over a focused, **generated** icon set
1051
+ (`icons/{color,stroked,filled}/*.svg` → `icon-data.ts` via tosijs-ui's
1052
+ `tosijs-make-icons`; `bun run icons`). A composition-suffix subset
1053
+ (rotate/flip/scale/translate/opacity/stroke-width/colour) + the generator's
1054
+ directional redirects + hand-authored aliases (`iconAliases`, e.g.
1055
+ `moreHorizontal`). ~30 icons incl. `tosijs3d`, `xrColor`, `tosiXr`, keyboard,
1056
+ checkbox/stats/status marks.
1057
+ - **`iconGlyph`** — a **texture-safe** icon primitive (explicit-colour SVG `<g>`)
1058
+ for embedding icons in in-scene `widgets3d` / `SvgTexture` UI, where
1059
+ `currentColor` and CSS vars don't resolve in the raster.
1060
+ - **`flowLayout`** (`flow-layout.ts`) — a pure **CSS block/inline-block flow**
1061
+ layout core: the substrate for a first-class SVG UI surface that lives in the
1062
+ DOM and on a 3D texture. Babylon/DOM-free, unit-tested.
1063
+ - **`b3d.snapshot()`** — capture the current view as a PNG **data URL**
1064
+ (resolution-independent, via Babylon's render-target screenshot; works flat,
1065
+ including in-scene panels).
1066
+ - **`icon-name.ts`** — the pure composition-suffix parser behind `svgIcons`.
1067
+
1068
+ ### Changed
1069
+
1070
+ - **Scene chrome → icon lozenge.** The top-left toolbar is now one rounded pill
1071
+ with two `svgIcons` buttons (settings + `xrColor` Enter-VR); the scene-panel `×`
1072
+ close is a `close` icon. The doc-browser **header logo** = `tosiXr` and the
1073
+ **favicon** = the teal cube, both via tosijs-ui 1.7.4's new `logo` option.
1074
+ - **README** gained a 280px logo and its stale `xinjs-3d` links were fixed.
1075
+
1076
+ ### Fixed
1077
+
1078
+ - **keyboard icon** rendering — the generator strips `fill-rule` from non-`color`
1079
+ icons, so its evenodd key-holes broke; moved to `color/` with `currentColor`
1080
+ (filed upstream). `iconGlyph` bakes `currentColor` to an explicit colour so it
1081
+ renders in the texture raster too.
1082
+ - **Lozenge visible without `:has()`** — it defaulted to `display:none` and
1083
+ revealed via `:has()`, so a browser lacking `:has()` (older Firefox) never
1084
+ showed the toolbar. Now default-visible; `:has()` only suppresses the empty-pill
1085
+ flash.
1086
+ - The svg-icons "composition suffixes" snippet is a **static** code block (was an
1087
+ erroring live example).
1088
+
1089
+ ## 0.5.1
1090
+
1091
+ ### ⚠️ Breaking — renamed public methods (`on*` → `add*Listener` / `handle*`)
1092
+
1093
+ No peer-dependency changes this release. The break is a **method rename**: tosijs's
1094
+ `elementCreator` treats any `on<Event>`-named member as `addEventListener` sugar and
1095
+ **shadows** a component method of the same name (it now fires a live warning, and it was a
1096
+ latent bug — the handler silently never ran). So the multi-listener subscribe methods and the
1097
+ lifecycle hooks moved off the `on*` prefix. The new names are also more honest: the subscribe
1098
+ methods **push to a listener list** (add/remove semantics), not a single-handler setter.
1099
+
1100
+ | Old (0.5.0) | New (0.5.1) | On |
1101
+ | -------------------------------------- | -------------------------------------------- | ---------------------------------- |
1102
+ | `onSceneAddition` / `offSceneAddition` | `addSceneListener` / `removeSceneListener` | `B3d` (scene owner) |
1103
+ | `onOriginShift` / `offOriginShift` | `addOriginListener` / `removeOriginListener` | `B3d` (scene owner) |
1104
+ | `onGainFocus` / `onLoseFocus` | `handleGainFocus` / `handleLoseFocus` | `B3dControllable` |
1105
+ | `onButton` | `handleButton` | `TouchGamepadSource`, `B3dGamepad` |
1106
+
1107
+ **What a consumer must do:** if you wrote a custom scene child that called
1108
+ `owner.onSceneAddition(cb)` / `owner.onOriginShift(cb)`, or subclassed `B3dControllable` and
1109
+ overrode `onGainFocus`/`onLoseFocus`, or a gamepad source with an `onButton` callback, rename to
1110
+ the new members. It's a pure rename — signatures are unchanged. Built-in components are all
1111
+ migrated; this only affects code you wrote against these surfaces.
1112
+
1113
+ ### Added
1114
+
1115
+ - **Coordinate-free `MinSimApi`** (`world-contract.ts` §8) — a world-simulation boundary where
1116
+ **coordinates never cross the membrane**: a driver (an AI narrative engine, a scripted demo)
1117
+ sees _topology + a qualitative distance ladder_, never `x/y/z`. Places, portals, a 7-rung
1118
+ proximity ladder (`same-spot`…`present`, `elsewhere` for a different place), a `SchematicView`
1119
+ ("where am I", exits, contents-with-rung), and `route()`. `WorldStore` now implements it
1120
+ alongside its existing surface — the sim keeps real geometry sim-private and answers only in
1121
+ qualities.
1122
+ - **`world-topology.ts`** — the pure, Babylon-free, deterministic spatial math behind the
1123
+ surface: `proximityRung(distance, extent)` (a distance in, an adjective out; bands scale with
1124
+ a place's `extent`), `rungNominal` (the inverse, for placement), `routePortals` (cheapest
1125
+ portal path, bidirectional, locked portals impassable — Dijkstra, deterministic tie-break),
1126
+ `containmentPath` (root→here breadcrumb). Unit-tested without a store or an engine.
1127
+ - **`min-sim-conformance.ts`** — a **framework-agnostic shared conformance kit**
1128
+ (`runMinSimConformance(makeApi, harness)`) that pins the contract behaviour identically for
1129
+ any `MinSimApi` implementation. It imports no test runner (so it ships in the library) and
1130
+ takes the `describe`/`test`/`expect` harness as an argument — the same kit runs in this repo
1131
+ and in a driver's repo (the Ariosto use case), proving both stores behave the same at the seam.
1132
+ - **Clouds drift with wind** — the cloud layer now moves with the wind vector, so the sky is
1133
+ alive rather than a still image.
1134
+
1135
+ ### Changed
1136
+
1137
+ - **Demo cameras** migrated to a shared `demo-utils` `orbitCam` helper (tilt-clamped so an orbit
1138
+ can't dip below the horizon) instead of 24 hand-rolled per-demo cameras. Authoring-only — no
1139
+ library-API impact.
1140
+
1141
+ ### Fixed
1142
+
1143
+ - **Cloud whiteout** now behaves correctly: it ramps to full **earlier** (less "snaps on at the
1144
+ moment of entry", denser once inside), **blots out the sky** (not just scene geometry), and
1145
+ **mutes projected cloud shadows** under the whiteout (fogged fragments no longer darken through
1146
+ the fog).
1147
+ - **`b3d-controller`** `player` now defaults to `false`. tosijs began **throwing** on an
1148
+ `initAttributes` boolean that defaults to `true` (an absent HTML boolean attribute is `false`,
1149
+ so a `true` default can never turn on) — the old `player: true` default made the controller
1150
+ throw at construction and silently never wire input.
1151
+
1152
+ ## 0.5.0
1153
+
1154
+ ### ⚠️ Breaking — peer dependencies
1155
+
1156
+ - **Babylon.js 8 → 9.** Peer range for `@babylonjs/core`, `@babylonjs/gui`,
1157
+ `@babylonjs/loaders`, `@babylonjs/materials` moved `^8` → `^9`. **Consumers must
1158
+ upgrade their Babylon to `9.x`.** A project still on Babylon 8 will hit an
1159
+ `ERESOLVE` on install.
1160
+ - **Jolt physics `^1.0` → `^1.1`** (Jolt 5.6.0 — the friction model moved). Only
1161
+ relevant if you use `<tosi-b3d-physics>`; retune friction if grip/slide changed.
1162
+ - **Packaging fix:** `@babylonjs/*` and `jolt-physics` are now **peer-only** (they
1163
+ were previously declared in both `dependencies` and `peerDependencies`). A hard
1164
+ Babylon dependency would nest a _second_ engine copy in a consumer holding a
1165
+ direct Babylon dep — silently breaking `instanceof`, engine singletons, and
1166
+ plugin registration. No action needed by consumers; this just makes install
1167
+ resolution correct.
1168
+
1169
+ ### Added
1170
+
1171
+ - **Clouds** (`<tosi-b3d-clouds>`) — an opaque blob cloud layer you can _fly into_
1172
+ and lose the world inside (a fog whiteout, not a texture). A `coverage` weather
1173
+ dial from wisps to thunderheads, and `insideCloud` so a cloud is a _tactic_
1174
+ (break a radar lock, shake a pursuer).
1175
+ - **Projected cloud shadows** (`cloud-shadows.ts`) — the field painted top-down
1176
+ into one texture, sampled by world position in a material plugin, so shadows
1177
+ conform to terrain and fall on elevated receivers (the aircraft) too. Reusable.
1178
+ - **Shadow decals** (`shadow-decal.ts`) — a reusable soft blob-shadow for a single
1179
+ caster (character, vehicle, dropped item).
1180
+ - **Death** (`<tosi-b3d-death>`) — death's exit: explode + burning wreckage,
1181
+ release input, a third-person spectate shot, then a respawn panel. Flat and VR.
1182
+ - **Encounters** — `<tosi-b3d-spawner>` (seeded, "same seed, same battles"),
1183
+ `formations.ts` (pure placement math), and a named `prefab` registry with
1184
+ `spawnPrefab` for the set-dressing a death or spawn drops.
1185
+ - **Ambient effects** (`<tosi-b3d-ambient>`) — device-budgeted garnish (motes,
1186
+ bubbles, tumbling leaves) that competes for one pool and switches **off** rather
1187
+ than thinning into a lie. Pure budget allocator in `ambient-budget.ts`.
1188
+ - **Atmosphere** (`atmosphere.ts`) — pure fog compositing so underwater / cloud /
1189
+ space whiteouts layer over the base fog instead of fighting it.
1190
+ - **`setGameplayCamera`** — an XR-safe viewpoint affordance (move the rig in a
1191
+ headset, swap the camera when flat), so chase/death/vehicle transitions stop
1192
+ breaking in VR.
1193
+ - **`PLATFORM.md`** — the platform bet (stay on the web, don't abstract the
1194
+ renderer; the real risk of the Android XR / Vision Pro shift is _input_).
1195
+
1196
+ ### Changed
1197
+
1198
+ - **Aircraft** — the stick is dead on the ground (only the throttle lifts off);
1199
+ you can't crash-land until you've actually cleared the pad; chase camera banks
1200
+ smoothly and no longer jitters on throttle.
1201
+ - **Water** follows the camera as an endless sea; **terrain** auto-centers so a sea
1202
+ at `y=0` floods the valleys.
1203
+ - **HUD** survives respawn.
1204
+ - Babylon `8.56 → 9.16`, Jolt `1.0 → 1.1`, tosijs-ui `1.6.22 → 1.6.23`, tosijs
1205
+ `1.6.8 → 1.6.9` (dev-server/haltija reliability), haltija `→ 1.4.0`.
1206
+
1207
+ ### Fixed
1208
+
1209
+ - Death explosion/wreck FX land at the real wreck; a respawn is no longer charred
1210
+ (char a cloned material, not the shared library one).
1211
+ - The aircraft ground-ray no longer picks a cloud blob as "ground" (mid-air
1212
+ "crash"); cloud whiteout is opaque under LINEAR fog and builds from every
1213
+ direction; the live-debug timer is cleared on scene disconnect (no leak).