@solidrt/3d 0.0.56 → 0.0.58
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/AGENTS.md +1052 -639
- package/README.md +2 -1
- package/examples/README.md +19 -8
- package/examples/collision.tsx +17 -4
- package/examples/first-person.tsx +20 -10
- package/examples/fleet.tsx +11 -5
- package/examples/model.tsx +3 -3
- package/examples/pick.tsx +144 -34
- package/examples/probe.tsx +10 -4
- package/examples/scene-views.tsx +49 -58
- package/examples/sky-lit.tsx +10 -4
- package/examples/skybox.tsx +10 -4
- package/examples/standard.tsx +10 -4
- package/examples/wireframe.tsx +107 -0
- package/package.json +2 -2
- package/src/collision.ts +14 -193
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/first-person.ts +140 -188
- package/src/geometry-gpu.ts +45 -13
- package/src/geometry.ts +186 -5
- package/src/gltf.ts +97 -16
- package/src/index.ts +15 -10
- package/src/input.ts +79 -0
- package/src/material.ts +26 -8
- package/src/mesh.ts +5 -6
- package/src/model.ts +65 -33
- package/src/node.ts +100 -26
- package/src/orbit.ts +266 -235
- package/src/scene-pointer.ts +229 -101
- package/src/scene.ts +256 -79
- package/src/skeleton.ts +211 -0
- package/src/components.tsx +0 -1101
package/src/material.ts
CHANGED
|
@@ -41,7 +41,7 @@ import type {
|
|
|
41
41
|
Topology,
|
|
42
42
|
VertexAttribute,
|
|
43
43
|
} from "@solidrt/core/gpu"
|
|
44
|
-
import { layoutAttributes, layoutKey, layoutSlot } from "./geometry.ts"
|
|
44
|
+
import { FORMAT_FLOATS, layoutAttributes, layoutKey, layoutSlot } from "./geometry.ts"
|
|
45
45
|
import type { VertexLayout } from "./geometry.ts"
|
|
46
46
|
import { linearColor } from "./color.ts"
|
|
47
47
|
import {
|
|
@@ -63,9 +63,10 @@ import {
|
|
|
63
63
|
} from "./glsl.ts"
|
|
64
64
|
|
|
65
65
|
export type Material = {
|
|
66
|
-
/** The pipeline this material draws with for geometry of `layout`
|
|
67
|
-
* (lazily created, one per
|
|
68
|
-
|
|
66
|
+
/** The pipeline this material draws with for geometry of `layout` and
|
|
67
|
+
* `topology` (lazily created, one per pair met): the geometry says what
|
|
68
|
+
* its indices are, the material draws them that way. */
|
|
69
|
+
pipeline(layout: VertexLayout | undefined, topology: Topology): RenderPipelineId
|
|
69
70
|
/** Per-entry uniform values this material contributes at addDraw. */
|
|
70
71
|
params: ShaderParams
|
|
71
72
|
/** Per-entry sampler bindings, when the material samples textures. */
|
|
@@ -1072,7 +1073,6 @@ export type ShaderMaterialClassOptions = {
|
|
|
1072
1073
|
depthWrite?: boolean
|
|
1073
1074
|
blend?: BlendMode
|
|
1074
1075
|
cull?: CullMode
|
|
1075
|
-
topology?: Topology
|
|
1076
1076
|
label?: string
|
|
1077
1077
|
}
|
|
1078
1078
|
|
|
@@ -1142,6 +1142,24 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
1142
1142
|
let transparent = opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none")
|
|
1143
1143
|
let depth = opts.depth ?? true
|
|
1144
1144
|
let cull = opts.cull ?? "back"
|
|
1145
|
+
// Formats are the vertex vocabulary (f32/vec2/vec3/vec4), not WebGPU's
|
|
1146
|
+
// spelling, and an unknown one has no float count: it would leave here
|
|
1147
|
+
// intact, make a NaN record stride at the mesh, and fail as an arithmetic
|
|
1148
|
+
// complaint from createRecordMesh naming neither the attribute nor the
|
|
1149
|
+
// format. Checked where the name is written instead.
|
|
1150
|
+
for (let attr of opts.instanceAttributes ?? []) {
|
|
1151
|
+
if (!(attr.format in FORMAT_FLOATS)) {
|
|
1152
|
+
throw new Error(
|
|
1153
|
+
"shaderMaterial unknown instance attribute format '" +
|
|
1154
|
+
String(attr.format) +
|
|
1155
|
+
"' for " +
|
|
1156
|
+
attr.name +
|
|
1157
|
+
" (expected " +
|
|
1158
|
+
Object.keys(FORMAT_FLOATS).join(", ") +
|
|
1159
|
+
")",
|
|
1160
|
+
)
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1145
1163
|
// An empty list declares nothing - same as absent (the engine requires an
|
|
1146
1164
|
// instance buffer exactly when attributes are declared).
|
|
1147
1165
|
let instanceAttributes = opts.instanceAttributes?.length ? opts.instanceAttributes.map(a => ({ ...a })) : undefined
|
|
@@ -1192,8 +1210,8 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
1192
1210
|
// record buffer, declared on the pipeline beside the layout).
|
|
1193
1211
|
let attributes = (): VertexAttribute[] =>
|
|
1194
1212
|
programAttributes(programFor()).filter(a => !instanceAttributes?.some(i => i.name === a.name))
|
|
1195
|
-
let pipelineFor = (layout: VertexLayout | undefined): RenderPipelineId => {
|
|
1196
|
-
let key = layoutKey(layout)
|
|
1213
|
+
let pipelineFor = (layout: VertexLayout | undefined, topology: Topology): RenderPipelineId => {
|
|
1214
|
+
let key = layoutKey(layout) + "|" + topology
|
|
1197
1215
|
let pipeline = pipelines.get(key)
|
|
1198
1216
|
if (pipeline === undefined) {
|
|
1199
1217
|
pipeline = createRenderPipeline(programFor(), {
|
|
@@ -1205,7 +1223,7 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
1205
1223
|
depthWrite: opts.depthWrite ?? (transparent && depth ? false : undefined),
|
|
1206
1224
|
blend: opts.blend ?? (transparent ? "alpha" : undefined),
|
|
1207
1225
|
cull,
|
|
1208
|
-
topology
|
|
1226
|
+
topology,
|
|
1209
1227
|
label: opts.label,
|
|
1210
1228
|
})
|
|
1211
1229
|
pipelines.set(key, pipeline)
|
package/src/mesh.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// scene side is reached through the node's SceneHooks (node.ts).
|
|
5
5
|
|
|
6
6
|
import { createBuffer, destroyBuffer, writeBuffer } from "@solidrt/core/gpu"
|
|
7
|
-
import type { BufferId, DrawId, InstanceAttribute, ShaderParams, TextureBindings
|
|
8
|
-
import { geometryBounds, plane } from "./geometry.ts"
|
|
7
|
+
import type { BufferId, DrawId, InstanceAttribute, ShaderParams, TextureBindings } from "@solidrt/core/gpu"
|
|
8
|
+
import { FORMAT_FLOATS, geometryBounds, plane } from "./geometry.ts"
|
|
9
9
|
import type { Geometry } from "./geometry.ts"
|
|
10
10
|
import type { GeometryBuffers } from "./geometry-gpu.ts"
|
|
11
11
|
import type { Material } from "./material.ts"
|
|
@@ -24,7 +24,8 @@ export type Mesh = SceneNode & {
|
|
|
24
24
|
renderOrder: number
|
|
25
25
|
/** Draw into the scene's shadow map (default false, Three's default).
|
|
26
26
|
* Set with setCastShadow. A casting instanced mesh is skipped (the
|
|
27
|
-
* depth pass cannot know its record layout)
|
|
27
|
+
* depth pass cannot know its record layout), and so is any geometry
|
|
28
|
+
* that is not a triangle list: lines and points cast nothing. */
|
|
28
29
|
castShadow: boolean
|
|
29
30
|
/** Layer membership bitmask (default 1, Three's `object.layers`): a
|
|
30
31
|
* target draws the mesh when its mask intersects this. Not inherited
|
|
@@ -225,13 +226,11 @@ export function localBounds(mesh: Mesh): Float32Array | null {
|
|
|
225
226
|
return mesh._sprite ? SPRITE_BOUNDS : geometryBounds(mesh.geometry)
|
|
226
227
|
}
|
|
227
228
|
|
|
228
|
-
const ATTRIBUTE_FLOATS: Record<VertexAttribute["format"], number> = { f32: 1, vec2: 2, vec3: 3, vec4: 4 }
|
|
229
|
-
|
|
230
229
|
/** Floats per record of one instance slot of an attribute list (slot 0
|
|
231
230
|
* by default; the list's `slot` keys pick the others). */
|
|
232
231
|
export function instanceStride(attributes: InstanceAttribute[], slot = 0): number {
|
|
233
232
|
let stride = 0
|
|
234
|
-
for (let a of attributes) if ((a.slot ?? 0) === slot) stride +=
|
|
233
|
+
for (let a of attributes) if ((a.slot ?? 0) === slot) stride += FORMAT_FLOATS[a.format]
|
|
235
234
|
return stride
|
|
236
235
|
}
|
|
237
236
|
|
package/src/model.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { add, createGroup, remove, setTransform } from "./node.ts"
|
|
|
24
24
|
import type { SceneNode } from "./node.ts"
|
|
25
25
|
import { createMesh } from "./mesh.ts"
|
|
26
26
|
import type { Mesh } from "./mesh.ts"
|
|
27
|
+
import { refreshJointBounds, unbindSkeleton } from "./skeleton.ts"
|
|
28
|
+
import type { WornPiece } from "./skeleton.ts"
|
|
27
29
|
|
|
28
30
|
/** Anisotropic filtering level for a model's textures: the engines' usual
|
|
29
31
|
* default (Godot ships 2x, Unity's quality presets 2-8x) - enough to keep a
|
|
@@ -80,8 +82,30 @@ export type Model = SceneNode & {
|
|
|
80
82
|
/** Local rest-pose [minX, minY, minZ, maxX, maxY, maxZ] over every part
|
|
81
83
|
* (conservative for parts under rotated nodes). */
|
|
82
84
|
bounds: Float32Array
|
|
83
|
-
/** Detach the model and free its geometry buffers and textures.
|
|
85
|
+
/** Detach the model and free its geometry buffers and textures. A
|
|
86
|
+
* worn piece (bindSkeleton) comes off its body; a body takes the
|
|
87
|
+
* pieces it wears with it. */
|
|
84
88
|
dispose(): void
|
|
89
|
+
/** @internal The skins the parts draw, in file order: the palette
|
|
90
|
+
* texture (shared by identical skins), the joint nodes in row order,
|
|
91
|
+
* their inverse binds and joint-space influence boxes. */
|
|
92
|
+
_skins: ModelSkinNodes[]
|
|
93
|
+
/** @internal The pieces bound onto this model's skeleton. */
|
|
94
|
+
_worn: WornPiece[]
|
|
95
|
+
/** @internal The body this model is bound onto, or null. */
|
|
96
|
+
_body: Model | null
|
|
97
|
+
/** @internal Subtrees bindSkeleton moved under the body's joints (no
|
|
98
|
+
* longer under this model); detached again by dispose. */
|
|
99
|
+
_grafts: SceneNode[]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @internal One skin of a built model: ModelSkin with its joints
|
|
103
|
+
* resolved to the model's nodes and its palette texture. */
|
|
104
|
+
export type ModelSkinNodes = {
|
|
105
|
+
texture: TextureId
|
|
106
|
+
joints: SceneNode[]
|
|
107
|
+
inverseBind: Float32Array
|
|
108
|
+
jointBounds: Float32Array
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
/**
|
|
@@ -162,6 +186,10 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
162
186
|
let materials = data.materials.map((_, i) => materialFor(i, false))
|
|
163
187
|
|
|
164
188
|
let model = createGroup() as Model
|
|
189
|
+
model._skins = []
|
|
190
|
+
model._worn = []
|
|
191
|
+
model._body = null
|
|
192
|
+
model._grafts = []
|
|
165
193
|
// The node table is pre-order, so every parent group exists before its
|
|
166
194
|
// children reference it.
|
|
167
195
|
let groups: SceneNode[] = data.nodes.map((n) => {
|
|
@@ -183,39 +211,38 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
183
211
|
// joints move - there is no JS palette walk, and posing joints with
|
|
184
212
|
// setTransform is always enough. Identical skins (same joints, same
|
|
185
213
|
// inverse binds - the body/legs and LOD splits exporters produce) share
|
|
186
|
-
// one texture, so their palette is computed and uploaded once.
|
|
187
|
-
|
|
214
|
+
// one texture, so their palette is computed and uploaded once. A worn
|
|
215
|
+
// piece's rows move onto another model's joints (bindSkeleton).
|
|
188
216
|
data.skins.forEach((skin, i) => {
|
|
189
|
-
let joints = skin.joints.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
other.joints.join(",") === joints &&
|
|
194
|
-
other.inverseBind.length === skin.inverseBind.length &&
|
|
195
|
-
other.inverseBind.every((v, k) => v === skin.inverseBind[k])
|
|
196
|
-
) {
|
|
197
|
-
skinTextures.push(skinTextures[j]!)
|
|
198
|
-
return
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
let texture = createMutableTexture(new Float32Array(skin.joints.length * 16), 4, skin.joints.length, {
|
|
202
|
-
format: "rgba32f",
|
|
203
|
-
autoFree: false,
|
|
204
|
-
label: label ? label + "-skin" + i : "skin" + i,
|
|
217
|
+
let joints = skin.joints.map((n) => {
|
|
218
|
+
let joint = groups[n]
|
|
219
|
+
if (joint === undefined) throw new Error("createModel: skin " + i + " names a missing node " + n)
|
|
220
|
+
return joint
|
|
205
221
|
})
|
|
222
|
+
let key = skin.joints.join(",")
|
|
223
|
+
let same = model._skins.find(
|
|
224
|
+
(other, j) =>
|
|
225
|
+
data.skins[j]!.joints.join(",") === key &&
|
|
226
|
+
other.inverseBind.length === skin.inverseBind.length &&
|
|
227
|
+
other.inverseBind.every((v, k) => v === skin.inverseBind[k]),
|
|
228
|
+
)
|
|
229
|
+
let texture =
|
|
230
|
+
same?.texture ??
|
|
231
|
+
createMutableTexture(new Float32Array(skin.joints.length * 16), 4, skin.joints.length, {
|
|
232
|
+
format: "rgba32f",
|
|
233
|
+
autoFree: false,
|
|
234
|
+
label: label ? label + "-skin" + i : "skin" + i,
|
|
235
|
+
})
|
|
236
|
+
model._skins.push({ texture, joints, inverseBind: skin.inverseBind, jointBounds: skin.jointBounds })
|
|
237
|
+
if (same !== undefined) return
|
|
206
238
|
textures.push(texture)
|
|
207
|
-
|
|
208
|
-
for (let j = 0; j < skin.joints.length; j++) {
|
|
209
|
-
let joint = groups[skin.joints[j]!]
|
|
210
|
-
if (joint === undefined) throw new Error("createModel: skin " + i + " names a missing node " + skin.joints[j])
|
|
239
|
+
joints.forEach((joint, j) => {
|
|
211
240
|
;(joint._palettes ??= []).push({ texture, row: j, post: skin.inverseBind.slice(j * 16, j * 16 + 16), anchor: model })
|
|
212
|
-
|
|
213
|
-
// flush carries through the pose. A joint influencing nothing (an
|
|
214
|
-
// inverted box) gets none and contributes nothing to the union.
|
|
215
|
-
let jb = skin.jointBounds.subarray(j * 6, j * 6 + 6)
|
|
216
|
-
if (jb[0]! <= jb[3]! && jb[1]! <= jb[4]! && jb[2]! <= jb[5]!) joint._cullBounds = Float32Array.from(jb)
|
|
217
|
-
}
|
|
241
|
+
})
|
|
218
242
|
})
|
|
243
|
+
// Each joint's influence box, in joint space: culling-only bounds the
|
|
244
|
+
// flush carries through the pose, united over every skin reaching it.
|
|
245
|
+
refreshJointBounds(model)
|
|
219
246
|
model.parts = data.parts.map((part) => {
|
|
220
247
|
let skinned = part.skin !== null
|
|
221
248
|
let material = materialFor(part.material, skinned)
|
|
@@ -225,11 +252,11 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
225
252
|
// matrices place them, so its mesh hangs off the model root (the
|
|
226
253
|
// spec ignores the node's transform for skinned meshes) and uModel
|
|
227
254
|
// stays the model's own placement.
|
|
228
|
-
let
|
|
229
|
-
if (
|
|
230
|
-
mesh._textures = { uBones: texture }
|
|
255
|
+
let skin = model._skins[part.skin!]
|
|
256
|
+
if (skin === undefined) throw new Error("createModel: part '" + part.name + "' names a missing skin " + part.skin)
|
|
257
|
+
mesh._textures = { uBones: skin.texture }
|
|
231
258
|
// Culled by its joints' boxes, so the box follows the animation.
|
|
232
|
-
mesh._cullJoints =
|
|
259
|
+
mesh._cullJoints = skin.joints.slice()
|
|
233
260
|
add(model, mesh)
|
|
234
261
|
} else {
|
|
235
262
|
let node = groups[part.node]
|
|
@@ -242,6 +269,11 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
242
269
|
model.clips = data.clips
|
|
243
270
|
model.bounds = data.bounds
|
|
244
271
|
model.dispose = () => {
|
|
272
|
+
// A worn piece comes off first (its rows sit on the body's joints);
|
|
273
|
+
// a body takes its pieces with it (their skins read joints that are
|
|
274
|
+
// about to go).
|
|
275
|
+
if (model._body !== null) unbindSkeleton(model)
|
|
276
|
+
for (let worn of model._worn.slice()) worn.piece.dispose()
|
|
245
277
|
if (model.parent !== null) remove(model)
|
|
246
278
|
for (let part of model.parts) disposeGeometry(part.mesh.geometry)
|
|
247
279
|
for (let id of textures) destroyTexture(id)
|
package/src/node.ts
CHANGED
|
@@ -10,12 +10,14 @@ import * as spatial from "flux:spatial"
|
|
|
10
10
|
import type { NodeId, NodeTransition } from "flux:spatial"
|
|
11
11
|
import { on } from "srt:events"
|
|
12
12
|
import type { ShaderParams, TextureId } from "@solidrt/core/gpu"
|
|
13
|
+
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
13
14
|
// The scene's lookAt() aims a node; math's builds a camera's view matrix -
|
|
14
15
|
// the same pairing (and the same name) as Three's Object3D/Matrix4.
|
|
15
16
|
import { compose, eulerFromQuat, identity, mat4, multiply, quat, quatFromFrame, transformPoint, updateRotation, updateScale } from "./math.ts"
|
|
16
17
|
import type { Mat4, Quat, TransformUpdate, Vec3, Vec4 } from "./math.ts"
|
|
17
18
|
import type { CastingLight, Light } from "./light.ts"
|
|
18
19
|
import type { InstancedMesh, InstanceNode, Mesh } from "./mesh.ts"
|
|
20
|
+
import type { Scene, ViewHandle } from "./scene.ts"
|
|
19
21
|
|
|
20
22
|
// lookAt()'s default roll reference. Read-only: quatFromFrame never
|
|
21
23
|
// writes its inputs, so one shared vector is safe.
|
|
@@ -107,15 +109,23 @@ export type SceneNode = {
|
|
|
107
109
|
visible: boolean
|
|
108
110
|
/** Pointer event handlers - plain fields, assign freely (they touch no
|
|
109
111
|
* GPU state, so they need no setTransform-style write path; components
|
|
110
|
-
* sync their props here). Down/move/up dispatch on the
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
* sync their props here). Down/move/up/wheel/tap dispatch on the
|
|
113
|
+
* nearest hit - the struck instance of an instanced mesh, else the mesh
|
|
114
|
+
* - bubble through its ancestors and end at the scene's listeners
|
|
115
|
+
* (stopPropagation stops the walk, and a stopped down claims the whole
|
|
116
|
+
* press); enter/leave fire on the struck node alone. Events flow once
|
|
117
|
+
* the element showing the scene carries `scene.handlers`. */
|
|
118
|
+
onPointerDown?: (event: NodePointerEvent) => void
|
|
119
|
+
onPointerMove?: (event: NodePointerEvent) => void
|
|
120
|
+
onPointerUp?: (event: NodePointerEvent) => void
|
|
121
|
+
onPointerEnter?: (event: NodePointerEvent) => void
|
|
122
|
+
onPointerLeave?: (event: NodePointerEvent) => void
|
|
123
|
+
/** The wheel over the node (NodeWheelEvent: `deltaX`/`deltaY`), bubbling
|
|
124
|
+
* like down/move/up. */
|
|
125
|
+
onWheel?: (event: NodeWheelEvent) => void
|
|
126
|
+
/** A press released on this node within the slop, alone for its whole
|
|
127
|
+
* press (NodeTapEvent: `tapCount`); bubbles like down/move/up. */
|
|
128
|
+
onTap?: (event: NodeTapEvent) => void
|
|
119
129
|
/** A declared transition (setTransition) settled naturally on one
|
|
120
130
|
* component; a cancel, snap or scene leave never fires. */
|
|
121
131
|
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
@@ -133,7 +143,8 @@ export type SceneNode = {
|
|
|
133
143
|
* skin): bound to the core at every scene enter, so the flush writes
|
|
134
144
|
* `inverse(anchorWorld) * world * post` - the model-local bone matrix -
|
|
135
145
|
* to the palette texture's row whenever the node moves. `anchor` is the
|
|
136
|
-
* model root
|
|
146
|
+
* model root - the body's, for a piece bound onto its skeleton - and
|
|
147
|
+
* null for non-joints (the common case pays one null check). */
|
|
137
148
|
_palettes: { texture: TextureId; row: number; post: Float32Array; anchor: SceneNode }[] | null
|
|
138
149
|
/** A culling-only local box (a model joint's influence region, in joint
|
|
139
150
|
* space): bound at every scene enter so the joint's world box follows
|
|
@@ -146,26 +157,19 @@ export type TransitionEndEvent = {
|
|
|
146
157
|
component: "position" | "rotation" | "scale"
|
|
147
158
|
}
|
|
148
159
|
|
|
149
|
-
/**
|
|
150
|
-
*
|
|
151
|
-
|
|
152
|
-
* `point`/`distance` are null exactly when the ray misses the dispatch
|
|
153
|
-
* target - which happens only during a captured drag or on a leave.
|
|
154
|
-
*/
|
|
155
|
-
export type ScenePointerEvent = {
|
|
156
|
-
/** The mesh the event is about (the hit, or the captured mesh during a
|
|
157
|
-
* drag) - constant while the event bubbles. */
|
|
158
|
-
mesh: Mesh
|
|
160
|
+
/** What every scene pointer event carries, whichever handler sees it:
|
|
161
|
+
* the element pointer vocabulary carried over, plus the 3D fields. */
|
|
162
|
+
export type SceneEventBase = {
|
|
159
163
|
/** The instance struck when `mesh` is an instanced mesh (the walk
|
|
160
164
|
* starts there), null otherwise - constant while the event bubbles. */
|
|
161
165
|
instance: InstanceNode | null
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
/** World-space hit point on `mesh`, or null when the ray misses it. */
|
|
166
|
+
/** World-space hit point on `mesh`, or null when there is no mesh or
|
|
167
|
+
* the ray misses it (a captured drag off the mesh, a leave). */
|
|
165
168
|
point: Vec3 | null
|
|
166
169
|
/** Camera-ray distance to `point` in world units, or null with it. */
|
|
167
170
|
distance: number | null
|
|
168
|
-
/** Pointer position in scene pixels - project()'s coordinate space
|
|
171
|
+
/** Pointer position in scene pixels - project()'s coordinate space
|
|
172
|
+
* (a view's own pixels under a view leaf). */
|
|
169
173
|
x: number
|
|
170
174
|
y: number
|
|
171
175
|
pointerId: number
|
|
@@ -175,10 +179,77 @@ export type ScenePointerEvent = {
|
|
|
175
179
|
ctrlKey: boolean
|
|
176
180
|
altKey: boolean
|
|
177
181
|
metaKey: boolean
|
|
178
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* The element event the scene's leaf received, in the leaf's own frame
|
|
184
|
+
* (localX/localY, clientX/clientY, movementX/Y): what core's
|
|
185
|
+
* recognizers consume, so a mesh drags itself through `createPan` and
|
|
186
|
+
* a camera control's feed rides the same events the walk carries.
|
|
187
|
+
*/
|
|
188
|
+
native: ElementPointerEvent
|
|
189
|
+
/**
|
|
190
|
+
* Stops the walk after the current handler: no ancestor and none of the
|
|
191
|
+
* scene's listeners see the event. Stopping a DOWN claims the whole
|
|
192
|
+
* press - that pointer's move, up and tap never reach the scene either,
|
|
193
|
+
* so a mesh that drags itself stops its down once and an orbit control
|
|
194
|
+
* fed at the root never turns under it.
|
|
195
|
+
*/
|
|
179
196
|
stopPropagation(): void
|
|
180
197
|
}
|
|
181
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The event a mesh, instance or ancestor group handler receives: `mesh`
|
|
201
|
+
* is the hit (or the captured mesh during a drag), constant while the
|
|
202
|
+
* event bubbles; `currentTarget` the node whose handler is running.
|
|
203
|
+
*/
|
|
204
|
+
export type NodePointerEvent = SceneEventBase & {
|
|
205
|
+
mesh: Mesh
|
|
206
|
+
currentTarget: SceneNode
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The event as the SCENE's listeners see it (scene.listen, the `<Scene>`
|
|
211
|
+
* pointer props; a view's under its own leaf), the last stop of the walk:
|
|
212
|
+
* `mesh` is the hit it bubbled from, or null over empty space, where the
|
|
213
|
+
* scene is the only target.
|
|
214
|
+
*/
|
|
215
|
+
export type ScenePointerEvent = SceneEventBase & {
|
|
216
|
+
mesh: Mesh | null
|
|
217
|
+
currentTarget: Scene | ViewHandle
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type WheelFields = {
|
|
221
|
+
/** The wheel delta as the element event reports it. */
|
|
222
|
+
deltaX: number
|
|
223
|
+
deltaY: number
|
|
224
|
+
native: ElementWheelEvent
|
|
225
|
+
}
|
|
226
|
+
type TapFields = {
|
|
227
|
+
/** 1 for a tap, 2 for the second of a double tap (the same target,
|
|
228
|
+
* within the repeat interval and distance), and so on - DOM's `detail`,
|
|
229
|
+
* Unity's `clickCount`. */
|
|
230
|
+
tapCount: number
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export type NodeWheelEvent = NodePointerEvent & WheelFields
|
|
234
|
+
export type SceneWheelEvent = ScenePointerEvent & WheelFields
|
|
235
|
+
/**
|
|
236
|
+
* A press that released on the target it pressed without travelling past
|
|
237
|
+
* the slop, the only pointer down for its whole press (a pinch never
|
|
238
|
+
* taps). Dispatched after the up, bubbling the same way; `x`/`y` and
|
|
239
|
+
* `point` are the release point.
|
|
240
|
+
*/
|
|
241
|
+
export type NodeTapEvent = NodePointerEvent & TapFields
|
|
242
|
+
export type SceneTapEvent = ScenePointerEvent & TapFields
|
|
243
|
+
|
|
244
|
+
/** A listener at the scene's (or a view's) root; see Scene.listen. */
|
|
245
|
+
export type ScenePointerListener = {
|
|
246
|
+
onPointerDown?: (event: ScenePointerEvent) => void
|
|
247
|
+
onPointerMove?: (event: ScenePointerEvent) => void
|
|
248
|
+
onPointerUp?: (event: ScenePointerEvent) => void
|
|
249
|
+
onWheel?: (event: SceneWheelEvent) => void
|
|
250
|
+
onTap?: (event: SceneTapEvent) => void
|
|
251
|
+
}
|
|
252
|
+
|
|
182
253
|
/** A bare node record (internal: the mesh and light constructors build on it). */
|
|
183
254
|
export function makeNode(kind: SceneNode["kind"]): SceneNode {
|
|
184
255
|
return {
|
|
@@ -245,7 +316,10 @@ export function enterScene(node: SceneNode, scene: SceneHooks): void {
|
|
|
245
316
|
}
|
|
246
317
|
}
|
|
247
318
|
if (node._cullBounds !== null) spatial.setCullBounds(node._node, node._cullBounds)
|
|
248
|
-
if (node.kind === "mesh")
|
|
319
|
+
if (node.kind === "mesh") {
|
|
320
|
+
spatial.setLayers(node._node, (node as Mesh).layers)
|
|
321
|
+
scene._attach(node as Mesh)
|
|
322
|
+
}
|
|
249
323
|
else if (node.kind === "light") scene._attachLight(node as Light)
|
|
250
324
|
else if (node.kind === "instance") scene._attachInstance(node as InstanceNode)
|
|
251
325
|
for (let c of node.children) enterScene(c, scene)
|