@weasel-js/kernel3d 1.4.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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/index.d.ts +287 -0
- package/dist/index.js +392 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @weasel-js/kernel3d
|
|
2
|
+
|
|
3
|
+
Poses, an orbit camera, ray picking and screen-projected chrome geometry for
|
|
4
|
+
weasel, over `@weasel-js/core`'s scene graph and dispatcher.
|
|
5
|
+
|
|
6
|
+
**It hosts a renderer rather than owning one.** A consumer brings its own — a
|
|
7
|
+
bespoke WebGL pass, three.js, anything — and the kernel hands it poses. Nothing
|
|
8
|
+
here touches WebGL.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createScene } from '@weasel-js/core';
|
|
12
|
+
import {
|
|
13
|
+
createCamera, createNodeAtPoint, createPoseDescriptor, pose3, type Pose3,
|
|
14
|
+
} from '@weasel-js/kernel3d';
|
|
15
|
+
|
|
16
|
+
const scene = createScene<MyData, 'solids', Pose3>({ systemLayers: [{ id: 'solids' }] });
|
|
17
|
+
const world = { scene, viewport: () => ({ camera, width, height }) };
|
|
18
|
+
|
|
19
|
+
// Every dep takes its camera from a thunk. That is the whole trick: the
|
|
20
|
+
// dispatcher keeps handing actions two numbers, and the camera each dep closes
|
|
21
|
+
// over is what makes them a ray.
|
|
22
|
+
const nodeAtPoint = createNodeAtPoint(world);
|
|
23
|
+
const poseDescriptor = createPoseDescriptor(world);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What core did not have to change
|
|
27
|
+
|
|
28
|
+
`Scene` is generic over its pose and holds a `Pose3` — position, quaternion,
|
|
29
|
+
scale — with no adapter and no special case. `InvocationCtx` still carries two
|
|
30
|
+
numbers: a 3D host passes the dispatcher an identity `clientToWorld`, so
|
|
31
|
+
`ctx.world` is the screen point, and each dep rebuilds the ray itself. Tools,
|
|
32
|
+
the select tool included, transfer untouched.
|
|
33
|
+
|
|
34
|
+
Two things do not, and the kernel says so rather than guessing:
|
|
35
|
+
|
|
36
|
+
- **`ViewApi` has no orientation**, so an orbit camera cannot travel through
|
|
37
|
+
the kit's `view`. The kernel declares a `camera3d` dep of its own.
|
|
38
|
+
- **`PoseDescriptor.remapBounds` and `fromBounds` throw.** A screen rectangle
|
|
39
|
+
does not name a 3D pose without a depth. `getBounds` and `intersectsRect` run
|
|
40
|
+
the other way and work, which is what drives selection chrome through an
|
|
41
|
+
orbit.
|
|
42
|
+
|
|
43
|
+
## The bounds seam
|
|
44
|
+
|
|
45
|
+
`World3d.bounds` is how a consumer says how big a node is. The kernel's default
|
|
46
|
+
carries a unit cube by the pose; override it when a primitive knows better. A
|
|
47
|
+
sphere is the case that forces the seam to be shaped this way — its box is the
|
|
48
|
+
same under every rotation, and no transform of a local box reproduces that.
|
|
49
|
+
|
|
50
|
+
## Math
|
|
51
|
+
|
|
52
|
+
Vectors, quaternions, 4×4 matrices and ray intersection live in
|
|
53
|
+
`@weasel-js/geom/3d` — the same dependency-free geometry package the 2D kit is
|
|
54
|
+
built on. They are immutable tuples, not classes: `History.serialize()` promises
|
|
55
|
+
a structured-clone-safe form, and `structuredClone` keeps a class instance's
|
|
56
|
+
data while dropping its prototype, so a `Vector3` in a pose would return from a
|
|
57
|
+
reload as a bare object whose first method call throws far from the cause.
|
|
58
|
+
|
|
59
|
+
Interop with three.js is close to free anyway — `Vector3`, `Quaternion` and
|
|
60
|
+
`Matrix4` all carry `fromArray`/`toArray` and agree with this layout.
|
|
61
|
+
|
|
62
|
+
Direction: `docs/superpowers/specs/2026-08-22-3d-kernel-design.md`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { Vec3, Mat4, Quat, Aabb, Ray } from '@weasel-js/geom/3d';
|
|
2
|
+
import { SceneNode, Scene, NodeId, AreaSelectDep, InsertExtras, InsertDep, NodeAtPointDep, PoseDescriptor, SnapDep, OngoingHandle, Action, Tool } from '@weasel-js/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An orbit camera: a target, two angles and a distance. Immutable — every
|
|
6
|
+
* gesture returns a new one, so it can live in instrument state.
|
|
7
|
+
*
|
|
8
|
+
* This is the shape `ViewApi` cannot hold, and the reason the kernel declares a
|
|
9
|
+
* `camera3d` dep of its own rather than reusing the kit's `view`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface Camera3d {
|
|
13
|
+
target: Vec3;
|
|
14
|
+
/** Radians about +y, measured from +z. */
|
|
15
|
+
yaw: number;
|
|
16
|
+
/** Radians above the xz plane. */
|
|
17
|
+
pitch: number;
|
|
18
|
+
distance: number;
|
|
19
|
+
fov: number;
|
|
20
|
+
near: number;
|
|
21
|
+
far: number;
|
|
22
|
+
}
|
|
23
|
+
/** Short of vertical: at exactly ±π/2 the eye lands on the up axis and `lookAt` degenerates. */
|
|
24
|
+
declare const MAX_PITCH: number;
|
|
25
|
+
declare function createCamera(init: Partial<Camera3d>): Camera3d;
|
|
26
|
+
declare function cameraEye(camera: Camera3d): Vec3;
|
|
27
|
+
declare function cameraView(camera: Camera3d): Mat4;
|
|
28
|
+
declare function cameraViewProjection(camera: Camera3d, aspect: number): Mat4;
|
|
29
|
+
declare function orbitBy(camera: Camera3d, deltaYaw: number, deltaPitch: number): Camera3d;
|
|
30
|
+
declare function dollyBy(camera: Camera3d, factor: number): Camera3d;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A 3D pose, and the two things every consumer of one needs: its matrix, and
|
|
34
|
+
* the world box it occupies.
|
|
35
|
+
*
|
|
36
|
+
* `Scene<TData, TLayer, Pose3>` is the whole of how this reaches weasel —
|
|
37
|
+
* `Scene` is generic over its pose and holds this one with no adapter, no
|
|
38
|
+
* subclass and no special case.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
interface Pose3 {
|
|
42
|
+
position: Vec3;
|
|
43
|
+
rotation: Quat;
|
|
44
|
+
scale: Vec3;
|
|
45
|
+
}
|
|
46
|
+
/** The box a primitive occupies before its pose. A unit primitive centred on
|
|
47
|
+
* its own origin — which is what the kernel assumes nothing about — is
|
|
48
|
+
* `UNIT_CUBE`. */
|
|
49
|
+
declare const UNIT_CUBE: Aabb;
|
|
50
|
+
declare function pose3(position: Vec3, scale?: Vec3, rotation?: Quat): Pose3;
|
|
51
|
+
declare function poseMatrix(pose: Pose3): Mat4;
|
|
52
|
+
/**
|
|
53
|
+
* The world box a posed primitive occupies, given the box it occupies in its
|
|
54
|
+
* own space.
|
|
55
|
+
*
|
|
56
|
+
* Which local box a primitive has is the consumer's answer, not the kernel's:
|
|
57
|
+
* a sphere's is rotation-invariant and a mesh's comes from its vertices, and
|
|
58
|
+
* neither is something a pose can tell you.
|
|
59
|
+
*/
|
|
60
|
+
declare function aabbOfPose(pose: Pose3, local?: Aabb): Aabb;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The screen side of the kernel: where a pointer points, and what rectangle a
|
|
64
|
+
* world box covers. This is the seam weasel's 2D chrome meets — everything
|
|
65
|
+
* here speaks `Bounds`-shaped screen rectangles, which is why a 3D host can
|
|
66
|
+
* pass the dispatcher's world point through unchanged.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/** A pane's rectangle in CSS pixels, relative to the client. */
|
|
70
|
+
interface ViewportRect {
|
|
71
|
+
x: number;
|
|
72
|
+
y: number;
|
|
73
|
+
w: number;
|
|
74
|
+
h: number;
|
|
75
|
+
}
|
|
76
|
+
/** A screen rectangle in the shape core's `Bounds` uses. */
|
|
77
|
+
interface ScreenBox {
|
|
78
|
+
x: number;
|
|
79
|
+
y: number;
|
|
80
|
+
width: number;
|
|
81
|
+
height: number;
|
|
82
|
+
}
|
|
83
|
+
/** A `ScreenBox` a host is being asked to outline. `tint` says why: warm for
|
|
84
|
+
* what is selected, cool for what the gesture in hand is proposing. */
|
|
85
|
+
interface ChromeBox extends ScreenBox {
|
|
86
|
+
tint?: 'selection' | 'gesture';
|
|
87
|
+
}
|
|
88
|
+
/** Screen point (y down, rect-relative) to normalized device coordinates (y up). */
|
|
89
|
+
declare function screenToNdc(point: {
|
|
90
|
+
x: number;
|
|
91
|
+
y: number;
|
|
92
|
+
}, rect: ViewportRect): {
|
|
93
|
+
x: number;
|
|
94
|
+
y: number;
|
|
95
|
+
};
|
|
96
|
+
declare function ndcToScreen(ndc: {
|
|
97
|
+
x: number;
|
|
98
|
+
y: number;
|
|
99
|
+
}, rect: ViewportRect): {
|
|
100
|
+
x: number;
|
|
101
|
+
y: number;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The ray a screen point names, given the camera that drew the rect. This is
|
|
105
|
+
* the whole of what 3D picking needs from a pointer — which is why the world
|
|
106
|
+
* point the dispatcher carries stays two numbers.
|
|
107
|
+
*/
|
|
108
|
+
declare function rayThroughScreenPoint(point: {
|
|
109
|
+
x: number;
|
|
110
|
+
y: number;
|
|
111
|
+
}, rect: ViewportRect, viewProjection: Mat4, eye: Vec3): Ray;
|
|
112
|
+
/**
|
|
113
|
+
* The screen rectangle a world-space box covers — the currency weasel's chrome
|
|
114
|
+
* and `PoseDescriptor.getBounds` speak in. `null` when the box is entirely
|
|
115
|
+
* behind the near plane.
|
|
116
|
+
*
|
|
117
|
+
* Each of the twelve edges is clipped against the near plane before it is
|
|
118
|
+
* projected, rather than dropping the corners behind it. Dropping them reports
|
|
119
|
+
* a box that is too small for anything straddling the near plane, and the error
|
|
120
|
+
* grows as the camera moves in: a solid the pointer is inside of would outline
|
|
121
|
+
* as a sliver, or as nothing.
|
|
122
|
+
*/
|
|
123
|
+
declare function projectAabbToScreen(box: Aabb, viewProjection: Mat4, rect: ViewportRect): ScreenBox | null;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The kit's deps, implemented against a 3D world.
|
|
127
|
+
*
|
|
128
|
+
* Every one of these takes its camera from a thunk rather than an argument.
|
|
129
|
+
* That is the kernel's central claim in code: the dispatcher keeps handing
|
|
130
|
+
* actions two numbers, and what makes them mean something in 3D is the camera
|
|
131
|
+
* each dep already closes over. `InvocationCtx` needs no widening, and none of
|
|
132
|
+
* these needs a ray passed in.
|
|
133
|
+
*
|
|
134
|
+
* Where a kit contract cannot be honored, the implementation throws rather than
|
|
135
|
+
* guessing. A guess would report a fit the kernel does not have.
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
interface Viewport3d {
|
|
139
|
+
camera: Camera3d;
|
|
140
|
+
width: number;
|
|
141
|
+
height: number;
|
|
142
|
+
/**
|
|
143
|
+
* Where the pane sits in client coordinates. A 3D host works in one space
|
|
144
|
+
* from the event to the ray — it passes the dispatcher an identity
|
|
145
|
+
* `clientToWorld` — so the deps subtract this rather than the dispatcher.
|
|
146
|
+
*/
|
|
147
|
+
originX?: number;
|
|
148
|
+
originY?: number;
|
|
149
|
+
}
|
|
150
|
+
type ViewportSource = () => Viewport3d;
|
|
151
|
+
/** A 3D scene is core's scene with a `Pose3`. Nothing wraps it. */
|
|
152
|
+
type Scene3d<TData, TLayer extends string> = Scene<TData, TLayer, Pose3>;
|
|
153
|
+
type Node3d<TData, TLayer extends string> = SceneNode<TData, TLayer, Pose3>;
|
|
154
|
+
/**
|
|
155
|
+
* The world box a node occupies. The kernel cannot answer this, and the reason
|
|
156
|
+
* is sharper than "it does not know the mesh": a sphere's box is the same under
|
|
157
|
+
* every rotation, and no transform of a local box reproduces that — rotate a
|
|
158
|
+
* cube and it widens. Asking for the world box directly is what lets a
|
|
159
|
+
* primitive answer for its own symmetry.
|
|
160
|
+
*
|
|
161
|
+
* Omitted, a node is a unit cube carried by its pose.
|
|
162
|
+
*/
|
|
163
|
+
type NodeBounds<TData, TLayer extends string> = (node: Node3d<TData, TLayer>) => Aabb;
|
|
164
|
+
interface World3d<TData, TLayer extends string> {
|
|
165
|
+
scene: Scene3d<TData, TLayer>;
|
|
166
|
+
viewport: ViewportSource;
|
|
167
|
+
bounds?: NodeBounds<TData, TLayer>;
|
|
168
|
+
/** The axis gestures resolve against. Default `+y`. */
|
|
169
|
+
up?: Vec3;
|
|
170
|
+
}
|
|
171
|
+
/** The screen box a node covers, or null when it projects to nothing visible. */
|
|
172
|
+
declare function screenBoxOf<TData, TLayer extends string>(world: World3d<TData, TLayer>, id: NodeId, viewport: Viewport3d): ScreenBox | null;
|
|
173
|
+
/**
|
|
174
|
+
* Nearest node along the ray the screen point names.
|
|
175
|
+
*
|
|
176
|
+
* The kit calls this with what it believes is a world point; a 3D host feeds
|
|
177
|
+
* the dispatcher an identity `clientToWorld`, so it is the pane-relative screen
|
|
178
|
+
* point, which is exactly what a ray needs.
|
|
179
|
+
*/
|
|
180
|
+
declare function createNodeAtPoint<TData, TLayer extends string>(world: World3d<TData, TLayer>): NodeAtPointDep;
|
|
181
|
+
/** Marquee: the nodes whose projected box the screen rectangle touches. */
|
|
182
|
+
declare function createAreaSelect<TData, TLayer extends string>(world: World3d<TData, TLayer>, selection: {
|
|
183
|
+
get(): readonly NodeId[];
|
|
184
|
+
set(ids: NodeId[]): void;
|
|
185
|
+
}): AreaSelectDep;
|
|
186
|
+
/** No grid in 3D yet; the identity keeps `insertAction` happy without lying. */
|
|
187
|
+
declare function createSnap(): SnapDep;
|
|
188
|
+
/** The ground-plane rectangle a drag swept out, in world units. */
|
|
189
|
+
interface Footprint {
|
|
190
|
+
/** Centre of the swept rectangle, on the ground plane. */
|
|
191
|
+
center: Vec3;
|
|
192
|
+
/** Extent along the two axes the up axis is not. */
|
|
193
|
+
width: number;
|
|
194
|
+
depth: number;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* A drag rectangle becomes a footprint on the ground plane: its four corners
|
|
198
|
+
* are cast onto the plane and the hit points give the extent. What node that
|
|
199
|
+
* implies is the consumer's answer — the kernel does not know what it is
|
|
200
|
+
* inserting.
|
|
201
|
+
*
|
|
202
|
+
* The kit's `InsertDep.commit` hands over a screen-space AABB and nothing else,
|
|
203
|
+
* which turns out to be enough: the depth the 2D contract cannot express comes
|
|
204
|
+
* from the plane, not from the caller.
|
|
205
|
+
*/
|
|
206
|
+
declare function createInsert(opts: {
|
|
207
|
+
viewport: ViewportSource;
|
|
208
|
+
up?: Vec3;
|
|
209
|
+
/** Smallest footprint a click-sized drag produces. Default 0.25. */
|
|
210
|
+
minExtent?: number;
|
|
211
|
+
mint(footprint: Footprint, extras: InsertExtras): NodeId | null;
|
|
212
|
+
}): InsertDep;
|
|
213
|
+
/**
|
|
214
|
+
* `PoseDescriptor` over a 3D pose, with `Bounds` read as the screen box the
|
|
215
|
+
* node covers. Chrome and marquee math work unchanged; the two methods that
|
|
216
|
+
* run the other way cannot.
|
|
217
|
+
*
|
|
218
|
+
* `forNode` is what lets a sphere bound itself as a sphere: every node is a
|
|
219
|
+
* `Pose3`, so the shape lives on `node.data` and nothing but the node can say
|
|
220
|
+
* which one this is. Unspecialized — the kit calling with only a pose in hand —
|
|
221
|
+
* it answers with the default local box, which is the looser of the two.
|
|
222
|
+
*
|
|
223
|
+
* The contract mismatch that remains, recorded in the kernel doc: a screen
|
|
224
|
+
* rectangle does not name a 3D pose without a depth, so `remapBounds` and
|
|
225
|
+
* `fromBounds` have no honest answer.
|
|
226
|
+
*/
|
|
227
|
+
declare function createPoseDescriptor<TData, TLayer extends string>(world: World3d<TData, TLayer>): PoseDescriptor<Pose3, Node3d<TData, TLayer>>;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* What a host paints for a gesture that proposes chrome rather than a pose.
|
|
231
|
+
*
|
|
232
|
+
* A marquee sweep and a box-drag both publish an `OngoingOverlay`, which used
|
|
233
|
+
* to reach paint only through core's own 2D layer. `resolveOverlays` hands back
|
|
234
|
+
* world geometry with no renderer in it, so the same call `<SceneCanvas>` makes
|
|
235
|
+
* serves a consumer-owned 3D renderer.
|
|
236
|
+
*
|
|
237
|
+
* A 3D host's `clientToWorld` is identity, so an overlay's world bounds are the
|
|
238
|
+
* client rect the deps already work in, and the pane origin comes off exactly
|
|
239
|
+
* once. Only the rectangular variants resolve here: a lasso, a pencil and a run
|
|
240
|
+
* of points are paths, and a path is geometry this cannot flatten to a box.
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
/** Gesture chrome, in CSS pixels relative to the pane's top-left. */
|
|
244
|
+
declare function collectOverlayBoxes(handles: Iterable<OngoingHandle>, origin: {
|
|
245
|
+
x: number;
|
|
246
|
+
y: number;
|
|
247
|
+
}): ChromeBox[];
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Camera tools and their actions.
|
|
251
|
+
*
|
|
252
|
+
* Shape tools transfer from the kit untouched — a 3D host mounts core's own
|
|
253
|
+
* `useSelectTool` and a rectangle-style insert binding and neither notices the
|
|
254
|
+
* dimension. Only the viewport tools are written here, which is the one
|
|
255
|
+
* exception the kernel doc predicted: `useHandTool` routes to
|
|
256
|
+
* `viewport.dragPan`, and that wants the 2D `view`.
|
|
257
|
+
*/
|
|
258
|
+
|
|
259
|
+
declare const orbitAction: Action;
|
|
260
|
+
declare const dollyAction: Action;
|
|
261
|
+
declare function useOrbitTool(): Tool<null>;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The kernel's one new dep.
|
|
265
|
+
*
|
|
266
|
+
* `ViewApi` is pan and scale with no orientation, so an orbit camera cannot
|
|
267
|
+
* travel through the kit's `view`. It gets a slot of its own, declared the way
|
|
268
|
+
* any consumer declares one — which is the only place the kernel asks core for
|
|
269
|
+
* anything core does not already offer.
|
|
270
|
+
*/
|
|
271
|
+
|
|
272
|
+
interface Camera3dDep {
|
|
273
|
+
get(): Camera3d;
|
|
274
|
+
set(camera: Camera3d): void;
|
|
275
|
+
/** Pane size in CSS pixels — what turns a screen point into a ray. */
|
|
276
|
+
size(): {
|
|
277
|
+
width: number;
|
|
278
|
+
height: number;
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
declare module '@weasel-js/core' {
|
|
282
|
+
interface DepSchema {
|
|
283
|
+
camera3d?: Camera3dDep;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export { type Camera3d, type Camera3dDep, type ChromeBox, type Footprint, MAX_PITCH, type Node3d, type NodeBounds, type Pose3, type Scene3d, type ScreenBox, UNIT_CUBE, type Viewport3d, type ViewportRect, type ViewportSource, type World3d, aabbOfPose, cameraEye, cameraView, cameraViewProjection, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, ndcToScreen, orbitAction, orbitBy, pose3, poseMatrix, projectAabbToScreen, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { lookAt, multiply, perspective, quatIdentity, compose, transformAabb, invert, transformPoint, normalize, sub, transformPoint4, intersectRayAabb, intersectRayPlane } from '@weasel-js/geom/3d';
|
|
2
|
+
import { resolveOverlays, defineTool } from '@weasel-js/core';
|
|
3
|
+
|
|
4
|
+
// src/camera.ts
|
|
5
|
+
var MAX_PITCH = Math.PI / 2 - 0.01;
|
|
6
|
+
var MIN_DISTANCE = 0.5;
|
|
7
|
+
var MAX_DISTANCE = 500;
|
|
8
|
+
var UP = [0, 1, 0];
|
|
9
|
+
function createCamera(init) {
|
|
10
|
+
return {
|
|
11
|
+
target: init.target ?? [0, 0, 0],
|
|
12
|
+
yaw: init.yaw ?? 0,
|
|
13
|
+
pitch: init.pitch ?? 0,
|
|
14
|
+
distance: init.distance ?? 10,
|
|
15
|
+
fov: init.fov ?? Math.PI / 4,
|
|
16
|
+
near: init.near ?? 0.1,
|
|
17
|
+
far: init.far ?? 1e3
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function cameraEye(camera) {
|
|
21
|
+
const { target, yaw, pitch, distance } = camera;
|
|
22
|
+
const horizontal = Math.cos(pitch) * distance;
|
|
23
|
+
return [
|
|
24
|
+
target[0] + horizontal * Math.sin(yaw),
|
|
25
|
+
target[1] + Math.sin(pitch) * distance,
|
|
26
|
+
target[2] + horizontal * Math.cos(yaw)
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
function cameraView(camera) {
|
|
30
|
+
return lookAt(cameraEye(camera), camera.target, UP);
|
|
31
|
+
}
|
|
32
|
+
function cameraViewProjection(camera, aspect) {
|
|
33
|
+
return multiply(perspective(camera.fov, aspect, camera.near, camera.far), cameraView(camera));
|
|
34
|
+
}
|
|
35
|
+
function orbitBy(camera, deltaYaw, deltaPitch) {
|
|
36
|
+
return {
|
|
37
|
+
...camera,
|
|
38
|
+
yaw: camera.yaw + deltaYaw,
|
|
39
|
+
pitch: Math.max(-MAX_PITCH, Math.min(MAX_PITCH, camera.pitch + deltaPitch))
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function dollyBy(camera, factor) {
|
|
43
|
+
return {
|
|
44
|
+
...camera,
|
|
45
|
+
distance: Math.max(MIN_DISTANCE, Math.min(MAX_DISTANCE, camera.distance * factor))
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
var UNIT_CUBE = { min: [-0.5, -0.5, -0.5], max: [0.5, 0.5, 0.5] };
|
|
49
|
+
function pose3(position, scale = [1, 1, 1], rotation = quatIdentity()) {
|
|
50
|
+
return { position, rotation, scale };
|
|
51
|
+
}
|
|
52
|
+
function poseMatrix(pose) {
|
|
53
|
+
return compose(pose.position, pose.rotation, pose.scale);
|
|
54
|
+
}
|
|
55
|
+
function aabbOfPose(pose, local = UNIT_CUBE) {
|
|
56
|
+
return transformAabb(poseMatrix(pose), local);
|
|
57
|
+
}
|
|
58
|
+
var EPS = 1e-9;
|
|
59
|
+
function screenToNdc(point, rect) {
|
|
60
|
+
return {
|
|
61
|
+
x: (point.x - rect.x) / rect.w * 2 - 1,
|
|
62
|
+
y: 1 - (point.y - rect.y) / rect.h * 2
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function ndcToScreen(ndc, rect) {
|
|
66
|
+
return {
|
|
67
|
+
x: rect.x + (ndc.x * 0.5 + 0.5) * rect.w,
|
|
68
|
+
y: rect.y + (1 - (ndc.y * 0.5 + 0.5)) * rect.h
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function rayThroughScreenPoint(point, rect, viewProjection, eye) {
|
|
72
|
+
const inv = invert(viewProjection);
|
|
73
|
+
if (!inv) return { origin: eye, direction: [0, 0, -1] };
|
|
74
|
+
const ndc = screenToNdc(point, rect);
|
|
75
|
+
const near = transformPoint(inv, [ndc.x, ndc.y, -1]);
|
|
76
|
+
const far = transformPoint(inv, [ndc.x, ndc.y, 1]);
|
|
77
|
+
return { origin: eye, direction: normalize(sub(far, near)) };
|
|
78
|
+
}
|
|
79
|
+
function nearSide(p) {
|
|
80
|
+
return p[2] + p[3];
|
|
81
|
+
}
|
|
82
|
+
function lerpClip(a, b, t) {
|
|
83
|
+
return [
|
|
84
|
+
a[0] + (b[0] - a[0]) * t,
|
|
85
|
+
a[1] + (b[1] - a[1]) * t,
|
|
86
|
+
a[2] + (b[2] - a[2]) * t,
|
|
87
|
+
a[3] + (b[3] - a[3]) * t
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
var EDGE_BITS = [1, 2, 4];
|
|
91
|
+
function projectAabbToScreen(box, viewProjection, rect) {
|
|
92
|
+
const corners = [];
|
|
93
|
+
for (let i = 0; i < 8; i++) {
|
|
94
|
+
const corner = [
|
|
95
|
+
i & 1 ? box.max[0] : box.min[0],
|
|
96
|
+
i & 2 ? box.max[1] : box.min[1],
|
|
97
|
+
i & 4 ? box.max[2] : box.min[2]
|
|
98
|
+
];
|
|
99
|
+
corners.push(transformPoint4(viewProjection, corner));
|
|
100
|
+
}
|
|
101
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
102
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
103
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
104
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
105
|
+
let any = false;
|
|
106
|
+
const extend = (p) => {
|
|
107
|
+
if (p[3] <= EPS) return;
|
|
108
|
+
const screen = ndcToScreen({ x: p[0] / p[3], y: p[1] / p[3] }, rect);
|
|
109
|
+
minX = Math.min(minX, screen.x);
|
|
110
|
+
minY = Math.min(minY, screen.y);
|
|
111
|
+
maxX = Math.max(maxX, screen.x);
|
|
112
|
+
maxY = Math.max(maxY, screen.y);
|
|
113
|
+
any = true;
|
|
114
|
+
};
|
|
115
|
+
for (let i = 0; i < 8; i++) {
|
|
116
|
+
for (const bit of EDGE_BITS) {
|
|
117
|
+
if (i & bit) continue;
|
|
118
|
+
let a = corners[i];
|
|
119
|
+
let b = corners[i | bit];
|
|
120
|
+
const da = nearSide(a);
|
|
121
|
+
const db = nearSide(b);
|
|
122
|
+
if (da < 0 && db < 0) continue;
|
|
123
|
+
if (da < 0) a = lerpClip(a, b, da / (da - db));
|
|
124
|
+
else if (db < 0) b = lerpClip(b, a, db / (db - da));
|
|
125
|
+
extend(a);
|
|
126
|
+
extend(b);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!any) return null;
|
|
130
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
131
|
+
}
|
|
132
|
+
var DEFAULT_UP = [0, 1, 0];
|
|
133
|
+
function boundsOfNode(world, node) {
|
|
134
|
+
return world.bounds ? world.bounds(node) : aabbOfPose(node.pose);
|
|
135
|
+
}
|
|
136
|
+
function rectOf(viewport) {
|
|
137
|
+
return {
|
|
138
|
+
x: viewport.originX ?? 0,
|
|
139
|
+
y: viewport.originY ?? 0,
|
|
140
|
+
w: viewport.width,
|
|
141
|
+
h: viewport.height
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function viewProjectionOf(viewport) {
|
|
145
|
+
return cameraViewProjection(viewport.camera, viewport.width / Math.max(1, viewport.height));
|
|
146
|
+
}
|
|
147
|
+
function rayAt(viewport, point) {
|
|
148
|
+
return rayThroughScreenPoint(
|
|
149
|
+
point,
|
|
150
|
+
rectOf(viewport),
|
|
151
|
+
viewProjectionOf(viewport),
|
|
152
|
+
cameraEye(viewport.camera)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
function pointOnRay(ray, t) {
|
|
156
|
+
return [
|
|
157
|
+
ray.origin[0] + ray.direction[0] * t,
|
|
158
|
+
ray.origin[1] + ray.direction[1] * t,
|
|
159
|
+
ray.origin[2] + ray.direction[2] * t
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
function overlaps(a, b) {
|
|
163
|
+
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
164
|
+
}
|
|
165
|
+
function screenBoxOf(world, id, viewport) {
|
|
166
|
+
const node = world.scene.get(id);
|
|
167
|
+
if (!node) return null;
|
|
168
|
+
return projectAabbToScreen(
|
|
169
|
+
boundsOfNode(world, node),
|
|
170
|
+
viewProjectionOf(viewport),
|
|
171
|
+
rectOf(viewport)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
function createNodeAtPoint(world) {
|
|
175
|
+
return (point, exclude) => {
|
|
176
|
+
const excluded = exclude ? new Set(exclude) : null;
|
|
177
|
+
const vp = world.viewport();
|
|
178
|
+
const ray = rayAt(vp, point);
|
|
179
|
+
let best = null;
|
|
180
|
+
let bestT = Infinity;
|
|
181
|
+
for (const node of world.scene.renderOrderNodes()) {
|
|
182
|
+
if (excluded?.has(node.id)) continue;
|
|
183
|
+
const { min, max } = boundsOfNode(world, node);
|
|
184
|
+
const t = intersectRayAabb(ray, min, max);
|
|
185
|
+
if (t !== null && t < bestT) {
|
|
186
|
+
bestT = t;
|
|
187
|
+
best = node.id;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return best;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function createAreaSelect(world, selection) {
|
|
194
|
+
return {
|
|
195
|
+
hitTestArea(bounds) {
|
|
196
|
+
const vp = world.viewport();
|
|
197
|
+
const hits = [];
|
|
198
|
+
for (const node of world.scene.renderOrderNodes()) {
|
|
199
|
+
const box = screenBoxOf(world, node.id, vp);
|
|
200
|
+
if (box && overlaps(box, bounds)) hits.push(node.id);
|
|
201
|
+
}
|
|
202
|
+
return hits;
|
|
203
|
+
},
|
|
204
|
+
getSelection: () => [...selection.get()],
|
|
205
|
+
setSelection: (ids) => selection.set([...ids])
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function createSnap() {
|
|
209
|
+
return { point: (p) => p };
|
|
210
|
+
}
|
|
211
|
+
function createInsert(opts) {
|
|
212
|
+
const up = opts.up ?? DEFAULT_UP;
|
|
213
|
+
const minExtent = opts.minExtent ?? 0.25;
|
|
214
|
+
const axes = up[1] !== 0 ? [0, 2] : up[0] !== 0 ? [1, 2] : [0, 1];
|
|
215
|
+
return {
|
|
216
|
+
commit(bounds, extras) {
|
|
217
|
+
const vp = opts.viewport();
|
|
218
|
+
const corners = [
|
|
219
|
+
{ x: bounds.x, y: bounds.y },
|
|
220
|
+
{ x: bounds.x + bounds.width, y: bounds.y },
|
|
221
|
+
{ x: bounds.x, y: bounds.y + bounds.height },
|
|
222
|
+
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height }
|
|
223
|
+
];
|
|
224
|
+
const hits = [];
|
|
225
|
+
for (const corner of corners) {
|
|
226
|
+
const ray = rayAt(vp, corner);
|
|
227
|
+
const t = intersectRayPlane(ray, up, 0);
|
|
228
|
+
if (t !== null) hits.push(pointOnRay(ray, t));
|
|
229
|
+
}
|
|
230
|
+
if (hits.length === 0) return null;
|
|
231
|
+
const [u, v] = axes;
|
|
232
|
+
let minU = Infinity, maxU = -Infinity, minV = Infinity, maxV = -Infinity;
|
|
233
|
+
for (const h of hits) {
|
|
234
|
+
minU = Math.min(minU, h[u]);
|
|
235
|
+
maxU = Math.max(maxU, h[u]);
|
|
236
|
+
minV = Math.min(minV, h[v]);
|
|
237
|
+
maxV = Math.max(maxV, h[v]);
|
|
238
|
+
}
|
|
239
|
+
const center = [0, 0, 0];
|
|
240
|
+
center[u] = (minU + maxU) / 2;
|
|
241
|
+
center[v] = (minV + maxV) / 2;
|
|
242
|
+
return opts.mint(
|
|
243
|
+
{
|
|
244
|
+
center,
|
|
245
|
+
width: Math.max(minExtent, maxU - minU),
|
|
246
|
+
depth: Math.max(minExtent, maxV - minV)
|
|
247
|
+
},
|
|
248
|
+
extras
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function createPoseDescriptor(world) {
|
|
254
|
+
const up = world.up ?? DEFAULT_UP;
|
|
255
|
+
const upAxis = up[1] !== 0 ? 1 : up[0] !== 0 ? 0 : 2;
|
|
256
|
+
const specialized = /* @__PURE__ */ new WeakMap();
|
|
257
|
+
function build(worldBox) {
|
|
258
|
+
const boundsOf = (pose) => {
|
|
259
|
+
const vp = world.viewport();
|
|
260
|
+
return projectAabbToScreen(worldBox(pose), viewProjectionOf(vp), rectOf(vp));
|
|
261
|
+
};
|
|
262
|
+
const descriptor = {
|
|
263
|
+
forNode(node) {
|
|
264
|
+
let d = specialized.get(node);
|
|
265
|
+
if (!d) {
|
|
266
|
+
d = build((pose) => boundsOfNode(world, { ...node, pose }));
|
|
267
|
+
specialized.set(node, d);
|
|
268
|
+
}
|
|
269
|
+
return d;
|
|
270
|
+
},
|
|
271
|
+
getBounds(pose) {
|
|
272
|
+
return boundsOf(pose) ?? { x: 0, y: 0, width: 0, height: 0 };
|
|
273
|
+
},
|
|
274
|
+
remapBounds() {
|
|
275
|
+
throw new Error(
|
|
276
|
+
"kernel3d: remapBounds has no 3D answer \u2014 a screen rectangle does not name a pose without a depth."
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
fromBounds() {
|
|
280
|
+
throw new Error(
|
|
281
|
+
"kernel3d: fromBounds has no 3D answer \u2014 a screen rectangle does not name a pose without a depth."
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
/**
|
|
285
|
+
* Screen-space deltas resolve against the plane the node already sits on,
|
|
286
|
+
* so the two numbers the kit passes are enough: the camera that turns
|
|
287
|
+
* them into a world position is the one this dep closed over.
|
|
288
|
+
*/
|
|
289
|
+
translate(pose, dx, dy) {
|
|
290
|
+
const vp = world.viewport();
|
|
291
|
+
const box = boundsOf(pose);
|
|
292
|
+
if (!box) return pose;
|
|
293
|
+
const from = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
294
|
+
const to = { x: from.x + dx, y: from.y + dy };
|
|
295
|
+
const offset = pose.position[upAxis];
|
|
296
|
+
const ray = rayAt(vp, to);
|
|
297
|
+
const t = intersectRayPlane(ray, up, offset);
|
|
298
|
+
if (t === null) return pose;
|
|
299
|
+
const anchorRay = rayAt(vp, from);
|
|
300
|
+
const anchorT = intersectRayPlane(anchorRay, up, offset);
|
|
301
|
+
if (anchorT === null) return pose;
|
|
302
|
+
const delta = sub(pointOnRay(ray, t), pointOnRay(anchorRay, anchorT));
|
|
303
|
+
const position = [
|
|
304
|
+
pose.position[0] + delta[0],
|
|
305
|
+
pose.position[1] + delta[1],
|
|
306
|
+
pose.position[2] + delta[2]
|
|
307
|
+
];
|
|
308
|
+
position[upAxis] = pose.position[upAxis];
|
|
309
|
+
return { ...pose, position };
|
|
310
|
+
},
|
|
311
|
+
intersectsRect(pose, rect) {
|
|
312
|
+
const box = boundsOf(pose);
|
|
313
|
+
return box ? overlaps(box, rect) : false;
|
|
314
|
+
},
|
|
315
|
+
supportsRotation() {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
return descriptor;
|
|
320
|
+
}
|
|
321
|
+
return build((pose) => aabbOfPose(pose));
|
|
322
|
+
}
|
|
323
|
+
function collectOverlayBoxes(handles, origin) {
|
|
324
|
+
const overlays = [];
|
|
325
|
+
for (const handle of handles) {
|
|
326
|
+
const ov = handle.overlay?.();
|
|
327
|
+
if (ov) overlays.push(ov);
|
|
328
|
+
}
|
|
329
|
+
const boxes = [];
|
|
330
|
+
for (const resolved of resolveOverlays(overlays)) {
|
|
331
|
+
if (resolved.kind !== "marquee" && resolved.kind !== "insertPreview") continue;
|
|
332
|
+
const { x, y, width, height } = resolved.bounds;
|
|
333
|
+
boxes.push({ x: x - origin.x, y: y - origin.y, width, height, tint: "gesture" });
|
|
334
|
+
}
|
|
335
|
+
return boxes;
|
|
336
|
+
}
|
|
337
|
+
var ORBIT_RATE = 6e-3;
|
|
338
|
+
var DOLLY_RATE = 15e-4;
|
|
339
|
+
function cameraDep(deps) {
|
|
340
|
+
return deps.camera3d;
|
|
341
|
+
}
|
|
342
|
+
var orbitAction = {
|
|
343
|
+
id: "camera.orbit",
|
|
344
|
+
label: "Orbit",
|
|
345
|
+
requires: ["camera3d"],
|
|
346
|
+
invoker: {
|
|
347
|
+
timing: "ongoing",
|
|
348
|
+
start(ctx) {
|
|
349
|
+
const dep = cameraDep(ctx.deps);
|
|
350
|
+
const origin = dep?.get();
|
|
351
|
+
return {
|
|
352
|
+
kind: "orbit",
|
|
353
|
+
onMove(move) {
|
|
354
|
+
if (!dep || !origin) return;
|
|
355
|
+
const delta = move.drag?.screenDelta ?? move.drag?.delta;
|
|
356
|
+
if (!delta) return;
|
|
357
|
+
dep.set(orbitBy(origin, -delta.x * ORBIT_RATE, delta.y * ORBIT_RATE));
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
var dollyAction = {
|
|
364
|
+
id: "camera.dolly",
|
|
365
|
+
label: "Dolly",
|
|
366
|
+
group: "viewport",
|
|
367
|
+
// Wheel, in any tool — the dispatcher merges the event's deltas into params.
|
|
368
|
+
defaultBinding: [{ spec: { kind: "wheel" }, opts: {} }],
|
|
369
|
+
requires: ["camera3d"],
|
|
370
|
+
invoker: {
|
|
371
|
+
timing: "immediate",
|
|
372
|
+
run(deps, params) {
|
|
373
|
+
const dep = cameraDep(deps);
|
|
374
|
+
if (!dep) return;
|
|
375
|
+
const amount = typeof params?.deltaY === "number" ? params.deltaY : 0;
|
|
376
|
+
dep.set(dollyBy(dep.get(), Math.exp(amount * DOLLY_RATE)));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
function useOrbitTool() {
|
|
381
|
+
return defineTool({
|
|
382
|
+
id: "orbit",
|
|
383
|
+
hookName: "useOrbitTool",
|
|
384
|
+
cursor: "grab",
|
|
385
|
+
presentation: { label: "Orbit", group: "viewport" },
|
|
386
|
+
bindings: [{ spec: { kind: "drag" }, actionId: "camera.orbit" }]
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export { MAX_PITCH, UNIT_CUBE, aabbOfPose, cameraEye, cameraView, cameraViewProjection, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, ndcToScreen, orbitAction, orbitBy, pose3, poseMatrix, projectAabbToScreen, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool };
|
|
391
|
+
//# sourceMappingURL=index.js.map
|
|
392
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/camera.ts","../src/pose3.ts","../src/screen.ts","../src/deps.ts","../src/overlays.ts","../src/tools.ts"],"names":["sub"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * An orbit camera: a target, two angles and a distance. Immutable — every\n * gesture returns a new one, so it can live in instrument state.\n *\n * This is the shape `ViewApi` cannot hold, and the reason the kernel declares a\n * `camera3d` dep of its own rather than reusing the kit's `view`.\n */\n\nimport {\n lookAt,\n multiply,\n perspective,\n type Mat4,\n type Vec3,\n} from '@weasel-js/geom/3d';\n\nexport interface Camera3d {\n target: Vec3;\n /** Radians about +y, measured from +z. */\n yaw: number;\n /** Radians above the xz plane. */\n pitch: number;\n distance: number;\n fov: number;\n near: number;\n far: number;\n}\n\n/** Short of vertical: at exactly ±π/2 the eye lands on the up axis and `lookAt` degenerates. */\nexport const MAX_PITCH = Math.PI / 2 - 0.01;\nconst MIN_DISTANCE = 0.5;\nconst MAX_DISTANCE = 500;\nconst UP: Vec3 = [0, 1, 0];\n\nexport function createCamera(init: Partial<Camera3d>): Camera3d {\n return {\n target: init.target ?? [0, 0, 0],\n yaw: init.yaw ?? 0,\n pitch: init.pitch ?? 0,\n distance: init.distance ?? 10,\n fov: init.fov ?? Math.PI / 4,\n near: init.near ?? 0.1,\n far: init.far ?? 1000,\n };\n}\n\nexport function cameraEye(camera: Camera3d): Vec3 {\n const { target, yaw, pitch, distance } = camera;\n const horizontal = Math.cos(pitch) * distance;\n return [\n target[0] + horizontal * Math.sin(yaw),\n target[1] + Math.sin(pitch) * distance,\n target[2] + horizontal * Math.cos(yaw),\n ];\n}\n\nexport function cameraView(camera: Camera3d): Mat4 {\n return lookAt(cameraEye(camera), camera.target, UP);\n}\n\nexport function cameraViewProjection(camera: Camera3d, aspect: number): Mat4 {\n return multiply(perspective(camera.fov, aspect, camera.near, camera.far), cameraView(camera));\n}\n\nexport function orbitBy(camera: Camera3d, deltaYaw: number, deltaPitch: number): Camera3d {\n return {\n ...camera,\n yaw: camera.yaw + deltaYaw,\n pitch: Math.max(-MAX_PITCH, Math.min(MAX_PITCH, camera.pitch + deltaPitch)),\n };\n}\n\nexport function dollyBy(camera: Camera3d, factor: number): Camera3d {\n return {\n ...camera,\n distance: Math.max(MIN_DISTANCE, Math.min(MAX_DISTANCE, camera.distance * factor)),\n };\n}\n","/**\n * A 3D pose, and the two things every consumer of one needs: its matrix, and\n * the world box it occupies.\n *\n * `Scene<TData, TLayer, Pose3>` is the whole of how this reaches weasel —\n * `Scene` is generic over its pose and holds this one with no adapter, no\n * subclass and no special case.\n */\n\nimport {\n compose, quatIdentity, transformAabb, type Aabb, type Mat4, type Quat, type Vec3,\n} from '@weasel-js/geom/3d';\n\nexport interface Pose3 {\n position: Vec3;\n rotation: Quat;\n scale: Vec3;\n}\n\n/** The box a primitive occupies before its pose. A unit primitive centred on\n * its own origin — which is what the kernel assumes nothing about — is\n * `UNIT_CUBE`. */\nexport const UNIT_CUBE: Aabb = { min: [-0.5, -0.5, -0.5], max: [0.5, 0.5, 0.5] };\n\nexport function pose3(\n position: Vec3,\n scale: Vec3 = [1, 1, 1],\n rotation: Quat = quatIdentity(),\n): Pose3 {\n return { position, rotation, scale };\n}\n\nexport function poseMatrix(pose: Pose3): Mat4 {\n return compose(pose.position, pose.rotation, pose.scale);\n}\n\n/**\n * The world box a posed primitive occupies, given the box it occupies in its\n * own space.\n *\n * Which local box a primitive has is the consumer's answer, not the kernel's:\n * a sphere's is rotation-invariant and a mesh's comes from its vertices, and\n * neither is something a pose can tell you.\n */\nexport function aabbOfPose(pose: Pose3, local: Aabb = UNIT_CUBE): Aabb {\n return transformAabb(poseMatrix(pose), local);\n}\n","/**\n * The screen side of the kernel: where a pointer points, and what rectangle a\n * world box covers. This is the seam weasel's 2D chrome meets — everything\n * here speaks `Bounds`-shaped screen rectangles, which is why a 3D host can\n * pass the dispatcher's world point through unchanged.\n */\n\nimport {\n invert,\n normalize,\n sub,\n transformPoint,\n transformPoint4,\n type Aabb,\n type Mat4,\n type Ray,\n type Vec3,\n} from '@weasel-js/geom/3d';\n\n/** A pane's rectangle in CSS pixels, relative to the client. */\nexport interface ViewportRect {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/** A screen rectangle in the shape core's `Bounds` uses. */\nexport interface ScreenBox {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** A `ScreenBox` a host is being asked to outline. `tint` says why: warm for\n * what is selected, cool for what the gesture in hand is proposing. */\nexport interface ChromeBox extends ScreenBox {\n tint?: 'selection' | 'gesture';\n}\n\nconst EPS = 1e-9;\n\n/** Screen point (y down, rect-relative) to normalized device coordinates (y up). */\nexport function screenToNdc(\n point: { x: number; y: number },\n rect: ViewportRect,\n): { x: number; y: number } {\n return {\n x: ((point.x - rect.x) / rect.w) * 2 - 1,\n y: 1 - ((point.y - rect.y) / rect.h) * 2,\n };\n}\n\nexport function ndcToScreen(\n ndc: { x: number; y: number },\n rect: ViewportRect,\n): { x: number; y: number } {\n return {\n x: rect.x + (ndc.x * 0.5 + 0.5) * rect.w,\n y: rect.y + (1 - (ndc.y * 0.5 + 0.5)) * rect.h,\n };\n}\n\n/**\n * The ray a screen point names, given the camera that drew the rect. This is\n * the whole of what 3D picking needs from a pointer — which is why the world\n * point the dispatcher carries stays two numbers.\n */\nexport function rayThroughScreenPoint(\n point: { x: number; y: number },\n rect: ViewportRect,\n viewProjection: Mat4,\n eye: Vec3,\n): Ray {\n const inv = invert(viewProjection);\n if (!inv) return { origin: eye, direction: [0, 0, -1] };\n const ndc = screenToNdc(point, rect);\n const near = transformPoint(inv, [ndc.x, ndc.y, -1]);\n const far = transformPoint(inv, [ndc.x, ndc.y, 1]);\n return { origin: eye, direction: normalize(sub(far, near)) };\n}\n\ntype Clip = readonly [number, number, number, number];\n\n/** Signed distance to the near plane in clip space; `>= 0` is in front of it.\n * GL puts the near plane at `z = -w`. */\nfunction nearSide(p: Clip): number {\n return p[2] + p[3];\n}\n\nfunction lerpClip(a: Clip, b: Clip, t: number): Clip {\n return [\n a[0] + (b[0] - a[0]) * t,\n a[1] + (b[1] - a[1]) * t,\n a[2] + (b[2] - a[2]) * t,\n a[3] + (b[3] - a[3]) * t,\n ];\n}\n\n/** The three edges leaving corner `i` toward a higher bit, as bit masks. */\nconst EDGE_BITS = [1, 2, 4] as const;\n\n/**\n * The screen rectangle a world-space box covers — the currency weasel's chrome\n * and `PoseDescriptor.getBounds` speak in. `null` when the box is entirely\n * behind the near plane.\n *\n * Each of the twelve edges is clipped against the near plane before it is\n * projected, rather than dropping the corners behind it. Dropping them reports\n * a box that is too small for anything straddling the near plane, and the error\n * grows as the camera moves in: a solid the pointer is inside of would outline\n * as a sliver, or as nothing.\n */\nexport function projectAabbToScreen(\n box: Aabb,\n viewProjection: Mat4,\n rect: ViewportRect,\n): ScreenBox | null {\n const corners: Clip[] = [];\n for (let i = 0; i < 8; i++) {\n const corner: Vec3 = [\n i & 1 ? box.max[0] : box.min[0],\n i & 2 ? box.max[1] : box.min[1],\n i & 4 ? box.max[2] : box.min[2],\n ];\n corners.push(transformPoint4(viewProjection, corner));\n }\n\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n let any = false;\n\n const extend = (p: Clip): void => {\n if (p[3] <= EPS) return;\n const screen = ndcToScreen({ x: p[0] / p[3], y: p[1] / p[3] }, rect);\n minX = Math.min(minX, screen.x);\n minY = Math.min(minY, screen.y);\n maxX = Math.max(maxX, screen.x);\n maxY = Math.max(maxY, screen.y);\n any = true;\n };\n\n for (let i = 0; i < 8; i++) {\n for (const bit of EDGE_BITS) {\n if (i & bit) continue;\n let a = corners[i];\n let b = corners[i | bit];\n const da = nearSide(a);\n const db = nearSide(b);\n if (da < 0 && db < 0) continue;\n if (da < 0) a = lerpClip(a, b, da / (da - db));\n else if (db < 0) b = lerpClip(b, a, db / (db - da));\n extend(a);\n extend(b);\n }\n }\n\n if (!any) return null;\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n}\n","/**\n * The kit's deps, implemented against a 3D world.\n *\n * Every one of these takes its camera from a thunk rather than an argument.\n * That is the kernel's central claim in code: the dispatcher keeps handing\n * actions two numbers, and what makes them mean something in 3D is the camera\n * each dep already closes over. `InvocationCtx` needs no widening, and none of\n * these needs a ray passed in.\n *\n * Where a kit contract cannot be honored, the implementation throws rather than\n * guessing. A guess would report a fit the kernel does not have.\n */\n\nimport type {\n AreaSelectDep,\n InsertDep,\n InsertExtras,\n NodeAtPointDep,\n NodeId,\n PoseDescriptor,\n Scene,\n SceneNode,\n SnapDep,\n} from '@weasel-js/core';\nimport {\n intersectRayAabb,\n intersectRayPlane,\n sub,\n type Aabb,\n type Ray,\n type Vec3,\n} from '@weasel-js/geom/3d';\nimport { cameraEye, cameraViewProjection, type Camera3d } from './camera';\nimport { aabbOfPose, type Pose3 } from './pose3';\nimport {\n projectAabbToScreen,\n rayThroughScreenPoint,\n type ScreenBox,\n type ViewportRect,\n} from './screen';\n\nexport interface Viewport3d {\n camera: Camera3d;\n width: number;\n height: number;\n /**\n * Where the pane sits in client coordinates. A 3D host works in one space\n * from the event to the ray — it passes the dispatcher an identity\n * `clientToWorld` — so the deps subtract this rather than the dispatcher.\n */\n originX?: number;\n originY?: number;\n}\n\nexport type ViewportSource = () => Viewport3d;\n\n/** A 3D scene is core's scene with a `Pose3`. Nothing wraps it. */\nexport type Scene3d<TData, TLayer extends string> = Scene<TData, TLayer, Pose3>;\nexport type Node3d<TData, TLayer extends string> = SceneNode<TData, TLayer, Pose3>;\n\n/**\n * The world box a node occupies. The kernel cannot answer this, and the reason\n * is sharper than \"it does not know the mesh\": a sphere's box is the same under\n * every rotation, and no transform of a local box reproduces that — rotate a\n * cube and it widens. Asking for the world box directly is what lets a\n * primitive answer for its own symmetry.\n *\n * Omitted, a node is a unit cube carried by its pose.\n */\nexport type NodeBounds<TData, TLayer extends string> = (\n node: Node3d<TData, TLayer>,\n) => Aabb;\n\nexport interface World3d<TData, TLayer extends string> {\n scene: Scene3d<TData, TLayer>;\n viewport: ViewportSource;\n bounds?: NodeBounds<TData, TLayer>;\n /** The axis gestures resolve against. Default `+y`. */\n up?: Vec3;\n}\n\nconst DEFAULT_UP: Vec3 = [0, 1, 0];\n\nfunction boundsOfNode<TData, TLayer extends string>(\n world: World3d<TData, TLayer>,\n node: Node3d<TData, TLayer>,\n): Aabb {\n return world.bounds ? world.bounds(node) : aabbOfPose(node.pose);\n}\n\n/** The pane, in the coordinate space the host's points arrive in. */\nfunction rectOf(viewport: Viewport3d): ViewportRect {\n return {\n x: viewport.originX ?? 0,\n y: viewport.originY ?? 0,\n w: viewport.width,\n h: viewport.height,\n };\n}\n\nfunction viewProjectionOf(viewport: Viewport3d) {\n return cameraViewProjection(viewport.camera, viewport.width / Math.max(1, viewport.height));\n}\n\nfunction rayAt(viewport: Viewport3d, point: { x: number; y: number }): Ray {\n return rayThroughScreenPoint(\n point,\n rectOf(viewport),\n viewProjectionOf(viewport),\n cameraEye(viewport.camera),\n );\n}\n\nfunction pointOnRay(ray: Ray, t: number): Vec3 {\n return [\n ray.origin[0] + ray.direction[0] * t,\n ray.origin[1] + ray.direction[1] * t,\n ray.origin[2] + ray.direction[2] * t,\n ];\n}\n\nfunction overlaps(a: ScreenBox, b: ScreenBox): boolean {\n return (\n a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y\n );\n}\n\n/** The screen box a node covers, or null when it projects to nothing visible. */\nexport function screenBoxOf<TData, TLayer extends string>(\n world: World3d<TData, TLayer>,\n id: NodeId,\n viewport: Viewport3d,\n): ScreenBox | null {\n const node = world.scene.get(id) as Node3d<TData, TLayer> | undefined;\n if (!node) return null;\n return projectAabbToScreen(\n boundsOfNode(world, node),\n viewProjectionOf(viewport),\n rectOf(viewport),\n );\n}\n\n/**\n * Nearest node along the ray the screen point names.\n *\n * The kit calls this with what it believes is a world point; a 3D host feeds\n * the dispatcher an identity `clientToWorld`, so it is the pane-relative screen\n * point, which is exactly what a ray needs.\n */\nexport function createNodeAtPoint<TData, TLayer extends string>(\n world: World3d<TData, TLayer>,\n): NodeAtPointDep {\n return (point, exclude) => {\n const excluded = exclude ? new Set(exclude) : null;\n const vp = world.viewport();\n const ray = rayAt(vp, point);\n let best: NodeId | null = null;\n let bestT = Infinity;\n for (const node of world.scene.renderOrderNodes() as readonly Node3d<TData, TLayer>[]) {\n if (excluded?.has(node.id)) continue;\n const { min, max } = boundsOfNode(world, node);\n const t = intersectRayAabb(ray, min, max);\n if (t !== null && t < bestT) {\n bestT = t;\n best = node.id;\n }\n }\n return best;\n };\n}\n\n/** Marquee: the nodes whose projected box the screen rectangle touches. */\nexport function createAreaSelect<TData, TLayer extends string>(\n world: World3d<TData, TLayer>,\n selection: { get(): readonly NodeId[]; set(ids: NodeId[]): void },\n): AreaSelectDep {\n return {\n hitTestArea(bounds) {\n const vp = world.viewport();\n const hits: NodeId[] = [];\n for (const node of world.scene.renderOrderNodes()) {\n const box = screenBoxOf(world, node.id, vp);\n if (box && overlaps(box, bounds)) hits.push(node.id);\n }\n return hits;\n },\n getSelection: () => [...selection.get()],\n setSelection: (ids) => selection.set([...ids]),\n };\n}\n\n/** No grid in 3D yet; the identity keeps `insertAction` happy without lying. */\nexport function createSnap(): SnapDep {\n return { point: (p) => p };\n}\n\n/** The ground-plane rectangle a drag swept out, in world units. */\nexport interface Footprint {\n /** Centre of the swept rectangle, on the ground plane. */\n center: Vec3;\n /** Extent along the two axes the up axis is not. */\n width: number;\n depth: number;\n}\n\n/**\n * A drag rectangle becomes a footprint on the ground plane: its four corners\n * are cast onto the plane and the hit points give the extent. What node that\n * implies is the consumer's answer — the kernel does not know what it is\n * inserting.\n *\n * The kit's `InsertDep.commit` hands over a screen-space AABB and nothing else,\n * which turns out to be enough: the depth the 2D contract cannot express comes\n * from the plane, not from the caller.\n */\nexport function createInsert(opts: {\n viewport: ViewportSource;\n up?: Vec3;\n /** Smallest footprint a click-sized drag produces. Default 0.25. */\n minExtent?: number;\n mint(footprint: Footprint, extras: InsertExtras): NodeId | null;\n}): InsertDep {\n const up = opts.up ?? DEFAULT_UP;\n const minExtent = opts.minExtent ?? 0.25;\n // The two axes the up axis is not, so a footprint reads the same whichever\n // way up the world is.\n const axes: [number, number] = up[1] !== 0 ? [0, 2] : up[0] !== 0 ? [1, 2] : [0, 1];\n\n return {\n commit(bounds, extras) {\n const vp = opts.viewport();\n const corners = [\n { x: bounds.x, y: bounds.y },\n { x: bounds.x + bounds.width, y: bounds.y },\n { x: bounds.x, y: bounds.y + bounds.height },\n { x: bounds.x + bounds.width, y: bounds.y + bounds.height },\n ];\n\n const hits: Vec3[] = [];\n for (const corner of corners) {\n const ray = rayAt(vp, corner);\n const t = intersectRayPlane(ray, up, 0);\n if (t !== null) hits.push(pointOnRay(ray, t));\n }\n if (hits.length === 0) return null;\n\n const [u, v] = axes;\n let minU = Infinity, maxU = -Infinity, minV = Infinity, maxV = -Infinity;\n for (const h of hits) {\n minU = Math.min(minU, h[u]); maxU = Math.max(maxU, h[u]);\n minV = Math.min(minV, h[v]); maxV = Math.max(maxV, h[v]);\n }\n\n const center: [number, number, number] = [0, 0, 0];\n center[u] = (minU + maxU) / 2;\n center[v] = (minV + maxV) / 2;\n return opts.mint(\n {\n center,\n width: Math.max(minExtent, maxU - minU),\n depth: Math.max(minExtent, maxV - minV),\n },\n extras,\n );\n },\n };\n}\n\n/**\n * `PoseDescriptor` over a 3D pose, with `Bounds` read as the screen box the\n * node covers. Chrome and marquee math work unchanged; the two methods that\n * run the other way cannot.\n *\n * `forNode` is what lets a sphere bound itself as a sphere: every node is a\n * `Pose3`, so the shape lives on `node.data` and nothing but the node can say\n * which one this is. Unspecialized — the kit calling with only a pose in hand —\n * it answers with the default local box, which is the looser of the two.\n *\n * The contract mismatch that remains, recorded in the kernel doc: a screen\n * rectangle does not name a 3D pose without a depth, so `remapBounds` and\n * `fromBounds` have no honest answer.\n */\nexport function createPoseDescriptor<TData, TLayer extends string>(\n world: World3d<TData, TLayer>,\n): PoseDescriptor<Pose3, Node3d<TData, TLayer>> {\n const up = world.up ?? DEFAULT_UP;\n const upAxis = up[1] !== 0 ? 1 : up[0] !== 0 ? 0 : 2;\n const specialized = new WeakMap<object, PoseDescriptor<Pose3, Node3d<TData, TLayer>>>();\n\n function build(worldBox: (pose: Pose3) => Aabb): PoseDescriptor<Pose3, Node3d<TData, TLayer>> {\n const boundsOf = (pose: Pose3) => {\n const vp = world.viewport();\n return projectAabbToScreen(worldBox(pose), viewProjectionOf(vp), rectOf(vp));\n };\n\n const descriptor: PoseDescriptor<Pose3, Node3d<TData, TLayer>> = {\n forNode(node) {\n let d = specialized.get(node);\n if (!d) {\n // The pose arriving here is often an in-flight override rather than\n // `node.pose`, so the consumer is asked about that pose on this node\n // rather than about the node as committed.\n d = build((pose) => boundsOfNode(world, { ...node, pose }));\n specialized.set(node, d);\n }\n return d;\n },\n\n getBounds(pose) {\n return boundsOf(pose) ?? { x: 0, y: 0, width: 0, height: 0 };\n },\n\n remapBounds() {\n throw new Error(\n 'kernel3d: remapBounds has no 3D answer — a screen rectangle does not name a pose without a depth.',\n );\n },\n\n fromBounds() {\n throw new Error(\n 'kernel3d: fromBounds has no 3D answer — a screen rectangle does not name a pose without a depth.',\n );\n },\n\n /**\n * Screen-space deltas resolve against the plane the node already sits on,\n * so the two numbers the kit passes are enough: the camera that turns\n * them into a world position is the one this dep closed over.\n */\n translate(pose, dx, dy) {\n const vp = world.viewport();\n const box = boundsOf(pose);\n if (!box) return pose;\n const from = { x: box.x + box.width / 2, y: box.y + box.height / 2 };\n const to = { x: from.x + dx, y: from.y + dy };\n const offset = pose.position[upAxis];\n\n const ray = rayAt(vp, to);\n const t = intersectRayPlane(ray, up, offset);\n if (t === null) return pose;\n\n const anchorRay = rayAt(vp, from);\n const anchorT = intersectRayPlane(anchorRay, up, offset);\n if (anchorT === null) return pose;\n\n const delta = sub(pointOnRay(ray, t), pointOnRay(anchorRay, anchorT));\n const position: [number, number, number] = [\n pose.position[0] + delta[0],\n pose.position[1] + delta[1],\n pose.position[2] + delta[2],\n ];\n position[upAxis] = pose.position[upAxis];\n return { ...pose, position };\n },\n\n intersectsRect(pose, rect) {\n const box = boundsOf(pose);\n return box ? overlaps(box, rect) : false;\n },\n\n supportsRotation() {\n return false;\n },\n };\n\n return descriptor;\n }\n\n return build((pose) => aabbOfPose(pose));\n}\n","/**\n * What a host paints for a gesture that proposes chrome rather than a pose.\n *\n * A marquee sweep and a box-drag both publish an `OngoingOverlay`, which used\n * to reach paint only through core's own 2D layer. `resolveOverlays` hands back\n * world geometry with no renderer in it, so the same call `<SceneCanvas>` makes\n * serves a consumer-owned 3D renderer.\n *\n * A 3D host's `clientToWorld` is identity, so an overlay's world bounds are the\n * client rect the deps already work in, and the pane origin comes off exactly\n * once. Only the rectangular variants resolve here: a lasso, a pencil and a run\n * of points are paths, and a path is geometry this cannot flatten to a box.\n */\n\nimport { resolveOverlays, type OngoingHandle } from '@weasel-js/core';\nimport type { ChromeBox } from './screen';\n\n/** Gesture chrome, in CSS pixels relative to the pane's top-left. */\nexport function collectOverlayBoxes(\n handles: Iterable<OngoingHandle>,\n origin: { x: number; y: number },\n): ChromeBox[] {\n const overlays = [];\n for (const handle of handles) {\n const ov = handle.overlay?.();\n if (ov) overlays.push(ov);\n }\n\n const boxes: ChromeBox[] = [];\n for (const resolved of resolveOverlays(overlays)) {\n if (resolved.kind !== 'marquee' && resolved.kind !== 'insertPreview') continue;\n const { x, y, width, height } = resolved.bounds;\n boxes.push({ x: x - origin.x, y: y - origin.y, width, height, tint: 'gesture' });\n }\n return boxes;\n}\n","/**\n * Camera tools and their actions.\n *\n * Shape tools transfer from the kit untouched — a 3D host mounts core's own\n * `useSelectTool` and a rectangle-style insert binding and neither notices the\n * dimension. Only the viewport tools are written here, which is the one\n * exception the kernel doc predicted: `useHandTool` routes to\n * `viewport.dragPan`, and that wants the 2D `view`.\n */\n\nimport { defineTool, type Action, type Tool } from '@weasel-js/core';\nimport { dollyBy, orbitBy, type Camera3d } from './camera';\nimport type { Camera3dDep } from './cameraDep';\n\n/** Radians per CSS pixel of drag. A full pane width is a bit over half a turn. */\nconst ORBIT_RATE = 0.006;\nconst DOLLY_RATE = 0.0015;\n\nfunction cameraDep(deps: unknown): Camera3dDep | undefined {\n return (deps as { camera3d?: Camera3dDep }).camera3d;\n}\n\nexport const orbitAction: Action = {\n id: 'camera.orbit',\n label: 'Orbit',\n requires: ['camera3d'],\n invoker: {\n timing: 'ongoing',\n start(ctx) {\n const dep = cameraDep(ctx.deps);\n const origin: Camera3d | undefined = dep?.get();\n return {\n kind: 'orbit',\n onMove(move) {\n if (!dep || !origin) return;\n // `drag.delta` runs from the drag origin, not the previous move, so\n // this composes against the camera as it was when the drag began.\n const delta = move.drag?.screenDelta ?? move.drag?.delta;\n if (!delta) return;\n dep.set(orbitBy(origin, -delta.x * ORBIT_RATE, delta.y * ORBIT_RATE));\n },\n };\n },\n },\n};\n\nexport const dollyAction: Action = {\n id: 'camera.dolly',\n label: 'Dolly',\n group: 'viewport',\n // Wheel, in any tool — the dispatcher merges the event's deltas into params.\n defaultBinding: [{ spec: { kind: 'wheel' }, opts: {} }],\n requires: ['camera3d'],\n invoker: {\n timing: 'immediate',\n run(deps, params) {\n const dep = cameraDep(deps);\n if (!dep) return;\n const amount = typeof params?.deltaY === 'number' ? params.deltaY : 0;\n dep.set(dollyBy(dep.get(), Math.exp(amount * DOLLY_RATE)));\n },\n },\n};\n\nexport function useOrbitTool(): Tool<null> {\n return defineTool<null>({\n id: 'orbit',\n hookName: 'useOrbitTool',\n cursor: 'grab',\n presentation: { label: 'Orbit', group: 'viewport' },\n bindings: [{ spec: { kind: 'drag' }, actionId: 'camera.orbit' }],\n });\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weasel-js/kernel3d",
|
|
3
|
+
"version": "1.4.4",
|
|
4
|
+
"description": "3D kernel for weasel: poses, orbit camera, ray picking and screen-projected chrome geometry over core's scene graph and dispatcher. Hosts a renderer rather than owning one.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"author": "orochi235",
|
|
19
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
23
|
+
"directory": "packages/kernel3d"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@weasel-js/geom": "1.4.4"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@weasel-js/core": "1.4.4"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"provenance": true
|
|
48
|
+
}
|
|
49
|
+
}
|