@solidrt/3d 0.0.48 → 0.0.49
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 +86 -4
- package/README.md +13 -2
- package/examples/README.md +8 -0
- package/examples/pick.tsx +98 -0
- package/examples/scene-background.tsx +43 -0
- package/package.json +3 -3
- package/src/bvh.ts +258 -0
- package/src/components.tsx +59 -5
- package/src/geometry.ts +29 -0
- package/src/index.ts +2 -2
- package/src/material.ts +37 -0
- package/src/math.ts +42 -0
- package/src/scene.ts +395 -6
package/src/components.tsx
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
setVisible,
|
|
22
22
|
} from "./scene.ts"
|
|
23
23
|
import type { ShaderParams } from "@solidrt/core/gpu"
|
|
24
|
-
import type { Mesh as MeshNode, Scene as SceneHandle, SceneNode } from "./scene.ts"
|
|
24
|
+
import type { Mesh as MeshNode, Scene as SceneHandle, SceneNode, ScenePointerEvent } from "./scene.ts"
|
|
25
25
|
import type { Geometry } from "./geometry.ts"
|
|
26
26
|
import type { Material } from "./material.ts"
|
|
27
27
|
import type { Quat, Vec3 } from "./math.ts"
|
|
@@ -48,7 +48,24 @@ export type TransformProps = {
|
|
|
48
48
|
visible?: boolean
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Mesh pointer events, the element vocabulary one tree deeper: the nearest
|
|
53
|
+
* hit mesh receives the event, down/move/up bubble to ancestor Groups
|
|
54
|
+
* (stopPropagation stops the walk), enter/leave pair on the mesh alone.
|
|
55
|
+
* Events flow while the element showing the scene carries scene.handlers -
|
|
56
|
+
* the built-in <Scene> leaf does (opt out with events={false}); an `output`
|
|
57
|
+
* leaf spreads them itself.
|
|
58
|
+
*/
|
|
59
|
+
export type PointerEventProps = {
|
|
60
|
+
onPointerDown?: (event: ScenePointerEvent) => void
|
|
61
|
+
onPointerMove?: (event: ScenePointerEvent) => void
|
|
62
|
+
onPointerUp?: (event: ScenePointerEvent) => void
|
|
63
|
+
/** Meshes only: a Group never receives enter/leave. */
|
|
64
|
+
onPointerEnter?: (event: ScenePointerEvent) => void
|
|
65
|
+
onPointerLeave?: (event: ScenePointerEvent) => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function syncNode(node: SceneNode, props: TransformProps & PointerEventProps): void {
|
|
52
69
|
createEffect(
|
|
53
70
|
() => [props.position, props.rotation, props.quaternion, props.scale, props.visible] as const,
|
|
54
71
|
([position, rotation, quaternion, scale, visible]) => {
|
|
@@ -56,6 +73,16 @@ function syncNode(node: SceneNode, props: TransformProps): void {
|
|
|
56
73
|
setVisible(node, visible !== false)
|
|
57
74
|
},
|
|
58
75
|
)
|
|
76
|
+
createEffect(
|
|
77
|
+
() => [props.onPointerDown, props.onPointerMove, props.onPointerUp, props.onPointerEnter, props.onPointerLeave] as const,
|
|
78
|
+
([down, move, up, enter, leave]) => {
|
|
79
|
+
node.onPointerDown = down
|
|
80
|
+
node.onPointerMove = move
|
|
81
|
+
node.onPointerUp = up
|
|
82
|
+
node.onPointerEnter = enter
|
|
83
|
+
node.onPointerLeave = leave
|
|
84
|
+
},
|
|
85
|
+
)
|
|
59
86
|
}
|
|
60
87
|
|
|
61
88
|
export type SceneProps = {
|
|
@@ -64,6 +91,12 @@ export type SceneProps = {
|
|
|
64
91
|
width: number
|
|
65
92
|
height: number
|
|
66
93
|
clearColor?: [number, number, number, number]
|
|
94
|
+
/** Fragment GLSL drawn behind the meshes, inside the scene's own pass
|
|
95
|
+
* (scene.setBackground): vUV/iResolution/fragColor contract, so a
|
|
96
|
+
* createShaderTexture backdrop ports verbatim. Reactive - swapping the
|
|
97
|
+
* source replaces the background; undefined removes it. Three's
|
|
98
|
+
* `scene.background = color` is `clearColor` here. */
|
|
99
|
+
background?: string
|
|
67
100
|
label?: string
|
|
68
101
|
ref?: (scene: SceneHandle) => void
|
|
69
102
|
/**
|
|
@@ -72,8 +105,16 @@ export type SceneProps = {
|
|
|
72
105
|
* leaf - a `<d-texture>`, a leaf carrying paint/pointer/layout props, or
|
|
73
106
|
* a post-effect chain (a shader target sampling the id; created in the
|
|
74
107
|
* callback it disposes with the Scene). Return null to render no leaf.
|
|
108
|
+
* Mesh pointer events then need the scene's handlers on your leaf:
|
|
109
|
+
* `<texture src={texture} {...useScene().scene.handlers} />`.
|
|
75
110
|
*/
|
|
76
111
|
output?: (texture: TextureId) => Element
|
|
112
|
+
/**
|
|
113
|
+
* Mesh pointer events (default on): the built-in leaf carries
|
|
114
|
+
* scene.handlers, so Mesh/Group onPointer* props receive events. `false`
|
|
115
|
+
* detaches them - the leaf then costs no pointer routing at all.
|
|
116
|
+
*/
|
|
117
|
+
events?: boolean
|
|
77
118
|
}
|
|
78
119
|
|
|
79
120
|
/**
|
|
@@ -91,14 +132,27 @@ export let Scene: ParentComponent<SceneProps> = props => {
|
|
|
91
132
|
() => [props.width, props.height] as const,
|
|
92
133
|
([w, h]) => scene.setSize(w, h),
|
|
93
134
|
)
|
|
135
|
+
createEffect(
|
|
136
|
+
() => props.background,
|
|
137
|
+
b => scene.setBackground(b ?? null),
|
|
138
|
+
)
|
|
94
139
|
untrack(() => props.ref)?.(scene)
|
|
95
140
|
let output = untrack(() => props.output)
|
|
141
|
+
let events = untrack(() => props.events) !== false
|
|
96
142
|
return (
|
|
97
143
|
<SceneContext value={{ scene, parent: scene.root }}>
|
|
98
144
|
{output ? (
|
|
99
145
|
untrack(() => output(scene.texture))
|
|
100
146
|
) : (
|
|
101
|
-
<texture
|
|
147
|
+
<texture
|
|
148
|
+
src={scene.texture}
|
|
149
|
+
width={props.width}
|
|
150
|
+
height={props.height}
|
|
151
|
+
onPointerDown={events ? scene.handlers.onPointerDown : undefined}
|
|
152
|
+
onPointerMove={events ? scene.handlers.onPointerMove : undefined}
|
|
153
|
+
onPointerUp={events ? scene.handlers.onPointerUp : undefined}
|
|
154
|
+
onPointerLeave={events ? scene.handlers.onPointerLeave : undefined}
|
|
155
|
+
/>
|
|
102
156
|
)}
|
|
103
157
|
{props.children}
|
|
104
158
|
</SceneContext>
|
|
@@ -106,7 +160,7 @@ export let Scene: ParentComponent<SceneProps> = props => {
|
|
|
106
160
|
}
|
|
107
161
|
|
|
108
162
|
/** A transform node: children inherit its position/rotation/scale. */
|
|
109
|
-
export let Group: ParentComponent<TransformProps & { ref?: (node: SceneNode) => void }> = props => {
|
|
163
|
+
export let Group: ParentComponent<TransformProps & PointerEventProps & { ref?: (node: SceneNode) => void }> = props => {
|
|
110
164
|
let ctx = useContext(SceneContext)
|
|
111
165
|
let node = createGroup()
|
|
112
166
|
add(ctx.parent, node)
|
|
@@ -116,7 +170,7 @@ export let Group: ParentComponent<TransformProps & { ref?: (node: SceneNode) =>
|
|
|
116
170
|
return <SceneContext value={{ scene: ctx.scene, parent: node }}>{props.children}</SceneContext>
|
|
117
171
|
}
|
|
118
172
|
|
|
119
|
-
export type MeshProps = TransformProps & {
|
|
173
|
+
export type MeshProps = TransformProps & PointerEventProps & {
|
|
120
174
|
geometry: Geometry
|
|
121
175
|
material: Material
|
|
122
176
|
/** Per-mesh uniforms for a custom material (setMeshParams as a prop).
|
package/src/geometry.ts
CHANGED
|
@@ -63,6 +63,35 @@ export type Geometry = {
|
|
|
63
63
|
label?: string
|
|
64
64
|
_buffer?: BufferId
|
|
65
65
|
_index?: BufferId
|
|
66
|
+
_bounds?: Float32Array
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The geometry's LOCAL axis-aligned bounds as [minX, minY, minZ, maxX,
|
|
71
|
+
* maxY, maxZ], computed from the vertices on first use and cached (like
|
|
72
|
+
* the GPU buffers, geometry is treated as immutable after creation).
|
|
73
|
+
* Picking's narrowphase volume; a flat geometry legitimately has zero
|
|
74
|
+
* extent on an axis. An empty geometry yields a zero box at the origin.
|
|
75
|
+
*/
|
|
76
|
+
export function geometryBounds(geometry: Geometry): Float32Array {
|
|
77
|
+
let bounds = geometry._bounds
|
|
78
|
+
if (bounds === undefined) {
|
|
79
|
+
bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity])
|
|
80
|
+
let v = geometry.vertices
|
|
81
|
+
let stride = geometry.layout === "colored" ? COLORED_FLOATS : FLOATS_PER_VERTEX
|
|
82
|
+
for (let i = 0; i + 2 < v.length; i += stride) {
|
|
83
|
+
let x = v[i]!, y = v[i + 1]!, z = v[i + 2]!
|
|
84
|
+
if (x < bounds[0]!) bounds[0] = x
|
|
85
|
+
if (y < bounds[1]!) bounds[1] = y
|
|
86
|
+
if (z < bounds[2]!) bounds[2] = z
|
|
87
|
+
if (x > bounds[3]!) bounds[3] = x
|
|
88
|
+
if (y > bounds[4]!) bounds[4] = y
|
|
89
|
+
if (z > bounds[5]!) bounds[5] = z
|
|
90
|
+
}
|
|
91
|
+
if (bounds[0]! > bounds[3]!) bounds.fill(0)
|
|
92
|
+
geometry._bounds = bounds
|
|
93
|
+
}
|
|
94
|
+
return bounds
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
/** The geometry's GPU buffers, created on first use and cached on it,
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// PerspectiveCamera) on top. See AGENTS.md for the model and the traps.
|
|
7
7
|
|
|
8
8
|
export { add, createGroup, createMesh, createScene, getRotation, lookAt, remove, setGeometry, setMaterial, setMeshParams, setTransform, setVisible, worldPosition } from "./scene.ts"
|
|
9
|
-
export type { CameraUpdate, Mesh as MeshNode, Scene as SceneHandle, SceneNode, SceneOptions, TransformUpdate } from "./scene.ts"
|
|
9
|
+
export type { CameraUpdate, Hit, Mesh as MeshNode, Scene as SceneHandle, SceneHandlers, SceneNode, SceneOptions, ScenePointerEvent, TransformUpdate } from "./scene.ts"
|
|
10
10
|
export { box, circle, cone, cylinder, disposeGeometry, fillColors, plane, ring, sphere, torus, torusKnot, withColors, FLOATS_PER_VERTEX, VERTEX_LAYOUTS } from "./geometry.ts"
|
|
11
11
|
export type { ColorFill, Geometry, VertexLayout } from "./geometry.ts"
|
|
12
12
|
export { fillet, roundRect, shape, triangulate } from "./profile.ts"
|
|
@@ -16,7 +16,7 @@ export type { PathFrames, PathPoint, SweepPath } from "./sweep.ts"
|
|
|
16
16
|
export { shaderMaterial, unlit } from "./material.ts"
|
|
17
17
|
export type { Material, ShaderMaterialOptions, UnlitOptions } from "./material.ts"
|
|
18
18
|
export { Group, Mesh, PerspectiveCamera, Scene, useScene } from "./components.tsx"
|
|
19
|
-
export type { MeshProps, PerspectiveCameraProps, SceneProps, TransformProps } from "./components.tsx"
|
|
19
|
+
export type { MeshProps, PerspectiveCameraProps, PointerEventProps, SceneProps, TransformProps } from "./components.tsx"
|
|
20
20
|
export { createOrbitCamera } from "./orbit.ts"
|
|
21
21
|
export type { OrbitCamera, OrbitCameraOptions, OrbitPose } from "./orbit.ts"
|
|
22
22
|
// math's lookAt (the camera view matrix) stays on the /math subpath: the
|
package/src/material.ts
CHANGED
|
@@ -144,6 +144,43 @@ function needsHeader(source: string): boolean {
|
|
|
144
144
|
return !source.trimStart().startsWith("#version")
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// The scene-background pass (scene.setBackground). The vertex stage is the
|
|
148
|
+
// engine's own attributeless fullscreen triangle (gl_VertexID, no vertex
|
|
149
|
+
// buffer), emitting the SAME vUV the shader-target contract provides: 0..1
|
|
150
|
+
// with origin at the displayed top-left - so a backdrop fragment written
|
|
151
|
+
// for createShaderTexture ports verbatim.
|
|
152
|
+
const BACKGROUND_VERTEX = glsl`
|
|
153
|
+
out vec2 vUV;
|
|
154
|
+
void main() {
|
|
155
|
+
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
156
|
+
vUV = p;
|
|
157
|
+
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
|
158
|
+
}
|
|
159
|
+
`
|
|
160
|
+
|
|
161
|
+
// Pipeline fragments get no vUV from the engine preamble (a pipeline's
|
|
162
|
+
// varyings are its own), so the background slot injects the full
|
|
163
|
+
// shader-target fragment contract itself: vUV, fragColor, iResolution.
|
|
164
|
+
const BACKGROUND_FRAGMENT_PREAMBLE =
|
|
165
|
+
"#version 300 es\nprecision highp float;\nin vec2 vUV;\nout vec4 fragColor;\nuniform vec2 iResolution;\n"
|
|
166
|
+
|
|
167
|
+
/** The scene's background pipeline (internal - reached via
|
|
168
|
+
* scene.setBackground): depth-free, attributeless, drawn as entry zero of
|
|
169
|
+
* the scene pass. */
|
|
170
|
+
export function backgroundPipeline(fragment: string, label: string): { pipeline: RenderPipelineId; program: ProgramId } {
|
|
171
|
+
let vs = compileShader("vertex", BACKGROUND_VERTEX, { header: true })
|
|
172
|
+
let fs = compileShader(
|
|
173
|
+
"fragment",
|
|
174
|
+
needsHeader(fragment) ? BACKGROUND_FRAGMENT_PREAMBLE + fragment : fragment,
|
|
175
|
+
{ header: false },
|
|
176
|
+
)
|
|
177
|
+
let program = linkProgram(vs, fs, { label })
|
|
178
|
+
destroyShader(vs)
|
|
179
|
+
destroyShader(fs)
|
|
180
|
+
let pipeline = createRenderPipeline(program, { label })
|
|
181
|
+
return { pipeline, program }
|
|
182
|
+
}
|
|
183
|
+
|
|
147
184
|
export type ShaderMaterialOptions = {
|
|
148
185
|
/**
|
|
149
186
|
* Vertex stage GLSL. MUST declare and use `uniform mat4 uModel` (the
|
package/src/math.ts
CHANGED
|
@@ -57,6 +57,48 @@ export function transformPoint(out: Vec4, m: Mat4, p: Vec3): Vec4 {
|
|
|
57
57
|
return out
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Transform a DIRECTION by m's upper 3x3 (w = 0: rotation and scale apply,
|
|
62
|
+
* translation does not). The ray-direction counterpart of transformPoint.
|
|
63
|
+
*/
|
|
64
|
+
export function transformVector(out: Vec3, m: Mat4, v: Vec3): Vec3 {
|
|
65
|
+
let x = v[0], y = v[1], z = v[2]
|
|
66
|
+
out[0] = m[0] * x + m[4] * y + m[8] * z
|
|
67
|
+
out[1] = m[1] * x + m[5] * y + m[9] * z
|
|
68
|
+
out[2] = m[2] * x + m[6] * y + m[10] * z
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Invert an AFFINE matrix - a world or local matrix whose bottom row is
|
|
74
|
+
* 0,0,0,1, NOT a projection: the upper 3x3 inverts by cofactors and the
|
|
75
|
+
* translation is pulled back through it. Picking's world-to-local step.
|
|
76
|
+
* `out` may alias `m`. A degenerate (zero-scale) matrix yields the raw
|
|
77
|
+
* cofactors instead of NaNs, the same policy as normalMatrix.
|
|
78
|
+
*/
|
|
79
|
+
export function invertAffine(out: Mat4, m: Mat4): Mat4 {
|
|
80
|
+
let a = m[0], b = m[4], c = m[8]
|
|
81
|
+
let d = m[1], e = m[5], f = m[9]
|
|
82
|
+
let g = m[2], h = m[6], i = m[10]
|
|
83
|
+
let tx = m[12], ty = m[13], tz = m[14]
|
|
84
|
+
let c00 = e * i - f * h
|
|
85
|
+
let c01 = f * g - d * i
|
|
86
|
+
let c02 = d * h - e * g
|
|
87
|
+
let det = a * c00 + b * c01 + c * c02
|
|
88
|
+
let s = 1 / (det || 1)
|
|
89
|
+
let r00 = c00 * s, r01 = (c * h - b * i) * s, r02 = (b * f - c * e) * s
|
|
90
|
+
let r10 = c01 * s, r11 = (a * i - c * g) * s, r12 = (c * d - a * f) * s
|
|
91
|
+
let r20 = c02 * s, r21 = (b * g - a * h) * s, r22 = (a * e - b * d) * s
|
|
92
|
+
out[0] = r00; out[1] = r10; out[2] = r20; out[3] = 0
|
|
93
|
+
out[4] = r01; out[5] = r11; out[6] = r21; out[7] = 0
|
|
94
|
+
out[8] = r02; out[9] = r12; out[10] = r22; out[11] = 0
|
|
95
|
+
out[12] = -(r00 * tx + r01 * ty + r02 * tz)
|
|
96
|
+
out[13] = -(r10 * tx + r11 * ty + r12 * tz)
|
|
97
|
+
out[14] = -(r20 * tx + r21 * ty + r22 * tz)
|
|
98
|
+
out[15] = 1
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
60
102
|
/** out = a * b (column vectors: b applies first). out may alias a or b. */
|
|
61
103
|
export function multiply(out: Mat4, a: Mat4, b: Mat4): Mat4 {
|
|
62
104
|
let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]
|