tosijs-3d 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +900 -0
- package/dist/b3d-aircraft.d.ts +14 -0
- package/dist/b3d-aircraft.d.ts.map +1 -1
- package/dist/b3d-aircraft.js +45 -0
- package/dist/b3d-aircraft.js.map +1 -1
- package/dist/b3d-ambient.d.ts +12 -0
- package/dist/b3d-ambient.d.ts.map +1 -1
- package/dist/b3d-ambient.js +83 -1
- package/dist/b3d-ambient.js.map +1 -1
- package/dist/b3d-biped.d.ts +290 -0
- package/dist/b3d-biped.d.ts.map +1 -1
- package/dist/b3d-biped.js +1366 -22
- package/dist/b3d-biped.js.map +1 -1
- package/dist/b3d-collisions.d.ts.map +1 -1
- package/dist/b3d-collisions.js +45 -7
- package/dist/b3d-collisions.js.map +1 -1
- package/dist/b3d-controllable.d.ts +50 -0
- package/dist/b3d-controllable.d.ts.map +1 -1
- package/dist/b3d-controllable.js +61 -0
- package/dist/b3d-controllable.js.map +1 -1
- package/dist/b3d-death.d.ts +26 -0
- package/dist/b3d-death.d.ts.map +1 -1
- package/dist/b3d-death.js +224 -2
- package/dist/b3d-death.js.map +1 -1
- package/dist/b3d-interactive.d.ts +80 -0
- package/dist/b3d-interactive.d.ts.map +1 -0
- package/dist/b3d-interactive.js +284 -0
- package/dist/b3d-interactive.js.map +1 -0
- package/dist/b3d-library.d.ts +39 -0
- package/dist/b3d-library.d.ts.map +1 -1
- package/dist/b3d-library.js +67 -0
- package/dist/b3d-library.js.map +1 -1
- package/dist/b3d-svg-plane.d.ts +35 -0
- package/dist/b3d-svg-plane.d.ts.map +1 -1
- package/dist/b3d-svg-plane.js +96 -8
- package/dist/b3d-svg-plane.js.map +1 -1
- package/dist/b3d-water.d.ts.map +1 -1
- package/dist/b3d-water.js +32 -6
- package/dist/b3d-water.js.map +1 -1
- package/dist/buoyancy.d.ts +90 -0
- package/dist/buoyancy.d.ts.map +1 -0
- package/dist/buoyancy.js +150 -0
- package/dist/buoyancy.js.map +1 -0
- package/dist/curve-field.d.ts +39 -0
- package/dist/curve-field.d.ts.map +1 -0
- package/dist/curve-field.js +491 -0
- package/dist/curve-field.js.map +1 -0
- package/dist/curve.d.ts +241 -0
- package/dist/curve.d.ts.map +1 -0
- package/dist/curve.js +559 -0
- package/dist/curve.js.map +1 -0
- package/dist/dialog-placement.d.ts +69 -7
- package/dist/dialog-placement.d.ts.map +1 -1
- package/dist/dialog-placement.js +90 -9
- package/dist/dialog-placement.js.map +1 -1
- package/dist/embed-font.d.ts +48 -0
- package/dist/embed-font.d.ts.map +1 -0
- package/dist/embed-font.js +135 -0
- package/dist/embed-font.js.map +1 -0
- package/dist/footprint-field.d.ts +30 -0
- package/dist/footprint-field.d.ts.map +1 -0
- package/dist/footprint-field.js +256 -0
- package/dist/footprint-field.js.map +1 -0
- package/dist/frame-panel.d.ts.map +1 -1
- package/dist/frame-panel.js +15 -12
- package/dist/frame-panel.js.map +1 -1
- package/dist/glb-manifest.d.ts +75 -0
- package/dist/glb-manifest.d.ts.map +1 -0
- package/dist/glb-manifest.js +198 -0
- package/dist/glb-manifest.js.map +1 -0
- package/dist/icon-data.d.ts.map +1 -1
- package/dist/icon-data.js +20 -1
- package/dist/icon-data.js.map +1 -1
- package/dist/index.d.ts +33 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -3
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +66 -0
- package/dist/interaction.d.ts.map +1 -0
- package/dist/interaction.js +117 -0
- package/dist/interaction.js.map +1 -0
- package/dist/interactive-behavior.d.ts +105 -0
- package/dist/interactive-behavior.d.ts.map +1 -0
- package/dist/interactive-behavior.js +253 -0
- package/dist/interactive-behavior.js.map +1 -0
- package/dist/key-layout.d.ts +70 -0
- package/dist/key-layout.d.ts.map +1 -1
- package/dist/key-layout.js +122 -0
- package/dist/key-layout.js.map +1 -1
- package/dist/keyboard-gamepad.d.ts.map +1 -1
- package/dist/keyboard-gamepad.js +19 -6
- package/dist/keyboard-gamepad.js.map +1 -1
- package/dist/keyboard.d.ts +100 -1
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +280 -35
- package/dist/keyboard.js.map +1 -1
- package/dist/mantle.d.ts +56 -0
- package/dist/mantle.d.ts.map +1 -0
- package/dist/mantle.js +115 -0
- package/dist/mantle.js.map +1 -0
- package/dist/popup-surface.d.ts +5 -1
- package/dist/popup-surface.d.ts.map +1 -1
- package/dist/popup-surface.js +181 -14
- package/dist/popup-surface.js.map +1 -1
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +111 -15
- package/dist/surface.js.map +1 -1
- package/dist/svg-icons.d.ts.map +1 -1
- package/dist/svg-icons.js +13 -1
- package/dist/svg-icons.js.map +1 -1
- package/dist/svg-texture.d.ts.map +1 -1
- package/dist/svg-texture.js +40 -1
- package/dist/svg-texture.js.map +1 -1
- package/dist/swim-aim.d.ts +78 -0
- package/dist/swim-aim.d.ts.map +1 -0
- package/dist/swim-aim.js +140 -0
- package/dist/swim-aim.js.map +1 -0
- package/dist/theme-editor.d.ts +41 -0
- package/dist/theme-editor.d.ts.map +1 -0
- package/dist/theme-editor.js +303 -0
- package/dist/theme-editor.js.map +1 -0
- package/dist/tosi-b3d.d.ts +24 -4
- package/dist/tosi-b3d.d.ts.map +1 -1
- package/dist/tosi-b3d.js +238 -37
- package/dist/tosi-b3d.js.map +1 -1
- package/dist/vector-field.d.ts +58 -0
- package/dist/vector-field.d.ts.map +1 -0
- package/dist/vector-field.js +321 -0
- package/dist/vector-field.js.map +1 -0
- package/dist/virtual-gamepad.d.ts.map +1 -1
- package/dist/virtual-gamepad.js +46 -9
- package/dist/virtual-gamepad.js.map +1 -1
- package/dist/w3d-theme.d.ts +115 -17
- package/dist/w3d-theme.d.ts.map +1 -1
- package/dist/w3d-theme.js +311 -0
- package/dist/w3d-theme.js.map +1 -1
- package/dist/water-normal.d.ts +26 -0
- package/dist/water-normal.d.ts.map +1 -0
- package/dist/water-normal.js +105 -0
- package/dist/water-normal.js.map +1 -0
- package/dist/widgets3d-layout.d.ts +58 -0
- package/dist/widgets3d-layout.d.ts.map +1 -1
- package/dist/widgets3d-layout.js +79 -1
- package/dist/widgets3d-layout.js.map +1 -1
- package/dist/widgets3d.d.ts +51 -2
- package/dist/widgets3d.d.ts.map +1 -1
- package/dist/widgets3d.js +418 -112
- package/dist/widgets3d.js.map +1 -1
- package/dist/wreck-fall.d.ts +86 -0
- package/dist/wreck-fall.d.ts.map +1 -0
- package/dist/wreck-fall.js +129 -0
- package/dist/wreck-fall.js.map +1 -0
- package/package.json +9 -6
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,906 @@ All notable changes to **tosijs-3d**. This project is pre-1.0 (`0.x`), so minor
|
|
|
6
6
|
versions may carry breaking peer-dependency changes — each is called out in a
|
|
7
7
|
**⚠️ Breaking** block in its version section below, with what a consumer must do.
|
|
8
8
|
|
|
9
|
+
## 0.7.4
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`curve3d` + `footprint3d` — author a terrain province instead of coding
|
|
14
|
+
one.** A province is a **footprint** (extent by direction) plus **one curve
|
|
15
|
+
per layer**: a `shape` that remaps the height sample, and a `falloff` that
|
|
16
|
+
says how far its say extends. The pure model is `curve.ts` (Babylon-free,
|
|
17
|
+
55 tests); the province editor demo lives on `/curve-field/`.
|
|
18
|
+
|
|
19
|
+
**The range is closed on purpose.** A curve maps `[0,1]` to `[0,1]` and a drag
|
|
20
|
+
clamps rather than pushing the range, because a profile that can return 1.4
|
|
21
|
+
silently changes the height a province occupies — which is exactly what
|
|
22
|
+
`carve`/`patch-field` must agree about, and it fails as *geometry* while
|
|
23
|
+
reporting nothing. Amplitude belongs to the block; shape belongs to the curve.
|
|
24
|
+
`blendSample` composes them convexly, so a tile's bounds are known before
|
|
25
|
+
anything is evaluated, however many provinces overlap.
|
|
26
|
+
|
|
27
|
+
**A falloff is pinned to 0 at its edge; a profile is not.** A province still
|
|
28
|
+
carrying weight at its boundary does not blend into the terrain around it —
|
|
29
|
+
another silent step. Not monotonic, though: a crater rim and a volcano cone
|
|
30
|
+
are non-monotonic falloffs, so the edge is pinned and the middle left alone.
|
|
31
|
+
|
|
32
|
+
**A footprint is a polygon, not a sampled curve.** `ngon(6)` is six vertices,
|
|
33
|
+
and `polygonExtent` casts a real ray at the straight edge — interpolating
|
|
34
|
+
radius against angle bows every edge inward, which is why an earlier draft
|
|
35
|
+
carried twelve samples per edge to avoid doing one intersection. A circle is
|
|
36
|
+
therefore the expensive shape: a 16-gon, indistinguishable at province scale.
|
|
37
|
+
Vertices cannot pass their neighbours or reach the centre, which keeps the
|
|
38
|
+
polygon star-shaped — the property that makes "extent in this direction" have
|
|
39
|
+
an answer.
|
|
40
|
+
|
|
41
|
+
Presets are named for what they **are**: `shelf + mountains`, `desert
|
|
42
|
+
terraces`, `plateau`, `smooth edge`, `abrupt edge`, `messy circle`. "ease
|
|
43
|
+
in-out" describes a graph; "smooth edge" describes a province.
|
|
44
|
+
|
|
45
|
+
- **`vector3d` / `euler3d` — a coordinate on ONE row.** Three stacked labelled
|
|
46
|
+
fields is why an inspector panel ends up three times taller than it needs to
|
|
47
|
+
be. `euler3d` is not a styling variant: it **wraps** into `(-180, 180]` where
|
|
48
|
+
`vector3d` clamps, because a clamp fights you at exactly the angles you most
|
|
49
|
+
want to scrub through. Degrees, per the angle rule.
|
|
50
|
+
|
|
51
|
+
A row is **three tab stops**, not one: the host tracks focus per widget, so
|
|
52
|
+
the row keeps its own axis index and lights exactly one caret — all three at
|
|
53
|
+
once says focus is everywhere, which says nothing.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **A yielded camera is always given back.** Dragging a popup detaches the
|
|
58
|
+
camera so it does not orbit inside the same gesture, but the restore sat
|
|
59
|
+
behind an empty-popup-list early return — so anything that emptied the list
|
|
60
|
+
mid-gesture stranded the camera detached, with no way back. A detached camera
|
|
61
|
+
is a dead scene, not a glitch. Also added a `window` pointerup/pointercancel
|
|
62
|
+
backstop, because a pointer released outside the canvas produces no scene
|
|
63
|
+
event at all, and dragging toward the edge is how you leave the canvas.
|
|
64
|
+
|
|
65
|
+
- **`openPopup` threw when opened by a click.** It called `attachDrag()` from a
|
|
66
|
+
`whenMesh` callback declared above it — fine while that callback defers, which
|
|
67
|
+
is what happens at mount. But `whenReady` fires immediately when the scene is
|
|
68
|
+
already up, and appending a plane to a live scene runs its `sceneReady`
|
|
69
|
+
synchronously too, so opening a popup from a click completed the whole chain
|
|
70
|
+
inside `openPopup` and hit `attachDrag` in its temporal dead zone. Minified,
|
|
71
|
+
that reads `Cannot access 'E' before initialization`. The throw escaped before
|
|
72
|
+
the function returned, so the caller got no popup **and** no drag — one
|
|
73
|
+
exception, two symptoms, neither pointing at declaration order.
|
|
74
|
+
|
|
75
|
+
- **Dragging a popup no longer re-centres it under the pointer.** Babylon's
|
|
76
|
+
`_startDrag` falls back to a ray from the camera position when `fromRay` is
|
|
77
|
+
omitted, so passing only `startPickedPoint` was not enough: it still
|
|
78
|
+
recomputed the drag plane from a default. Both are needed — the ray says where
|
|
79
|
+
the gesture points, the point says where on the panel it took hold.
|
|
80
|
+
|
|
81
|
+
- **A panel owns its background.** It painted nothing of its own, so the seam
|
|
82
|
+
between the title bar and the content box leaked two ways: `box` draws its
|
|
83
|
+
background rounded, so its top corners curve away from the square-bottomed
|
|
84
|
+
bar, and it insets by `borderWidth / 2`, giving a hairline down both sides.
|
|
85
|
+
A panel is an opaque thing; the parts now sit on top of it. Panel chrome also
|
|
86
|
+
reads the theme, which it never did — it was hardcoded.
|
|
87
|
+
|
|
88
|
+
- **Popups have a rim, since they cannot have a drop shadow.** The panel is
|
|
89
|
+
emissive so it is unlit, and a cast shadow needs a receiver — a popup floating
|
|
90
|
+
in front of a scene has nothing to fall on. Drawn at **double** stroke width
|
|
91
|
+
so the clip discards the outer half of a stroke that straddles its path,
|
|
92
|
+
leaving the intended width with no half-pixel arithmetic. `muted` is a mid
|
|
93
|
+
grey deliberately: the backdrop is arbitrary 3D, so a light rim vanishes on a
|
|
94
|
+
pale scene and a dark one on a dark scene.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
- **Numeric fields scrub** — `inputField({type:'number', scrub, step, min, max})`
|
|
98
|
+
drags to adjust and clicks to type (tosijs-3d#50). Scrubbing lives on the
|
|
99
|
+
field rather than in a separate control because a typed field already knows it
|
|
100
|
+
is numeric; otherwise every numeric widget needs its own copy of parse, format
|
|
101
|
+
and commit.
|
|
102
|
+
|
|
103
|
+
**Tap versus drag is a distance, not a mode** — a press that never travels
|
|
104
|
+
places the caret, one that moves scrubs. No modifier and no separate hit zone,
|
|
105
|
+
which matters most in XR where aiming is the expensive part.
|
|
106
|
+
|
|
107
|
+
- **`slider3d`'s `step` is documented** — it always quantised the drag _and_ the
|
|
108
|
+
reported value, but nothing said so, so a consumer generating panels from JSON
|
|
109
|
+
Schema concluded sliders were continuous and turned snap settings into
|
|
110
|
+
cyclers. There is now a **widget reference table** covering every widget's
|
|
111
|
+
options, plus the methods on a panel and a field.
|
|
112
|
+
- **⚠️ `b3dWater`'s normal map is now PROCEDURAL by default** (tosijs-3d#46).
|
|
113
|
+
It defaulted to `/waterbump.png` — a file that ships in this repo's `static/`
|
|
114
|
+
and **not in the published package** — so every consumer taking the default
|
|
115
|
+
pointed at something they had never been told to serve. Babylon falls back
|
|
116
|
+
when a texture will not decode, so the sea rendered as a **checkerboard**,
|
|
117
|
+
which reads as a style rather than a fault; it was reported as _"is the water
|
|
118
|
+
supposed to look like a checkerboard (it actually looks awesome…)"_.
|
|
119
|
+
|
|
120
|
+
Shipping the PNG would have fixed the missing file and left the real problem:
|
|
121
|
+
a default that depends on a fixed URL resolving in someone else's app. The
|
|
122
|
+
built-in map is now generated from this library's own Perlin noise — no file,
|
|
123
|
+
no network, no path, and it works offline and in a headset. It tiles by
|
|
124
|
+
construction (sampled on a torus), so there is no seam to hide.
|
|
125
|
+
|
|
126
|
+
An explicit `normalMap` that fails to load now **logs an error** instead of
|
|
127
|
+
silently becoming a checkerboard. Pass `normalMap` to override.
|
|
128
|
+
|
|
129
|
+
- **`panel.openPopup()` — a popup is just another panel** (tosijs-3d#37, item
|
|
130
|
+
4). This was the seam blocking a real `select`, the colour picker, and any
|
|
131
|
+
in-panel menu: `panel3d` returned a bare `<svg>` while `openPopup`/`openMenu`
|
|
132
|
+
wanted a `Surface`, so a control inside a panel had nowhere to put its list.
|
|
133
|
+
|
|
134
|
+
Nothing new was needed to build one — it is a `panel3d` with `height: 'fit'`
|
|
135
|
+
placed by `placePopup` (which already flips and clamps). It returns the panel
|
|
136
|
+
plus **where** it goes; mounting stays the host's job, because that is the
|
|
137
|
+
part that genuinely differs: flat it is a positioned sibling, in a scene it is
|
|
138
|
+
another plane. A popup living inside its opener would be clipped by the
|
|
139
|
+
opener's own viewBox, which is exactly why it has to be its own panel.
|
|
140
|
+
|
|
141
|
+
The popup is capped at its bounds, so a long list scrolls rather than growing
|
|
142
|
+
past the surface it must land on — flipping cannot rescue a popup that is
|
|
143
|
+
taller than both sides.
|
|
144
|
+
|
|
145
|
+
- **Fixed: injected colour controls set tokens to `[object Event]`.** A tosijs
|
|
146
|
+
`Component` binds any `on*` prop as a **DOM event listener**, so an injected
|
|
147
|
+
`colorInput({onChange})` calls back with an `Event` rather than a colour.
|
|
148
|
+
Setting a token to one throws nothing — it stringifies, fails to parse, and
|
|
149
|
+
the widget paints **black**. `themeEditor` now accepts either and ignores
|
|
150
|
+
anything it cannot read as a colour.
|
|
151
|
+
- **⚠️ Panels no longer cast shadows by default.** `register()` is the
|
|
152
|
+
shadow-caster contract and a panel registers like any other mesh, so every
|
|
153
|
+
in-scene panel threw a hard-edged rectangle across whatever it faced.
|
|
154
|
+
|
|
155
|
+
It is the right default for what a panel _is_: UI is not scenery — a HUD or an
|
|
156
|
+
inspector is something you look **through** the world at, and a shadow makes
|
|
157
|
+
it furniture. It is also the worst case for the shadow map, being a large flat
|
|
158
|
+
quad that (when camera-relative) never leaves `activeDistance` and so never
|
|
159
|
+
culls. Set `castShadow` for a panel that really is scenery — a sign on a wall,
|
|
160
|
+
a screen in a room.
|
|
161
|
+
|
|
162
|
+
- **The scene panel sits in the top-left corner**, inset equally from top and
|
|
163
|
+
left, instead of hanging below the gear button. It sat clear of the button
|
|
164
|
+
that opens it, which pushed its own content off the bottom on a short scene —
|
|
165
|
+
and covering the gear is fine, because the panel carries its own close box.
|
|
166
|
+
- **The scene panel updates when pause state changes elsewhere.** The transport
|
|
167
|
+
row picks its label and icon from `paused`, but nothing repainted the panel
|
|
168
|
+
when that flipped — so resuming from the pause _dialog_ left the panel still
|
|
169
|
+
offering **Play**. It only looked right before because the common path is
|
|
170
|
+
pressing the panel's own button, which reopens the panel afterwards; a second
|
|
171
|
+
route to the same state had no reason to.
|
|
172
|
+
- **The `surface` demo's debug readout is a real 3D popup**, not an overlay
|
|
173
|
+
painted into the opener's own SVG — so in the scene it is a panel with depth
|
|
174
|
+
that can sit in front of the surface and be dragged in world space, rather
|
|
175
|
+
than a picture of one stuck to the plane. The flat view keeps the in-panel
|
|
176
|
+
form, because in a document a popup _is_ a box drawn over another box.
|
|
177
|
+
- **`buttonActiveText`** — the label colour while a clickable is held or
|
|
178
|
+
selected. Separate from `text` because the background under it has changed: a
|
|
179
|
+
theme whose `buttonActive` is a strong accent needs a light label on it, and
|
|
180
|
+
baking that into `text` would force every label in the panel to follow a
|
|
181
|
+
decision that is only about buttons. A held `button3d` and a selected icon
|
|
182
|
+
both take it.
|
|
183
|
+
- **"button" means any CLICKABLE**, and now says so in the source.
|
|
184
|
+
`buttonBg`/`buttonHover`/`buttonActive`/`buttonActiveText` style every widget
|
|
185
|
+
you can press — buttons, icon buttons, toggles, list rows, select cyclers.
|
|
186
|
+
`widget*` would be vaguer (a label is a widget and has no press state) and
|
|
187
|
+
`clickable*` is what they mean but longer than anyone will type.
|
|
188
|
+
- **`iconBar3d` colours like a button.** It used `buttonActive` — the _press_
|
|
189
|
+
colour — for the selected item, so a selected icon looked permanently held and
|
|
190
|
+
pressing one showed nothing new. Now three escalating states (`buttonBg` →
|
|
191
|
+
`buttonHover` → `buttonActive` while held) plus **`selectedBg`** for "this one
|
|
192
|
+
is on", which is a different axis and why the theme has a separate token for
|
|
193
|
+
it. Release clears the pressed look _before_ the handler runs, so a handler
|
|
194
|
+
that rebuilds the panel cannot leave a button stuck looking held; and a press
|
|
195
|
+
that drifts off its button no longer fires it.
|
|
196
|
+
- **`strokeWidth` reaches icons.** `iconGlyph` hardcoded `2`, so the token
|
|
197
|
+
affected nothing but the text caret — a themed panel could not make its icons
|
|
198
|
+
match its own line weight. It now defaults to `w3dTheme.strokeWidth`, and an
|
|
199
|
+
explicit option still wins (an icon used as a mark may want its own weight).
|
|
200
|
+
- **Web fonts now reach in-scene panels** — `registerSvgFont(family, url)`
|
|
201
|
+
fetches the face, base64s it, and injects an `@font-face` **into the
|
|
202
|
+
serialised SVG**, which is the only place a rasterised copy can find it.
|
|
203
|
+
|
|
204
|
+
A serialised SVG is its own document: it inherits neither the page's font
|
|
205
|
+
faces nor its custom properties (the second half is why `w3d-theme` bakes
|
|
206
|
+
literals). The symptom was quiet — a scene panel in the fallback family while
|
|
207
|
+
the identical flat panel rendered correctly.
|
|
208
|
+
|
|
209
|
+
Only faces the markup actually mentions are injected, because the payload is
|
|
210
|
+
re-parsed on every rasterisation; a woff2 is 20–100 KB and base64 adds a
|
|
211
|
+
third, so carrying every registered font on every texture would be the real
|
|
212
|
+
cost. Cached per URL, and a failed fetch is not cached as the answer.
|
|
213
|
+
|
|
214
|
+
- **`withTheme(partial, build)` — one default, per-panel overrides.**
|
|
215
|
+
`setW3dTheme` sets the default; this builds something under an override and
|
|
216
|
+
restores it afterwards (in a `finally`, so a throw cannot leave the palette
|
|
217
|
+
changed — a theme that silently persists after an error makes the _next_
|
|
218
|
+
widget look wrong for no visible reason).
|
|
219
|
+
|
|
220
|
+
It works because of the property that makes a single global table safe: a
|
|
221
|
+
widget reads the theme when it is **built**. So a scope is just set-build-
|
|
222
|
+
restore — no plumbing, no second table, and a widget built inside keeps its
|
|
223
|
+
colours forever, since they were baked into its attributes.
|
|
224
|
+
|
|
225
|
+
**Why not `panel3d({ theme })`:** a panel's children are constructed as
|
|
226
|
+
_arguments_, so they already exist by the time the panel function runs. An
|
|
227
|
+
option on the panel could only recolour the panel's own background while its
|
|
228
|
+
contents kept the default — worse than not offering it. Wrapping the
|
|
229
|
+
construction puts the children inside the scope, because that is when they
|
|
230
|
+
evaluate.
|
|
231
|
+
|
|
232
|
+
- **`themeEditor()` — the palette editor is a component**, not demo code. An
|
|
233
|
+
adopter theming an app wants exactly this UI, and copying it out of a doc
|
|
234
|
+
comment is how it drifts from the palette it edits. Its own module, so it
|
|
235
|
+
tree-shakes out of a build that never imports it.
|
|
236
|
+
|
|
237
|
+
Every metric has a **slider and a number field, kept in sync** — a slider
|
|
238
|
+
alone hides the value you are setting, and you cannot drag an exact 0.05.
|
|
239
|
+
The colour control is **injected**: alpha matters (`panelBg`, `rowHover` and
|
|
240
|
+
`selectedBg` are `rgba()`) and `<input type="color">` silently drops it, but
|
|
241
|
+
tosijs-ui is deliberately not a dependency — so pass its `colorInput`, or take
|
|
242
|
+
the native fallback knowing what it costs.
|
|
243
|
+
|
|
244
|
+
- **⚠️ The theme now actually reaches the widgets.** Every `--w3d-*` value was
|
|
245
|
+
captured in a module-level `const` at **import** time, so `setW3dTheme` could
|
|
246
|
+
never affect anything — colours, fonts and metrics alike. Only
|
|
247
|
+
`roundedRadius` appeared to work, because it happened to be read inline.
|
|
248
|
+
Reads are now live (getters), so a widget takes the theme in force when it is
|
|
249
|
+
**built**, which is what the docs always claimed.
|
|
250
|
+
- **The SVG UI is themeable** — 21 tokens, up from 17. Added `focus`,
|
|
251
|
+
`selectedBg`, `disabledBg`, `disabledText`, `strokeWidth`, `roundedRadius`,
|
|
252
|
+
`spacing`, `lineHeight`, `codeFontFamily`, `codeFontWeight`, plus `overlay`,
|
|
253
|
+
`divider`, `placeholder` and `caret`.
|
|
254
|
+
|
|
255
|
+
**`setW3dTheme(partial)` overrides at runtime.** The `--w3d-*` variables are
|
|
256
|
+
read once at load and deliberately so — an SVG destined for a texture is
|
|
257
|
+
serialised away from the document, where `var(--w3d-text)` resolves against
|
|
258
|
+
nothing and paints black. This is the way back in, and what a theme editor
|
|
259
|
+
needs. Widgets read the theme when they are **built**, so rebuild them after
|
|
260
|
+
calling it; a widget that re-read its colours would have to re-resolve them
|
|
261
|
+
per rasterised texture, which is the cost this design exists to avoid.
|
|
262
|
+
|
|
263
|
+
The four interaction states (`rowHover`, `focus`, `selectedBg`, `disabledBg`)
|
|
264
|
+
are separate tokens because they must stay tellable apart — `focus` is a
|
|
265
|
+
**stroke** rather than a fill, since a focus ring has to be visible on a
|
|
266
|
+
hovered row _and_ a selected one. Live demo on the `w3d-theme` page.
|
|
267
|
+
|
|
268
|
+
- **20 more icons**, for the ensemble editor: `mousePointer`, `refreshCcw`/`Cw`,
|
|
269
|
+
`move`, `copy`, `delete`, `trash`/`trash2`, `plus`/`plusCircle`,
|
|
270
|
+
`rotateCw`/`Ccw`, and the complete `corner*` family (8). 61 total.
|
|
271
|
+
- **`fieldGroup.attach(target?)`** wires real `keydown` events to the focused
|
|
272
|
+
field and returns a detacher. Opt-in — the library still never grabs the
|
|
273
|
+
document by itself — but without it every flat host writes the same six lines,
|
|
274
|
+
and a field you can click into that then refuses every character is a bad
|
|
275
|
+
first impression. It was one: the theme demo shipped with an unusable field.
|
|
276
|
+
|
|
277
|
+
It maps the event's modifier flags explicitly rather than passing the event,
|
|
278
|
+
so `keyIntent` keeps taking a plain shape and stays testable without a DOM.
|
|
279
|
+
|
|
280
|
+
- **⚠️ `fieldGroup` moved under `ui.*`** (`ui.fieldGroup`), alongside
|
|
281
|
+
`ui.inputField` and `ui.keyboard` — it is part of the same SVG UI surface and
|
|
282
|
+
was inconsistently top-level. Unreleased, so nothing external moves.
|
|
283
|
+
- **`fieldGroup` — one keyboard, many fields.** Three chores that always travel
|
|
284
|
+
together and were hand-rolled in every host (tosijs-3d#37, items 1 and 7):
|
|
285
|
+
**exclusivity** (focusing one un-focuses the rest — two lit fields both
|
|
286
|
+
claiming the keyboard is worse than none), **commit on leave** (so a
|
|
287
|
+
half-typed `1.` never survives the move), and **layout** (the incoming field's
|
|
288
|
+
`type` picks the keyboard mode, which is the point of having a type).
|
|
289
|
+
|
|
290
|
+
`handleKey(key, mods)` takes a key NAME rather than an event, so the same
|
|
291
|
+
routing serves a DOM listener, a synthetic source or a test — and it returns
|
|
292
|
+
whether the key was consumed, so a host knows when to `preventDefault`.
|
|
293
|
+
Attaching a real listener stays the host's choice; this library never grabs
|
|
294
|
+
the document.
|
|
295
|
+
|
|
296
|
+
- **`keyIntent`** — pure DOM-key → field-intent mapping. Named keys (`Tab`,
|
|
297
|
+
`Escape`, `F5`) and anything with Ctrl/Cmd/Alt are **not consumed**: swallowing
|
|
298
|
+
a shortcut because a field happens to have focus is worse than handling no
|
|
299
|
+
keys at all.
|
|
300
|
+
- **`inputField.onFocus` is settable on the object** (mirroring `onChange`), so
|
|
301
|
+
a manager learns about focus it did not initiate. Without it a tap and a
|
|
302
|
+
programmatic focus disagree about who is active, and keys go to the wrong
|
|
303
|
+
field — silently, and only sometimes.
|
|
304
|
+
- **`inputField` has a `type`** — `'text' | 'number' | 'integer' | 'email' |
|
|
305
|
+
'url' | 'tel'`, HTML's `inputmode` idea. **One property, three jobs**: the
|
|
306
|
+
host raises the matching keyboard layout on focus
|
|
307
|
+
(`field.keyboardMode` → `keyboard.setMode`), `commit()` normalises or refuses
|
|
308
|
+
the value, and `isValid()` answers without the host knowing the field's kind.
|
|
309
|
+
This is why there is no separate `numberField` — a number field is this one,
|
|
310
|
+
configured (tosijs-3d#37, item 2).
|
|
311
|
+
|
|
312
|
+
The layouts already existed (`numpad`, `dial`, `email`, `url`) and nothing
|
|
313
|
+
chose between them, so focusing a numeric field raised `alpha` and left you to
|
|
314
|
+
find the numpad — two deliberate taps per field, and worse in a headset where
|
|
315
|
+
there is no physical keyboard to fall back on.
|
|
316
|
+
|
|
317
|
+
**Validity while typing and validity as an answer are different questions**,
|
|
318
|
+
and conflating them is what makes typed fields either unusable or liars: `-`
|
|
319
|
+
and `1.` are legitimate things to have typed and illegitimate things to have
|
|
320
|
+
meant. So `isValid()` accepts them and `commit()` does not — and a refused
|
|
321
|
+
commit restores the last good value rather than writing `NaN` into the
|
|
322
|
+
document. Enter commits _before_ `onEnter` fires, so no handler has to re-do
|
|
323
|
+
the parse.
|
|
324
|
+
|
|
325
|
+
- **`slider3d` can show its value permanently** — `showValue: 'peek' | 'always'
|
|
326
|
+
| 'never'` (default `'peek'`, the existing behaviour) plus a `format` hook for
|
|
327
|
+
units. ensemble's coordinates were unreadable because a handle position is not
|
|
328
|
+
a number (tosijs-3d#37, item 3).
|
|
329
|
+
|
|
330
|
+
`'always'` reserves width for the **widest** value in the range (measured
|
|
331
|
+
through `format`, so units and precision count) and shortens the track to
|
|
332
|
+
clear it. Sizing to the _current_ value would make the track twitch mid-drag,
|
|
333
|
+
which reads as the slider fighting you.
|
|
334
|
+
|
|
335
|
+
- **`row3d` — lay widgets side by side.** A panel only stacks, so a
|
|
336
|
+
label-and-field pair cost two rows and eight fields became sixteen rows of
|
|
337
|
+
mostly whitespace (tosijs-3d#37, item 5). `weights` give the usual label/field
|
|
338
|
+
split; children are middle-aligned by default, since top-aligning a short
|
|
339
|
+
label beside a taller control makes it look detached from what it names.
|
|
340
|
+
|
|
341
|
+
Pointer routing is **by column**, delegating in each child's own coordinates —
|
|
342
|
+
a widget cannot know it has been put in a row, so it must still receive
|
|
343
|
+
`(0,0)` at its own top-left. Hit-testing follows the same path, which keeps
|
|
344
|
+
"grab between the controls to scroll" working inside a row.
|
|
345
|
+
|
|
346
|
+
- **`panel3d` sizes itself: `height: 'fit'` is the new default**, with
|
|
347
|
+
`maxHeight` to cap it (past the cap it scrolls rather than growing — fitting
|
|
348
|
+
and scrolling are the same mechanism seen from either side of a limit).
|
|
349
|
+
- **`panel.measure()` → `{content, viewport, overflow, fits}`.** The panel has
|
|
350
|
+
always known this (`stackLayout` returns the total) and simply never said, so
|
|
351
|
+
every consumer sizing a panel was guessing at a number the panel could have
|
|
352
|
+
told them.
|
|
353
|
+
|
|
354
|
+
Both exist because **clipping is silent**: a panel too short for its content
|
|
355
|
+
looks exactly like a panel missing its last control. The ensemble editor got
|
|
356
|
+
three heights wrong in one sitting — a command hidden behind another panel, an
|
|
357
|
+
option cut in half, a list showing five of eight rows — and noticed none of
|
|
358
|
+
them at the time (tosijs-3d#37, item 6).
|
|
359
|
+
|
|
360
|
+
The ordering is the whole trick: widgets measure against the inner WIDTH,
|
|
361
|
+
which is known from `width` alone, so the stack can be measured before the
|
|
362
|
+
panel has a height and the height derived from it. The previous code fixed the
|
|
363
|
+
height first and had no way to discover it was wrong.
|
|
364
|
+
|
|
365
|
+
**Behaviour change:** a `panel3d` with no explicit `height` used to be 480 and
|
|
366
|
+
now fits its content. All nine call sites in this repo pass a height, so
|
|
367
|
+
nothing here changed; an adopter relying on the old default should pass
|
|
368
|
+
`height: 480`.
|
|
369
|
+
|
|
370
|
+
## 0.7.3
|
|
371
|
+
|
|
372
|
+
### Added
|
|
373
|
+
|
|
374
|
+
- **⚠️ The biped uses the GTA V control layout.** Left stick moves and
|
|
375
|
+
**strafes**; right stick **turns the body** and pitches. It was tank-controlled
|
|
376
|
+
(left stick X turned), which nobody has muscle memory for any more and which
|
|
377
|
+
left the right stick doing camera work that could not steer. Turning the body
|
|
378
|
+
with the right stick makes **swim direction and body facing one thing** rather
|
|
379
|
+
than two that can disagree — the thing that made look-directed swimming fiddly.
|
|
380
|
+
The camera now sits straight behind the body and has no yaw of its own, so it
|
|
381
|
+
cannot end up pointing somewhere the character is not. Measured: strafe moves
|
|
382
|
+
4.65 m sideways with the yaw unchanged; turn rotates 34° with zero translation.
|
|
383
|
+
- **The biped's right stick is a LOOK control**, and jump and sneak exist.
|
|
384
|
+
Tonio spotted that look-directed swimming had nothing to aim with on a flat
|
|
385
|
+
screen: the right stick was bound to `cameraZoom` (Y) and a snap-back peek (X),
|
|
386
|
+
so a character had no aim at all. Now `lookX`/`lookY`, **persistent rather than
|
|
387
|
+
sprung** — a character's camera is how you look _around_, where the aircraft's
|
|
388
|
+
springs back because there it is a glance off the flight path. A `FollowCamera`
|
|
389
|
+
has no pitch of its own, so pitch is height: raise the camera and it looks down
|
|
390
|
+
at its locked target. Zoom moves to the d-pad, which sneak vacated.
|
|
391
|
+
- **Swim aim now reads that look** rather than integrating the stick
|
|
392
|
+
separately, so "swim where you are looking" is literally true flat and is the
|
|
393
|
+
same rule the headset already followed with your head.
|
|
394
|
+
- **Jump** — right bumper only. `A` briefly aliased it, and Tonio removed the
|
|
395
|
+
alias: the face buttons are **reserved for actions** (they are primary and
|
|
396
|
+
secondary fire on the aircraft), and a control vocabulary that changes
|
|
397
|
+
meaning per vehicle is one you have to relearn. The ground snap swallowed the
|
|
398
|
+
first version whole — the impulse lifted the body ~7 cm, the probe still saw
|
|
399
|
+
ground 0.6 m below, and the snap put it straight back, a jump that rose
|
|
400
|
+
exactly 0.00 m. The snap now yields while you are rising.
|
|
401
|
+
- **Standing jumps brace; running jumps fire on the press.** A running jump's
|
|
402
|
+
anticipation _is the run_ — the character is already loaded and moving, so a
|
|
403
|
+
wind-up can only read as a stumble. A standing jump has no run-up to borrow
|
|
404
|
+
from, so holding scrubs the clip into the braced pose and **parks there**,
|
|
405
|
+
and releasing launches. The hold is not a delay tax: it **scales the jump**
|
|
406
|
+
(`jumpMinScale` 0.45 → full), so it is a choice rather than a wait. Measured:
|
|
407
|
+
full brace 1.13 m, short hold 0.45 m, nothing at all while held.
|
|
408
|
+
- **The animation is retimed to the jump, not the jump to the animation.**
|
|
409
|
+
Physics stays fixed at `jumpSpeed`, and the clip's `speedRatio` is set so it
|
|
410
|
+
lasts exactly the flight time. Measured: `running-jump` plays at **1.02×**
|
|
411
|
+
(0.93 s clip over 0.92 s of flight). The first attempt had this backwards —
|
|
412
|
+
sizing the launch from the clip length — which made the jump a consequence of
|
|
413
|
+
whatever the animator exported.
|
|
414
|
+
- The jump clip holds for the **whole airborne period**, and the clip is chosen
|
|
415
|
+
at the press and never re-picked, so a standing jump cannot switch to the
|
|
416
|
+
running clip in mid-air.
|
|
417
|
+
- **Sneak** — left bumper, a **toggle on land and a held control in water**: a
|
|
418
|
+
stance you adopt for a while versus a thing you do continuously, and a toggle
|
|
419
|
+
whose state you must remember with your head underwater is worse than useless.
|
|
420
|
+
- **⚠️ Jumping is instantaneous again, and animated in three phases.** The
|
|
421
|
+
crouch-on-press/launch-on-release model was an adaptation to the stock rig's
|
|
422
|
+
single one-shot clip that happened to open with a crouch. Quaternius ships
|
|
423
|
+
`Jump_Start` / `Jump_Loop` / `Jump_Land`, where **start is a takeoff, not a
|
|
424
|
+
wind-up** — Tonio: _"it's basically designed for instantaneous jumps where
|
|
425
|
+
once you jump you enter the jump state and that's it."_ That is also the
|
|
426
|
+
platform-jumper contract in `MOBILITY-DESIGN.md`: the character does exactly
|
|
427
|
+
what you pressed, now; anticipation belongs to the intent model, where the
|
|
428
|
+
character decides before you ask. `jumpWindup` and `jumpMinScale` are gone, and
|
|
429
|
+
so is the `speedRatio` retiming — each clip now plays for as long as it is
|
|
430
|
+
actually true instead of being stretched to fit a flight it could not know.
|
|
431
|
+
- **`b3d-library` surfaces the glb's own catalogue** (#45). `getManifest()`
|
|
432
|
+
returns `{count, categories, items:[{name, category, tags, size}]}` built from
|
|
433
|
+
the loaded nodes; `getInfo(name)` answers `{category, tags, clips}` **without
|
|
434
|
+
instantiating**, which is what a clip picker needs (`metadata.animationGroups`
|
|
435
|
+
requires an instance to exist first). `getNames()` now narrows to the declared
|
|
436
|
+
items when a catalogue is present — the data-driven twin of the `.model`
|
|
437
|
+
convention, and a packed kit badly over-reports without it (measured: 559 raw
|
|
438
|
+
nodes vs 131 declared items). `getRootNames()`/`getHierarchy()` still expose
|
|
439
|
+
everything, so sub-part targets keep working, and `instantiate()` carries the
|
|
440
|
+
extras onto what it returns — including the `canonical: true` wrapper, which
|
|
441
|
+
was the actual reason the data looked unreachable.
|
|
442
|
+
- **`glb-manifest`** — the pure half: category/tag/size queries over a library
|
|
443
|
+
catalogue, plus GLB JSON-chunk parsing for the case where you want a
|
|
444
|
+
library's contents _without_ loading it into a scene. Note Babylon surfaces
|
|
445
|
+
per-**node** extras and drops per-**scene** ones, so `manifestFromNodes` is
|
|
446
|
+
the path that needs no parsing at all.
|
|
447
|
+
- **Mantling — the biped climbs onto a ledge too high to step onto.** No button:
|
|
448
|
+
push into it and the character solves it, which is the intent model rather
|
|
449
|
+
than the platform jumper. `mantle.ts` is the pure half (Babylon-free, 13
|
|
450
|
+
tests): `canMantle` (every clause a way of NOT being a ledge — too low is a
|
|
451
|
+
step, too high a wall, no landing is a fence you would be stranded on),
|
|
452
|
+
`mantlePath` (an ARC — rise leads translation, because a straight line drives
|
|
453
|
+
the body through the lip) and `mantleClip` (picks `ClimbUp_1m`/`_2m` by
|
|
454
|
+
MEASURING the ledge, since Quaternius indexes those clips by height).
|
|
455
|
+
|
|
456
|
+
It arrived as _"when you swim to the water's edge, you just pop instantly to
|
|
457
|
+
the surface onto the land"_, but it is not a water feature — climbing out of a
|
|
458
|
+
pond is mantling a lip of height h, so the bank, the low wall and the crate
|
|
459
|
+
are one verb. A swimmer's reach is measured from the **waterline**, not the
|
|
460
|
+
dangling feet: your hands are at the surface, and the feet reading made every
|
|
461
|
+
bank in the demo 3 m tall.
|
|
462
|
+
|
|
463
|
+
New attribute `climbReach` (2.2 m). The probe is throttled to 12 Hz — see
|
|
464
|
+
MOBILITY-DESIGN → "Probes: one budgeted read of the surroundings".
|
|
465
|
+
|
|
466
|
+
- **The wade↔swim transition is verified** — it had been the one untestable
|
|
467
|
+
case, because no water in the 0.2–0.9 m band existed anywhere in the demo.
|
|
468
|
+
With the canal's variable depth it now does: walking deep → shelf → deep
|
|
469
|
+
across a HARD step (4.24 m → 0.37 m → 4.56 m, the worst case for flicker)
|
|
470
|
+
produces exactly two state changes over 304 samples, one at each edge.
|
|
471
|
+
- **`wadeDepth` is a biped attribute** (`0.45` = fraction of standing height,
|
|
472
|
+
~0.8 m on a 1.83 m rig). Below it you wade; above it buoyancy takes over.
|
|
473
|
+
Tonio set the band — _"about 0.4-0.5 (it's hard to swim in water less than
|
|
474
|
+
waist deep)"_ — which is the real constraint: swimming shallower than that
|
|
475
|
+
is not a choice a person gets to make, the bottom is in the way. A fraction
|
|
476
|
+
rather than metres because it is a fact about the **body**, so a smaller
|
|
477
|
+
character starts swimming sooner in the same pond for free.
|
|
478
|
+
- **Camera zoom-IN works.** `cameraZoom` was `Math.max(0, dpadUp - dpadDown)`,
|
|
479
|
+
which discarded the whole zoom-in half — down produced `0`, not `−1`, so the
|
|
480
|
+
camera could only retreat. The unit test asserted the broken value, having
|
|
481
|
+
been written beside the broken line.
|
|
482
|
+
- **Vertical look moves the chase rig in XR.** Flat, `lookY` tilts the
|
|
483
|
+
`FollowCamera` via `heightOffset`; in a headset there is no `FollowCamera`, so
|
|
484
|
+
it did nothing. A regression from moving camera zoom onto the D-pad — XR
|
|
485
|
+
controllers have no D-pad, so the headset lost both the zoom and the tilt in
|
|
486
|
+
one move. Same formula as flat, clamped, so the two feel alike.
|
|
487
|
+
|
|
488
|
+
**Known gap:** camera zoom is now unreachable in VR. It needs a home that
|
|
489
|
+
exists on an XR controller — most likely a slider on the in-headset scene
|
|
490
|
+
panel.
|
|
491
|
+
|
|
492
|
+
- **Strafing is a toggle, `strafing="off"` by default.** Both sticks then turn
|
|
493
|
+
you — the left while moving or not, the right without moving — and the
|
|
494
|
+
sidestep clips never play. Off on principle: sidestepping is a shooter idiom
|
|
495
|
+
that reads oddly on a character meant to move like a person. That it also
|
|
496
|
+
avoids the weakest clips in the Quaternius set is a bonus, not the reason.
|
|
497
|
+
- **The pose is averaged over the cycle, not sampled once** — the fix for
|
|
498
|
+
_"I seem to porpoise out of the water"_. A swim cycle is not a fixed shape:
|
|
499
|
+
over `Swim_Fwd_Loop`'s 1.33 s the body's lowest point swings from −1.26 to
|
|
500
|
+
−0.28 as the legs kick. A single sample landed wherever the settle timer fell,
|
|
501
|
+
and the shallow end of that swing drove the derived buoyancy to its clamp and
|
|
502
|
+
fired the swimmer out of the water — intermittently, because it depended on
|
|
503
|
+
the phase.
|
|
504
|
+
- **`buoyancy` is METRES, not a multiplier** (`0` default = waterline at the
|
|
505
|
+
head; `0.1` floats ten centimetres higher). A multiplier is not authorable —
|
|
506
|
+
its effect depends on how tall the pose happens to be, so the same number
|
|
507
|
+
means different things for a tread and a crawl. Tonio: _"we can keep buoyancy
|
|
508
|
+
as a strict z offset for a given figure in water."_
|
|
509
|
+
- **The waterline is anchored at the swimmer's HEAD.** Which is what swimming
|
|
510
|
+
_is_ — you keep your head at the surface, and you do it by swimming rather
|
|
511
|
+
than by floating — so it is a fact about the activity, not about a clip, and
|
|
512
|
+
it holds for any humanoid rig without per-animation tuning. Treading now sits
|
|
513
|
+
with the water just above the neck and the head clear; a front crawl breaks
|
|
514
|
+
the surface. Both from one rule.
|
|
515
|
+
- **Swimmers float where the ANIMATION says, not where a constant says.** The
|
|
516
|
+
swim clips are authored with the **root at the waterline** — measured on the
|
|
517
|
+
Quaternius rig, `Swim_Idle_Loop` spans −1.37…+0.50 about the root and
|
|
518
|
+
`Swim_Fwd_Loop` −0.60…+0.31, so floating the root on the surface gives a tread
|
|
519
|
+
with head and shoulders out and a crawl with the head just breaking. The
|
|
520
|
+
equilibrium is now READ from the pose rather than tuned, so any standard
|
|
521
|
+
animation set floats correctly untouched.
|
|
522
|
+
|
|
523
|
+
The root itself is **not** a usable anchor: it means different things in
|
|
524
|
+
different clips — feet when standing, roughly waterline when swimming — and
|
|
525
|
+
taking it literally floated this rig at armpit height, since its root sits 73%
|
|
526
|
+
up the treading pose. It is still what tells us a pose _is_ a swim pose (the
|
|
527
|
+
body hangs below it), just not where the water goes.
|
|
528
|
+
|
|
529
|
+
Three bugs fell out of the old assumption that the root is at the feet, which
|
|
530
|
+
is true only while standing: the real head sat ~1.2 m under while treading;
|
|
531
|
+
the legs were buried in the seabed (the floor clamped the root, not the body's
|
|
532
|
+
lowest point); and `buoyancy` appeared to do nothing, because clearing that
|
|
533
|
+
head needed a submersion of 0.14 — `buoyancy ≈ 7`.
|
|
534
|
+
|
|
535
|
+
- **`buoyancy` is a biped attribute** — now a **trim** on the animation's
|
|
536
|
+
waterline (`1` = as authored, higher rides higher), because the absolute is
|
|
537
|
+
not ours to choose: the clip already encodes it, and a number here would be a
|
|
538
|
+
second opinion that goes stale with the next animation set.
|
|
539
|
+
- **A clip change no longer briefly makes a swimmer stand.** A newly-started
|
|
540
|
+
clip cannot be measured until its pose settles, and falling back to the
|
|
541
|
+
standing assumption for those frames put the root back at the feet — so a
|
|
542
|
+
swimmer floating at the waterline read as barely submerged and walked off
|
|
543
|
+
across it. Moving is exactly what changes the clip, which is why it showed as
|
|
544
|
+
_"when you move he tends to jump up to the surface and walk"_. An unmeasured
|
|
545
|
+
clip now inherits the last swim pose, `height / -bottom` is clamped so a
|
|
546
|
+
mid-blend reading cannot launch anyone, and **no floor under you means you
|
|
547
|
+
cannot stand** — full stop, rather than a number derived from where the body
|
|
548
|
+
drifted to.
|
|
549
|
+
- **The swim/stand test no longer feeds back on itself.** It asks how deep the
|
|
550
|
+
water is **at your feet on the floor**, which is stable across the switch —
|
|
551
|
+
the previous form measured the current pose, so raising buoyancy raised the
|
|
552
|
+
body, flipped the pose upright, and the upright reading locked the flip in:
|
|
553
|
+
at `buoyancy` 1.3 the character corked out and stood on the surface.
|
|
554
|
+
- **Neutral buoyancy starts at 1.5 m of head depth, not 0.5 m.** Holding depth
|
|
555
|
+
is for DIVING; at half a metre a swimmer whose head dipped barely under went
|
|
556
|
+
neutral and stayed there, with the up-thrust throttled near the surface too —
|
|
557
|
+
Tonio: _"still treading water with head underwater and it's hard to swim up."_
|
|
558
|
+
- **Sidestepping uses the lateral clips** rather than the forward walk — the
|
|
559
|
+
slide is gone. UAL ships a full eight-way set (Fwd, Fwd_L/R, Left, Right, Bwd,
|
|
560
|
+
Bwd_L/R) for jog, crouch **and** crawl; the biped now picks the lateral one
|
|
561
|
+
when sideways motion dominates, crouched or not, and falls back to walking on
|
|
562
|
+
a rig that has no such clip. Sneaking at rest holds `Crouch_Idle_Loop`.
|
|
563
|
+
- **⚠️ The right stick's Y is inverted by default** (`invertLookY: 'off'` to
|
|
564
|
+
restore) — pushing away tips the view down, the way a camera head works.
|
|
565
|
+
- **The follow camera can no longer go underground.** Pitch drives its HEIGHT,
|
|
566
|
+
so looking up walked it downward until the world vanished; `cameraMinHeight`
|
|
567
|
+
(0.5 m) floors it.
|
|
568
|
+
- **`ualAnimationStates()`** — the clip-name map for Quaternius UAL rigs, so
|
|
569
|
+
adopting that library is one line rather than twelve hand-written states. It
|
|
570
|
+
retires two fakes: `walkBackwards` becomes a real `Jog_Bwd_Loop` instead of
|
|
571
|
+
the walk cycle played in reverse, and `sneak` gets a `Crouch_Idle_Loop` that
|
|
572
|
+
holds at rest. `Jump_Start` / `Jump_Loop` / `Jump_Land` are mapped separately,
|
|
573
|
+
which is what will let the jump's brace genuinely hold rather than scrubbing a
|
|
574
|
+
one-shot — addressable now, not yet wired.
|
|
575
|
+
- **⚠️ The `tosi-b3d` demo uses a 1.83 m rig** from the CDN
|
|
576
|
+
(`quaternius/UAL1_core.glb`) instead of `omnidude.glb`, which measured
|
|
577
|
+
**0.88 m** — half human scale. **Camera framing roughly doubled** to match
|
|
578
|
+
(`cameraHeightOffset`, `cameraTargetHeight`, follow distances): that is the one
|
|
579
|
+
part of the tuning genuinely scale-bound, because it is in metres. Everything
|
|
580
|
+
else — run speed, step height, jump, the collision ellipsoid — was already
|
|
581
|
+
human-scale and is now _correct_ rather than needing adjustment. A project
|
|
582
|
+
using its own small rig will want the camera numbers back down.
|
|
583
|
+
- **The biped swims.** Water was already a medium rather than a boundary line
|
|
584
|
+
(underwater fog with a continuous crossing); the biped now treats it as one
|
|
585
|
+
too. Tonio: _"can we change the rate at which the biped falls in water. And
|
|
586
|
+
while we are at it the biped could learn to tread water and swim."_ Both
|
|
587
|
+
animations were already in the standard set, so this is one equation plus
|
|
588
|
+
wiring.
|
|
589
|
+
- **`buoyancy.ts`** — pure, tested. A body is slightly less dense than water,
|
|
590
|
+
so it is pushed up in proportion to how much of it is submerged and rests
|
|
591
|
+
where that balances its weight. Everything readable falls out of that one
|
|
592
|
+
equation instead of being special-cased: plunge-and-bob when you drop in, a
|
|
593
|
+
head that ends up **above** the surface (equilibrium is partial submersion —
|
|
594
|
+
nothing targets a head height), and wading that does nothing until the water
|
|
595
|
+
is deep enough to lift you. Sinking is ~1.5 m/s against ~20 m/s in air, and
|
|
596
|
+
drag blends by submersion rather than switching at the waterline, so
|
|
597
|
+
crossing the surface does not read as bouncing off it.
|
|
598
|
+
- **Swimming is deep enough AND not resting on the floor** — _resting on_,
|
|
599
|
+
not _within reach of_. The first version asked "is there ground below me?"
|
|
600
|
+
and left the character standing on the seabed under six metres of water.
|
|
601
|
+
The floor stops you sinking; it does not hold you down.
|
|
602
|
+
- `swim` while moving, `tread-water` while holding station.
|
|
603
|
+
- **Fixed after report: swimming broke when you pitched and turned at once.**
|
|
604
|
+
The body's yaw was read back out of the _pitched_ matrix, and
|
|
605
|
+
`atan2(forward.x, forward.z)` is ill-conditioned there — at 70° the
|
|
606
|
+
horizontal part is scaled by `cos 70° = 0.34`, so x and z collapse toward
|
|
607
|
+
zero, the recovered yaw gets noisy, and writing it straight back compounds
|
|
608
|
+
the noise into a body that wanders. The yaw is now **captured once from the
|
|
609
|
+
level matrix** and integrated from the turn input thereafter, so the only
|
|
610
|
+
reading happens while well conditioned. It hid because pitching _and_
|
|
611
|
+
turning together is one stick on a controller and two hands on a keyboard.
|
|
612
|
+
- Also fixed: the **mouse wheel** still fed `rightStickY`, which had become
|
|
613
|
+
`lookY` — so a scroll tipped the swimmer, since the swim aim is the look
|
|
614
|
+
pitch. It feeds the zoom axis now, so "the wheel zooms" survives remapping.
|
|
615
|
+
- **Look-directed swimming** (`swim-aim.ts`, pure + tested): the BODY pitches,
|
|
616
|
+
and the stroke follows for free — the biped already swims along its own
|
|
617
|
+
forward vector, so there is no separate vertical term and no way for aim and
|
|
618
|
+
motion to disagree. Aim comes from your **head** in a headset, and from the
|
|
619
|
+
right stick flat, because the biped's `FollowCamera` has a fixed pitch and
|
|
620
|
+
there is nothing to read. The stick **integrates** rather than mapping to an
|
|
621
|
+
absolute angle, so releasing it holds the descent instead of springing back
|
|
622
|
+
to level. Leaving the water unwinds the pitch on its own.
|
|
623
|
+
- **You can dive.** `sneak` down, `jump` up — crouch-to-descend matches the
|
|
624
|
+
GTA-V control vocabulary this project follows and leaves the triggers free.
|
|
625
|
+
Thrust competes with buoyancy rather than replacing it, so letting go hands
|
|
626
|
+
the vertical back to physics instead of pinning you.
|
|
627
|
+
- **Released, you HOLD depth and drift up slowly** rather than corking to the
|
|
628
|
+
surface (Tonio's call). Once your head is properly under, buoyancy blends
|
|
629
|
+
toward neutral — but stays just above 1, so you surface if you stop paying
|
|
630
|
+
attention. You also glide a little deeper after letting go: that is
|
|
631
|
+
momentum, and letting go is not a brake. Measured live in 12 m of water:
|
|
632
|
+
dive to the seabed, release, drift up at **0.2 m/s**; hold `jump` and it
|
|
633
|
+
becomes **0.65 m/s**.
|
|
634
|
+
- Verified live: released on a seabed under 6 m of water the biped rose and
|
|
635
|
+
settled at **0.866** submersion against a predicted 0.870, and a walk
|
|
636
|
+
downhill waded in and began swimming on its own.
|
|
637
|
+
|
|
638
|
+
### Changed
|
|
639
|
+
|
|
640
|
+
- **Toolchain bumped** — `tosijs` 1.7.8 → 1.8.0, `tosijs-ui` 1.9.8 → 1.12.3,
|
|
641
|
+
`haltija` 1.11.2 → 1.12.5, and **`tjs-lang` 0.13.6 added explicitly**: it is a
|
|
642
|
+
peer of tosijs-ui and was **not installed here at all**, which nothing warned
|
|
643
|
+
about. The site built anyway, so "works" and "works properly" were
|
|
644
|
+
indistinguishable — installing it changed the build output (`tjs-lang bundles
|
|
645
|
+
served same-origin at /tjs/` is new).
|
|
646
|
+
|
|
647
|
+
`peerDependencies` is deliberately unchanged: `tosijs ^1.7.8` already admits
|
|
648
|
+
1.8.0, so developing against the newer one costs consumers nothing and this
|
|
649
|
+
stays a **patch**.
|
|
650
|
+
|
|
651
|
+
Fallout, all fixed: an unused variable in `wreck-fall` failed lint, which
|
|
652
|
+
`bun start` runs first — so the dev server would not start, and because
|
|
653
|
+
`bun format` is `eslint && prettier`, **prettier had been silently skipped**
|
|
654
|
+
on every file since. Also: `bun run typecheck` is **green again** (the
|
|
655
|
+
standing `model-transform.test.ts:449` red is gone).
|
|
656
|
+
|
|
657
|
+
- **`.haltija.json` pins agent commands to this project's origin.** A shared
|
|
658
|
+
haltija server holds tabs from several repos and commands followed browser
|
|
659
|
+
FOCUS, so a command run from this directory could silently drive another
|
|
660
|
+
project's page — which happened repeatedly. New in haltija 1.12.
|
|
661
|
+
|
|
662
|
+
### Fixed
|
|
663
|
+
|
|
664
|
+
- **Entering water flipped your heading 180°.** With a mirrored glTF root
|
|
665
|
+
(`scaling.z = −1`) world forward is `−(R·ẑ)`, so reading yaw back gives
|
|
666
|
+
`θ + π` and writing it produces `−F`. I had reasoned this round-tripped; it
|
|
667
|
+
does not. On land nothing rewrites the quaternion, so it only showed the
|
|
668
|
+
instant pitch engaged — which is the instant you start swimming.
|
|
669
|
+
- **You could wade on top of the water.** Holding surface-thrust lifted you
|
|
670
|
+
until submersion fell under the swim threshold, at which point you were
|
|
671
|
+
classed as standing and got a walk cycle while still afloat. Up-thrust is now
|
|
672
|
+
gated by head depth, exactly as the upward aim is: you cannot push yourself
|
|
673
|
+
out of water, and surfacing is buoyancy's job anyway.
|
|
674
|
+
- **The swim/stand switch twitched at the waterline.** Walking down a ramp,
|
|
675
|
+
submersion hovers around the threshold and the mode flickered. `isSwimming`
|
|
676
|
+
now enters at 0.5 and holds until 0.35.
|
|
677
|
+
|
|
678
|
+
- **`setAnimationState` could pick the wrong clip.** The lookup was
|
|
679
|
+
`group.name.endsWith(animation)`, and a suffix match is ambiguous the moment
|
|
680
|
+
one clip's name ends with another's. Asking a Quaternius rig for `Idle_Loop`
|
|
681
|
+
played **`Crouch_Idle_Loop`** — but it was already latent in the stock set,
|
|
682
|
+
where `running-jump` ends with `jump`, so `setAnimationState('jump')` could
|
|
683
|
+
match the running jump depending on array order. Now an exact name match
|
|
684
|
+
(after stripping Babylon's `Clone of ` prefix), with the suffix behaviour kept
|
|
685
|
+
only as a fallback for rigs whose exporter adds some other prefix.
|
|
686
|
+
|
|
687
|
+
- **A `_collideCylinder` child of a `_collideMesh` parent built a cylinder around
|
|
688
|
+
the PARENT's whole subtree.** Reported as _"I put a collideMesh on the hull and
|
|
689
|
+
collideCylinders on each mast but the ship just seems to have a single giant
|
|
690
|
+
squat cylinder"_ — and the effect was a **34.4 m wide, 26 m tall** invisible
|
|
691
|
+
barrier you could not walk within twenty metres of. Two independent faults,
|
|
692
|
+
both in how a collider's bounds are chosen:
|
|
693
|
+
|
|
694
|
+
- The root-walk climbed while the parent had **any** collide annotation, so
|
|
695
|
+
each mast climbed onto the hull. The shape came from the leaf and the bounds
|
|
696
|
+
from the root, so it built the mast's _cylinder_ around the hull's _subtree_
|
|
697
|
+
— and one `processed` entry then swallowed the other two masts, which is why
|
|
698
|
+
there was exactly one. It now climbs only across the **same** annotation.
|
|
699
|
+
(The tell was the generated mesh's name: `Hull_collideMesh_collider` should
|
|
700
|
+
be impossible, because `_collideMesh` never builds a primitive.)
|
|
701
|
+
- Bounds combined the annotated node's children even when the node had its own
|
|
702
|
+
geometry. A mast is a Mesh 0.55 × 20.6 × 0.64 carrying the Crow's Nest, Flag,
|
|
703
|
+
Spars and SquareSails, so combining gave a 10.2 m cylinder around each mast
|
|
704
|
+
— the deck still walled off. Children are combined only when the annotated
|
|
705
|
+
node has no geometry of its own, which is the GLB TransformNode case the
|
|
706
|
+
behaviour was written for.
|
|
707
|
+
|
|
708
|
+
Ship colliders now measure **0.58–0.64 m across** at mast height, and the hull
|
|
709
|
+
keeps its mesh collider.
|
|
710
|
+
|
|
711
|
+
- **Ambient particles spawned on the wrong side of the water** — leaves below it,
|
|
712
|
+
bubbles above, and a lot of them. Reported while swimming, which is what made
|
|
713
|
+
it constant: third person parks the camera right at the surface. Intensity was
|
|
714
|
+
never the problem; it already ramps correctly on the camera's depth. The
|
|
715
|
+
**spawn box** was: a cube of side 2·radius centred on the eye, so with the eye
|
|
716
|
+
near the surface half of it sits in the wrong medium. Intensity says how much
|
|
717
|
+
to emit and cannot say where, so no amount of ramping could have fixed it. The
|
|
718
|
+
box is now clipped at the waterline — measured with the camera on the surface,
|
|
719
|
+
`leaves/above` spawns from −0.20 up and both `underwater` presets from −0.20
|
|
720
|
+
down, against a surface at −0.20. Same lesson as the biped's plane-vs-volume
|
|
721
|
+
submersion test, one layer along: a medium with a boundary needs a volume with
|
|
722
|
+
one too.
|
|
723
|
+
|
|
724
|
+
- **World-placed dialogs rendered MIRRORED** — the death/respawn panel read
|
|
725
|
+
"NWOD", and so did the pause panel in any scene using `placement="world"`.
|
|
726
|
+
Reported from a headset, but not headset-specific: it was mirrored flat too,
|
|
727
|
+
and had been since 0.7.2. A Babylon plane's visible face is local **−Z**, so
|
|
728
|
+
aiming `atan2(dx, dz)` at the eye — which points local **+Z** at it — shows you
|
|
729
|
+
the panel's **back**, and a `doubleSided` back reuses the front's UVs rather
|
|
730
|
+
than vanishing. Hence a mirror instead of a missing dialog, which is why it
|
|
731
|
+
survived a release. Now `dialog-placement.facingYawDeg`, tested on the property
|
|
732
|
+
that matters: turn the plane's visible face by this yaw and it points at the
|
|
733
|
+
eye. The same change fixes touch, which was mirrored with it — every
|
|
734
|
+
right-aligned control on a world dialog was mapping to the left.
|
|
735
|
+
|
|
736
|
+
- **The pause/continue panel is DOM on a flat screen**, and stays an in-scene
|
|
737
|
+
plane in VR — the scene panel's rule (one widget list, two presentations)
|
|
738
|
+
applied to the modal that most needed it. Tonio: _"in flat 3d the continue
|
|
739
|
+
should be presented in the dom like the scene panel."_ On a monitor the DOM
|
|
740
|
+
overlay is better on every axis that has bitten this panel: it cannot be
|
|
741
|
+
occluded, cannot be put somewhere odd by a line-of-sight cast, needs no raycast
|
|
742
|
+
to be clicked, and is exactly where you are already looking. All that machinery
|
|
743
|
+
exists because a headset has no DOM, which is the only place it earns its cost.
|
|
744
|
+
The same `panel3d` SVG serves both — its widgets' listeners work natively in
|
|
745
|
+
the DOM, and `handlePointer` serves the in-scene path.
|
|
746
|
+
- **Death and world-placed dialogs now survive a floating-origin rebase.**
|
|
747
|
+
Neither had opted in, so in a terrain scene — the only kind big enough to
|
|
748
|
+
rebase, which is why it hid — a shift mid-death slid the spectate camera, the
|
|
749
|
+
fire emitters and the dialog away from the crash while the world moved out
|
|
750
|
+
from under them. That reads exactly like the origin-teleport fixed above ("the
|
|
751
|
+
wreck is way off, I am looking at nothing"), and two causes producing one
|
|
752
|
+
description is how a fixed bug looks unfixed. Both use an origin LISTENER
|
|
753
|
+
rather than `registerWorldRoot`, because both hold world coordinates in JS and
|
|
754
|
+
the element owns the transform (shifting the node would be undone next render).
|
|
755
|
+
The falling wreck needed nothing — it re-reads its position from the node every
|
|
756
|
+
frame, so a shift is absorbed.
|
|
757
|
+
- **Leaving VR now pauses in EVERY scene**, not only those with
|
|
758
|
+
`enterXrOnResume`. Taking the headset off is a departure, not a view change.
|
|
759
|
+
The gate existed on the reasoning that a flat scene which merely visited XR
|
|
760
|
+
shouldn't acquire a pause panel on the way out; superseded by the symmetry,
|
|
761
|
+
which is the stronger argument (Tonio: _"just as entering VR should unpause,
|
|
762
|
+
exiting VR should probably pause"_) — one gesture means resume, its inverse
|
|
763
|
+
means stop, and a rule you opt into is not a pair. `enterXrOnResume` still owns
|
|
764
|
+
resume → enter.
|
|
765
|
+
- **The biped got stuck on slopes** — the cost of the fix below, found and fixed
|
|
766
|
+
in the same session. Snapping the feet onto the surface put the collision
|
|
767
|
+
ellipsoid's bottom exactly ON it, so moving into rising ground embedded it and
|
|
768
|
+
Babylon refused the move. The old 15 cm float had been acting as the clearance.
|
|
769
|
+
The body now starts a step above the feet (`STEP_OFFSET`, which is Unity's Step
|
|
770
|
+
Offset): anything lower than a step is walked over, the probe stands you on it
|
|
771
|
+
afterwards, and a wall is still a wall.
|
|
772
|
+
- **The biped floated above or sank into slopes, and never corrected.** Tonio,
|
|
773
|
+
on a long-standing one: _"you often end up a little offset from the ground …
|
|
774
|
+
this never really corrects. It just gets randomly messed up again when you
|
|
775
|
+
navigate another slope."_ Not random — the ground check was a **dead band with
|
|
776
|
+
no corrective term**: it probed 0.15 m down from just above the feet and, if
|
|
777
|
+
it found anything, did nothing. So the biped fell until the probe happened to
|
|
778
|
+
see ground and then stopped wherever in that 0.15 m window it landed, and any
|
|
779
|
+
error inside the band was permanent.
|
|
780
|
+
|
|
781
|
+
Slopes made it worse both ways. Going up, `moveWithCollisions` slides the body
|
|
782
|
+
up the ellipsoid and leaves it high in the band. Going down — "especially
|
|
783
|
+
down" — gravity moved at most `min(0.1, 9.81·dt)`, a hard **0.1 m per frame**
|
|
784
|
+
clamp, so a brisk descent outran it and it floated the whole way.
|
|
785
|
+
|
|
786
|
+
Now it probes a step up and a step down and puts the feet **on** the surface,
|
|
787
|
+
and a real fall accumulates velocity instead of moving a fixed amount per
|
|
788
|
+
frame (with the probe extended by the frame's fall so it cannot step over the
|
|
789
|
+
ground between frames). Measured on the b3d demo scene: **390 consecutive
|
|
790
|
+
frames walking a slope at offset 0.00000**, where before the error wandered
|
|
791
|
+
freely inside a 15 cm band. The probe also goes through `collidable()` now, so
|
|
792
|
+
a UI panel is no longer something you can stand on.
|
|
793
|
+
|
|
794
|
+
- **Dying in VR teleported you to the world origin.** Tonio: _"I collided with
|
|
795
|
+
wreckage high up and respawned at the origin or starting point with the wrecked
|
|
796
|
+
plane hanging in mid-air off in the distance."_ He was not moved away from the
|
|
797
|
+
wreck — the wreck stayed exactly where he died (measured: a corpse drifts 0.05 m
|
|
798
|
+
in 10 s); **he** was moved. `releaseFocus()` nulls the focused entity, so the
|
|
799
|
+
next XR frame finds nothing piloted and falls through to free locomotion, which
|
|
800
|
+
did `rig.parent = null`. That KEEPS THE LOCAL POSE and reinterprets it as world,
|
|
801
|
+
so a rig at local `(0, 2, −5)` behind its parent lands at `(0, 2, −5)` in the
|
|
802
|
+
world. Now `rig.setParent(null)`, which preserves the world transform.
|
|
803
|
+
|
|
804
|
+
It had been latent since 0.7.0 as the cockpit-only oddity in `TODO.md`
|
|
805
|
+
("the aircraft was moved way away from me"); parenting the chase rig — the
|
|
806
|
+
jitter fix above — made it reachable from the view people actually fly in,
|
|
807
|
+
which is how a five-month-old note finally got diagnosed. Both spellings are
|
|
808
|
+
now pinned in `babylon-orientation.test.ts`, because they look interchangeable
|
|
809
|
+
and the wrong one is shorter.
|
|
810
|
+
|
|
811
|
+
- **`B3dControllable.halt()`** — tell an entity it is dead, rather than relying on
|
|
812
|
+
`releaseFocus()` nulling its input provider and `_update` short-circuiting on
|
|
813
|
+
that. `b3d-death` calls it. Belt and braces for the focus-managed case; the only
|
|
814
|
+
thing that stops a `die()` handed an entity the focus manager never held.
|
|
815
|
+
- **A wreck hung in the air where it died.** Tonio, from a headset: _"I collided
|
|
816
|
+
with wreckage high up … the wrecked plane hanging in mid-air (it should really
|
|
817
|
+
tumble to the ground)."_ Hanging wreckage is worse than untidy — it is a solid
|
|
818
|
+
object in the sky, so the debris of one death becomes the cause of the next.
|
|
819
|
+
A wreck now falls: a tumbling ballistic descent on the velocity it died with,
|
|
820
|
+
one bad bounce, a skid, and a rest. The fires and the spectate camera go down
|
|
821
|
+
with it (a smoke column left at the kill point with the wreck 100 m below it
|
|
822
|
+
is worse than no fire at all). Rules are pure and tested in **`wreck-fall`**;
|
|
823
|
+
the spin is DERIVED from the velocity, never random, so the same crash looks
|
|
824
|
+
the same twice.
|
|
825
|
+
- **How you died decides how far it goes.** Flying into something is an
|
|
826
|
+
inelastic collision that eats most of the energy (`carry: 0.25`); being shot
|
|
827
|
+
down leaves you with nearly all of it (`0.7`). At full carry a 90 m/s crash
|
|
828
|
+
from 130 m travelled ~450 m before landing — a glide, not a crash, and it
|
|
829
|
+
dragged the spectate camera across that much terrain. Tonio: _"The plane
|
|
830
|
+
went flying off into the distance … pretty funny but not as expected."_
|
|
831
|
+
- Drag settled at `0.005` (≈44 m/s terminal). `0.02` was ten times too much (a
|
|
832
|
+
wreck shed its speed in half a second); `0.002` is the figure for a
|
|
833
|
+
STREAMLINED body, and a broken airframe tumbles broadside. Each was
|
|
834
|
+
plausible in isolation; only a real crash told them apart.
|
|
835
|
+
- New seam **`B3dControllable.getWorldVelocity()`**. `b3d-aircraft`'s own
|
|
836
|
+
`velocity` field reads ZERO in wing-borne flight — the fly-by-wire path
|
|
837
|
+
moves the node directly — so a wreck launched from it dropped straight down
|
|
838
|
+
out of a 60 m/s dive.
|
|
839
|
+
- A wreck with **nothing under it** (the edge of a finite ground, a kill over
|
|
840
|
+
open water) is abandoned after 1500 m rather than falling forever with the
|
|
841
|
+
camera chasing it down. Found by killing outside a 600 m ground plane: it
|
|
842
|
+
reached y = −25 and kept going.
|
|
843
|
+
- **The VR chase camera jittered and drifted with speed** — Tonio, in a headset:
|
|
844
|
+
_"throttle up the aircraft gets further away, throttle down it gets closer",_
|
|
845
|
+
and it jitters climbing, diving and turning. Three faults, all in the XR rig
|
|
846
|
+
and none in the flat one (which measures rigid to 0.2 mm):
|
|
847
|
+
|
|
848
|
+
- It was **not parented**. The XR rig runs in `onXRFrameObservable`, which
|
|
849
|
+
fires BEFORE `scene.render()`, while an entity moves in
|
|
850
|
+
`registerBeforeRender`, which fires inside it — so the rig was positioned
|
|
851
|
+
from last frame's aircraft position, every frame, and a variable frame time
|
|
852
|
+
turns that fixed lag into jitter.
|
|
853
|
+
- It **eased toward a world-space target**, and a first-order tracker sits
|
|
854
|
+
`v/k` behind — ~6.8 m at 61 m/s, proportional to speed. Hence the throttle
|
|
855
|
+
symptom.
|
|
856
|
+
- The easing used `lerp(a, b, k*dt)`, which is **not frame-rate independent**
|
|
857
|
+
(`1 - exp(-k*dt)` is).
|
|
858
|
+
|
|
859
|
+
The rig is now a CHILD of a level position+heading anchor the entity updates in
|
|
860
|
+
the same tick it moves, at a fixed local offset, with no easing at all — rigid
|
|
861
|
+
by construction rather than by good timing. New seam:
|
|
862
|
+
**`B3dControllable.getChaseAnchor()`**, `null` by default; `b3d-aircraft`
|
|
863
|
+
provides one. Entities without an anchor keep the eased path, now with the
|
|
864
|
+
correct exponential form.
|
|
865
|
+
|
|
866
|
+
- **All three panel sites now aim themselves the same way**, through
|
|
867
|
+
`dialog-placement.faceViewer`. `frame-panel` and the XR settings panel used to
|
|
868
|
+
face you with their BACK and cancel the resulting mirror with
|
|
869
|
+
`tex.uScale = -1` — the settings panel also flipping `1 - uv.x` on every pick,
|
|
870
|
+
two compensations for one avoidable cause. Both are gone. No visible change
|
|
871
|
+
(they were correct on screen and under the finger); it removes the trap that
|
|
872
|
+
produced the world-dialog bug above, since the knowledge lived in a comment in
|
|
873
|
+
two files and comments do not travel.
|
|
874
|
+
- **`rollDeg` keeps its meaning**: turning a panel around reverses how a roll
|
|
875
|
+
reads, so `faceViewer` negates it. Worth stating because reasoning said the
|
|
876
|
+
flips cancelled and they do not — the only roll in the library is `180°`,
|
|
877
|
+
which is symmetric and agrees with the wrong answer.
|
|
878
|
+
|
|
879
|
+
### Added
|
|
880
|
+
|
|
881
|
+
- **`<tosi-b3d-interactive>` — a mesh you can touch** (#36), the substrate doors,
|
|
882
|
+
knobs, switches, levers, consoles and lamps all stand on. `b3d-button` is a
|
|
883
|
+
floating Babylon GUI widget; this is the other thing — world geometry you reach
|
|
884
|
+
out to. Requested by tosijs-3d-ensemble, whose framing was fair and lands:
|
|
885
|
+
_tosijs-3d has almost nothing for building a PLACE, as opposed to a battle._
|
|
886
|
+
Point at a mesh (or a named sub-mesh — the knob, not the door), reach it, use
|
|
887
|
+
it; `hover` / `unhover` / `activate` / `refused` arrive as bubbling events or
|
|
888
|
+
`whenX` callbacks.
|
|
889
|
+
- **One implementation, flat and immersive.** Babylon routes XR controller rays
|
|
890
|
+
through `scene.onPointerObservable`, the same observable a mouse feeds, so
|
|
891
|
+
there is no XR branch and no second path to keep in step.
|
|
892
|
+
- **Composition, not a god-feature.** `vetoes` is the seam ensemble asked for:
|
|
893
|
+
a `lockable` pushes one, `interactive` never learns what a lock is, and the
|
|
894
|
+
refusal NAMES the refuser — the difference between a locked door and a broken
|
|
895
|
+
one. Vetoes run at activation, not at hover, so a locked door still highlights
|
|
896
|
+
and still reports being tried; silence is a bug report.
|
|
897
|
+
- **It never touches the transform.** An element that manages a node owns its
|
|
898
|
+
transform (the rule #35 was a violation of), so this reads the scene and never
|
|
899
|
+
writes to it. A door that opens moves its ELEMENT.
|
|
900
|
+
- **`interaction.ts`** — the pure, Babylon-free rules (`interactStep`,
|
|
901
|
+
`activationVeto`, `withinReach`), unit tested, so what counts as "you used it"
|
|
902
|
+
can be argued about without a scene. Activation is press-then-**release** on the
|
|
903
|
+
thing, because a press you drag off and release elsewhere is how anyone recovers
|
|
904
|
+
from touching the wrong thing — and aim wanders more in a headset, not less.
|
|
905
|
+
- **`InteractiveBehavior`** — the attachable form, for anything that already owns
|
|
906
|
+
a mesh (a loader, a biped, a vehicle), plus `nearestInteractive` / `useNearest`
|
|
907
|
+
for the "walk up and press E" control that wires to `ControlInput.interact`.
|
|
908
|
+
|
|
9
909
|
## 0.7.2
|
|
10
910
|
|
|
11
911
|
### Fixed
|