@weasel-js/kernel3d 1.10.1 → 1.11.1
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/README.md +12 -0
- package/dist/index.d.ts +218 -42
- package/dist/index.js +576 -103
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { lookAt, multiply, perspective, quatIdentity, compose, transformAabb, invert, transformPoint, normalize, sub, transformPoint4, intersectRayAabb,
|
|
2
|
-
import { resolveOverlays, defineTool } from '@weasel-js/core';
|
|
1
|
+
import { lookAt, multiply, perspective, add, scale, quatIdentity, compose, transformAabb, invert, transformPoint, normalize, sub, transformPoint4, intersectRayAabb, distance, dot, len, cross, intersectRayPlane, pointOnRay, aabbOfPoints } from '@weasel-js/geom/3d';
|
|
2
|
+
import { createSnapOutcomeStore, assertSnapSourceIds, DEFAULT_SNAP_PX, overlaysOf, resolveOverlays, defineTool, snapSourcesFor, resolvedSnapMarks, SNAP_AXIS_STEP, gatherSnapCandidates } from '@weasel-js/core';
|
|
3
|
+
import { isDev } from '@weasel-js/react';
|
|
4
|
+
import { closestOnFlat, resolveSnap, stepLine } from '@weasel-js/geom/nd';
|
|
3
5
|
import { useMemo } from 'react';
|
|
6
|
+
import { pref } from '@weasel-js/prefs';
|
|
4
7
|
|
|
5
8
|
// src/camera.ts
|
|
6
9
|
var MAX_PITCH = Math.PI / 2 - 0.01;
|
|
7
|
-
var MIN_DISTANCE = 0.5;
|
|
8
|
-
var MAX_DISTANCE = 500;
|
|
9
10
|
var UP = { x: 0, y: 1, z: 0 };
|
|
10
11
|
function createCamera(init) {
|
|
11
12
|
return {
|
|
@@ -19,11 +20,11 @@ function createCamera(init) {
|
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
function cameraEye(camera) {
|
|
22
|
-
const { target, yaw, pitch, distance } = camera;
|
|
23
|
-
const horizontal = Math.cos(pitch) *
|
|
23
|
+
const { target, yaw, pitch, distance: distance2 } = camera;
|
|
24
|
+
const horizontal = Math.cos(pitch) * distance2;
|
|
24
25
|
return {
|
|
25
26
|
x: target.x + horizontal * Math.sin(yaw),
|
|
26
|
-
y: target.y + Math.sin(pitch) *
|
|
27
|
+
y: target.y + Math.sin(pitch) * distance2,
|
|
27
28
|
z: target.z + horizontal * Math.cos(yaw)
|
|
28
29
|
};
|
|
29
30
|
}
|
|
@@ -33,25 +34,59 @@ function cameraView(camera) {
|
|
|
33
34
|
function cameraViewProjection(camera, aspect) {
|
|
34
35
|
return multiply(perspective(camera.fov, aspect, camera.near, camera.far), cameraView(camera));
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
var DEFAULT_ORBIT_LIMITS = {
|
|
38
|
+
minDistance: 0.5,
|
|
39
|
+
maxDistance: 500,
|
|
40
|
+
minPitch: -MAX_PITCH,
|
|
41
|
+
maxPitch: MAX_PITCH
|
|
42
|
+
};
|
|
43
|
+
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
44
|
+
function clampCamera(camera, limits = {}) {
|
|
45
|
+
const l = { ...DEFAULT_ORBIT_LIMITS, ...limits };
|
|
46
|
+
const pitch = clamp(camera.pitch, Math.max(-MAX_PITCH, l.minPitch), Math.min(MAX_PITCH, l.maxPitch));
|
|
47
|
+
const distance2 = clamp(camera.distance, l.minDistance, l.maxDistance);
|
|
48
|
+
if (pitch === camera.pitch && distance2 === camera.distance) return camera;
|
|
49
|
+
return { ...camera, pitch, distance: distance2 };
|
|
50
|
+
}
|
|
51
|
+
function orbitBy(camera, deltaYaw, deltaPitch, limits) {
|
|
52
|
+
return clampCamera(
|
|
53
|
+
{ ...camera, yaw: camera.yaw + deltaYaw, pitch: camera.pitch + deltaPitch },
|
|
54
|
+
limits
|
|
55
|
+
);
|
|
42
56
|
}
|
|
43
|
-
function dollyBy(camera, factor) {
|
|
44
|
-
return {
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
function dollyBy(camera, factor, limits) {
|
|
58
|
+
return clampCamera({ ...camera, distance: camera.distance * factor }, limits);
|
|
59
|
+
}
|
|
60
|
+
function panBy(camera, dx, dy, paneHeight) {
|
|
61
|
+
if (!(paneHeight > 0)) return camera;
|
|
62
|
+
const perPixel = 2 * camera.distance * Math.tan(camera.fov / 2) / paneHeight;
|
|
63
|
+
const { yaw, pitch } = camera;
|
|
64
|
+
const right = { x: Math.cos(yaw), y: 0, z: -Math.sin(yaw) };
|
|
65
|
+
const up = {
|
|
66
|
+
x: -Math.sin(yaw) * Math.sin(pitch),
|
|
67
|
+
y: Math.cos(pitch),
|
|
68
|
+
z: -Math.cos(yaw) * Math.sin(pitch)
|
|
47
69
|
};
|
|
70
|
+
const shift = add(scale(right, -dx * perPixel), scale(up, dy * perPixel));
|
|
71
|
+
return { ...camera, target: add(camera.target, shift) };
|
|
72
|
+
}
|
|
73
|
+
function dollyToward(camera, factor, at, pane, limits) {
|
|
74
|
+
const dollied = dollyBy(camera, factor, limits);
|
|
75
|
+
const kept = 1 - dollied.distance / camera.distance;
|
|
76
|
+
const moved = panBy(
|
|
77
|
+
camera,
|
|
78
|
+
-kept * (at.x - pane.width / 2),
|
|
79
|
+
-kept * (at.y - pane.height / 2),
|
|
80
|
+
pane.height
|
|
81
|
+
);
|
|
82
|
+
return { ...dollied, target: moved.target };
|
|
48
83
|
}
|
|
49
84
|
var UNIT_CUBE = {
|
|
50
85
|
min: { x: -0.5, y: -0.5, z: -0.5 },
|
|
51
86
|
max: { x: 0.5, y: 0.5, z: 0.5 }
|
|
52
87
|
};
|
|
53
|
-
function pose3(position,
|
|
54
|
-
return { position, rotation, scale };
|
|
88
|
+
function pose3(position, scale5 = { x: 1, y: 1, z: 1 }, rotation = quatIdentity()) {
|
|
89
|
+
return { position, rotation, scale: scale5 };
|
|
55
90
|
}
|
|
56
91
|
function poseMatrix(pose) {
|
|
57
92
|
return compose(pose.position, pose.rotation, pose.scale);
|
|
@@ -134,10 +169,6 @@ function projectAabbToScreen(box, viewProjection, rect) {
|
|
|
134
169
|
if (!any) return null;
|
|
135
170
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
136
171
|
}
|
|
137
|
-
var DEFAULT_UP = { x: 0, y: 1, z: 0 };
|
|
138
|
-
function boundsOfNode(world, node) {
|
|
139
|
-
return world.bounds ? world.bounds(node) : aabbOfPose(node.pose);
|
|
140
|
-
}
|
|
141
172
|
function rectOf(viewport) {
|
|
142
173
|
return {
|
|
143
174
|
x: viewport.originX ?? 0,
|
|
@@ -157,6 +188,189 @@ function rayAt(viewport, point) {
|
|
|
157
188
|
cameraEye(viewport.camera)
|
|
158
189
|
);
|
|
159
190
|
}
|
|
191
|
+
function projectPoint(p, viewport) {
|
|
192
|
+
const [x, y, z, w] = transformPoint4(viewProjectionOf(viewport), p);
|
|
193
|
+
if (!(w > 0) || z < -w) return null;
|
|
194
|
+
const screen = ndcToScreen({ x: x / w, y: y / w }, rectOf(viewport));
|
|
195
|
+
return Number.isFinite(screen.x) && Number.isFinite(screen.y) ? screen : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/world.ts
|
|
199
|
+
var DEFAULT_UP = { x: 0, y: 1, z: 0 };
|
|
200
|
+
function boundsOfNode(world, node) {
|
|
201
|
+
return world.bounds ? world.bounds(node) : aabbOfPose(node.pose);
|
|
202
|
+
}
|
|
203
|
+
var ZERO = { x: 0, y: 0, z: 0 };
|
|
204
|
+
var tuple = (p) => [p.x, p.y, p.z];
|
|
205
|
+
var vec3Of = (t) => ({ x: t[0], y: t[1], z: t[2] });
|
|
206
|
+
function planeBasis(up) {
|
|
207
|
+
const n = normalize(up);
|
|
208
|
+
const e = Math.abs(n.x) < 0.9 ? { x: 1, y: 0, z: 0 } : { x: 0, y: 0, z: 1 };
|
|
209
|
+
const u = normalize(sub(e, scale(n, dot(n, e))));
|
|
210
|
+
return [u, cross(u, n)];
|
|
211
|
+
}
|
|
212
|
+
function inputFlat3d(c, held, basis, delta0, about) {
|
|
213
|
+
const [u, v] = basis;
|
|
214
|
+
if (!held || c === void 0 || c === "aspect") return { at: [0, 0, 0], dirs: [tuple(u), tuple(v)] };
|
|
215
|
+
const step = c === "axis" ? SNAP_AXIS_STEP : c.step;
|
|
216
|
+
return stepLine(tuple(c === "axis" ? ZERO : about), tuple(delta0), [tuple(u), tuple(v)], step);
|
|
217
|
+
}
|
|
218
|
+
function subjectOf(world, ids, up) {
|
|
219
|
+
let box = null;
|
|
220
|
+
let anchor = null;
|
|
221
|
+
for (const id of ids) {
|
|
222
|
+
const node = world.scene.get(id);
|
|
223
|
+
if (!node) continue;
|
|
224
|
+
const b = boundsOfNode(world, node);
|
|
225
|
+
anchor ??= { box: b, height: dot(up, node.pose.position) };
|
|
226
|
+
box = box ? union(box, b) : b;
|
|
227
|
+
}
|
|
228
|
+
return box && anchor ? { box, anchor } : null;
|
|
229
|
+
}
|
|
230
|
+
function union(a, b) {
|
|
231
|
+
return {
|
|
232
|
+
min: { x: Math.min(a.min.x, b.min.x), y: Math.min(a.min.y, b.min.y), z: Math.min(a.min.z, b.min.z) },
|
|
233
|
+
max: { x: Math.max(a.max.x, b.max.x), y: Math.max(a.max.y, b.max.y), z: Math.max(a.max.z, b.max.z) }
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function boxCorner(box, i) {
|
|
237
|
+
return {
|
|
238
|
+
x: i & 1 ? box.max.x : box.min.x,
|
|
239
|
+
y: i & 2 ? box.max.y : box.min.y,
|
|
240
|
+
z: i & 4 ? box.max.z : box.min.z
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function boxProbes(box) {
|
|
244
|
+
const center = scale(add(box.min, box.max), 0.5);
|
|
245
|
+
const out = [];
|
|
246
|
+
for (let i = 0; i < 8; i++) {
|
|
247
|
+
out.push({ id: `corner:${i}`, at: boxCorner(box, i), role: "geometry" });
|
|
248
|
+
}
|
|
249
|
+
out.push(
|
|
250
|
+
{ id: "center", at: center, role: "geometry" },
|
|
251
|
+
{ id: "box.min", at: box.min, role: "box" },
|
|
252
|
+
{ id: "box.center", at: center, role: "box" },
|
|
253
|
+
{ id: "box.max", at: box.max, role: "box" }
|
|
254
|
+
);
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/snap/session3d.ts
|
|
259
|
+
var ZERO2 = { x: 0, y: 0, z: 0 };
|
|
260
|
+
function createSession3d(inputs) {
|
|
261
|
+
const { req, world, store } = inputs;
|
|
262
|
+
const up = normalize(world.up ?? DEFAULT_UP);
|
|
263
|
+
const basis = planeBasis(up);
|
|
264
|
+
const frame = frameOf(req, world, up);
|
|
265
|
+
const sources = snapSourcesFor(inputs.sources, req.gesture);
|
|
266
|
+
const unsnapped = (p) => {
|
|
267
|
+
store.set([]);
|
|
268
|
+
return { point: p, held: false, marks: [] };
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
at(p, mods) {
|
|
272
|
+
if (!frame) return unsnapped(p);
|
|
273
|
+
const vp = world.viewport();
|
|
274
|
+
const screen = frame.anchor(vp);
|
|
275
|
+
const anchor = screen && castOnto(vp, screen, up, frame.offset);
|
|
276
|
+
const hit = screen && castOnto(vp, { x: screen.x + p.x - req.press.x, y: screen.y + p.y - req.press.y }, up, frame.offset);
|
|
277
|
+
if (!screen || !anchor || !hit) return unsnapped(p);
|
|
278
|
+
const delta0 = sub(hit, anchor);
|
|
279
|
+
const origin = req.origin ? castOnto(vp, req.origin, up, frame.offset) : null;
|
|
280
|
+
const input = inputFlat3d(req.constrain, mods.constrain, basis, delta0, origin ? sub(origin, anchor) : ZERO2);
|
|
281
|
+
let delta;
|
|
282
|
+
let held = false;
|
|
283
|
+
if (mods.suspend) {
|
|
284
|
+
delta = vec3Of(closestOnFlat(input, tuple(delta0)));
|
|
285
|
+
store.set([]);
|
|
286
|
+
} else {
|
|
287
|
+
const probes = frame.probes(anchor);
|
|
288
|
+
const res = resolveSnap({
|
|
289
|
+
probes: probes.map((q) => ({ id: q.id, at: tuple(q.at), role: q.role })),
|
|
290
|
+
input,
|
|
291
|
+
delta0: tuple(delta0),
|
|
292
|
+
candidates: candidatesOf(inputs, sources, probes, delta0, vp, origin, up),
|
|
293
|
+
toScreen: (w) => {
|
|
294
|
+
const s = projectPoint(vec3Of(w), vp);
|
|
295
|
+
return s ? [s.x, s.y] : [NaN, NaN];
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
delta = vec3Of(res.delta);
|
|
299
|
+
held = res.members.length > 0;
|
|
300
|
+
store.set(resolvedSnapMarks(res.satisfied, delta));
|
|
301
|
+
}
|
|
302
|
+
const landed = projectPoint(add(anchor, delta), vp);
|
|
303
|
+
if (!landed) return unsnapped(p);
|
|
304
|
+
return {
|
|
305
|
+
point: { x: req.press.x + landed.x - screen.x, y: req.press.y + landed.y - screen.y },
|
|
306
|
+
held,
|
|
307
|
+
marks: store.get()
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
angle: (theta) => ({ theta, marks: [] }),
|
|
311
|
+
end() {
|
|
312
|
+
store.set([]);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function frameOf(req, world, up) {
|
|
317
|
+
if (req.gesture === "point") {
|
|
318
|
+
return {
|
|
319
|
+
offset: 0,
|
|
320
|
+
anchor: () => req.press,
|
|
321
|
+
probes: (at) => [{ id: "pointer", at, role: "pointer" }]
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (req.gesture !== "move") return null;
|
|
325
|
+
const subject = subjectOf(world, req.ids ?? [], up);
|
|
326
|
+
if (!subject) return null;
|
|
327
|
+
const probes = boxProbes(subject.box);
|
|
328
|
+
return {
|
|
329
|
+
offset: subject.anchor.height,
|
|
330
|
+
anchor(vp) {
|
|
331
|
+
const box = projectAabbToScreen(subject.anchor.box, viewProjectionOf(vp), rectOf(vp));
|
|
332
|
+
return box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : null;
|
|
333
|
+
},
|
|
334
|
+
probes: () => probes
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function castOnto(vp, s, up, offset) {
|
|
338
|
+
const ray = rayAt(vp, s);
|
|
339
|
+
const t = ray && intersectRayPlane(ray, up, offset);
|
|
340
|
+
return ray && t !== null ? pointOnRay(ray, t) : null;
|
|
341
|
+
}
|
|
342
|
+
function candidatesOf(inputs, sources, probes, delta0, vp, origin, up) {
|
|
343
|
+
const { req } = inputs;
|
|
344
|
+
const shifted = probes.map((q) => ({ ...q, at: add(q.at, delta0) }));
|
|
345
|
+
const perPx = worldPerPx(vp, shifted);
|
|
346
|
+
const found = gatherSnapCandidates(sources, shifted, (_source, offered, reachPx) => ({
|
|
347
|
+
gesture: req.gesture,
|
|
348
|
+
probes: offered,
|
|
349
|
+
delta0,
|
|
350
|
+
exclude: req.ids ?? [],
|
|
351
|
+
...req.ids ? { ids: req.ids } : {},
|
|
352
|
+
...req.parent !== void 0 ? { parent: req.parent } : {},
|
|
353
|
+
...origin ? { origin } : {},
|
|
354
|
+
reachWorld: (px) => px * perPx,
|
|
355
|
+
reachPx
|
|
356
|
+
}), { toVec: tuple, reachPx: inputs.reachPx, ...origin ? { origin } : {} });
|
|
357
|
+
return found.filter((c) => !level(c.locus, up));
|
|
358
|
+
}
|
|
359
|
+
function level(locus, up) {
|
|
360
|
+
if (locus.dirs.length !== 2) return false;
|
|
361
|
+
const n = cross(vec3Of(locus.dirs[0]), vec3Of(locus.dirs[1]));
|
|
362
|
+
return Math.abs(dot(n, up)) >= (1 - 1e-9) * len(n);
|
|
363
|
+
}
|
|
364
|
+
function worldPerPx(vp, probes) {
|
|
365
|
+
const eye = cameraEye(vp.camera);
|
|
366
|
+
const forward = normalize(sub(vp.camera.target, eye));
|
|
367
|
+
const k = 2 * Math.tan(vp.camera.fov / 2) / Math.max(1, vp.height);
|
|
368
|
+
let depth = 0;
|
|
369
|
+
for (const q of probes) depth = Math.max(depth, dot(sub(q.at, eye), forward));
|
|
370
|
+
return depth * k;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/deps.ts
|
|
160
374
|
function viewPlaneAt(camera, p) {
|
|
161
375
|
const normal = normalize(sub(camera.target, cameraEye(camera)));
|
|
162
376
|
return { normal, offset: dot(normal, p) };
|
|
@@ -167,24 +381,6 @@ function onPlane(vp, plane, point) {
|
|
|
167
381
|
const t = intersectRayPlane(ray, plane.normal, plane.offset);
|
|
168
382
|
return t === null ? null : pointOnRay(ray, t);
|
|
169
383
|
}
|
|
170
|
-
function aabbOfPoints(points) {
|
|
171
|
-
const min = { x: Infinity, y: Infinity, z: Infinity };
|
|
172
|
-
const max = { x: -Infinity, y: -Infinity, z: -Infinity };
|
|
173
|
-
for (const p of points) {
|
|
174
|
-
for (const axis of ["x", "y", "z"]) {
|
|
175
|
-
if (p[axis] < min[axis]) min[axis] = p[axis];
|
|
176
|
-
if (p[axis] > max[axis]) max[axis] = p[axis];
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return { min, max };
|
|
180
|
-
}
|
|
181
|
-
function pointOnRay(ray, t) {
|
|
182
|
-
return {
|
|
183
|
-
x: ray.origin.x + ray.direction.x * t,
|
|
184
|
-
y: ray.origin.y + ray.direction.y * t,
|
|
185
|
-
z: ray.origin.z + ray.direction.z * t
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
384
|
function overlaps(a, b) {
|
|
189
385
|
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;
|
|
190
386
|
}
|
|
@@ -232,49 +428,30 @@ function createAreaSelect(world, selection) {
|
|
|
232
428
|
setSelection: (ids) => selection.set([...ids])
|
|
233
429
|
};
|
|
234
430
|
}
|
|
235
|
-
|
|
236
|
-
|
|
431
|
+
var warnedGestureSources = false;
|
|
432
|
+
function warnGestureSources() {
|
|
433
|
+
if (warnedGestureSources || !isDev()) return;
|
|
434
|
+
warnedGestureSources = true;
|
|
435
|
+
console.warn(
|
|
436
|
+
"[kernel3d] a gesture passed snap `sources`, which are 2D and are not asked in a 3D host. List 3D sources in createSnap({ sources })."
|
|
437
|
+
);
|
|
237
438
|
}
|
|
238
|
-
function
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
439
|
+
function createSnap(world, opts = {}) {
|
|
440
|
+
const store = opts.store ?? createSnapOutcomeStore();
|
|
441
|
+
const listed = opts.sources ?? [];
|
|
442
|
+
assertSnapSourceIds(listed);
|
|
242
443
|
return {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!ray) continue;
|
|
255
|
-
const t = intersectRayPlane(ray, up, 0);
|
|
256
|
-
if (t !== null) hits.push(pointOnRay(ray, t));
|
|
257
|
-
}
|
|
258
|
-
if (hits.length === 0) return null;
|
|
259
|
-
const [u, v] = axes;
|
|
260
|
-
let minU = Infinity, maxU = -Infinity, minV = Infinity, maxV = -Infinity;
|
|
261
|
-
for (const h of hits) {
|
|
262
|
-
minU = Math.min(minU, h[u]);
|
|
263
|
-
maxU = Math.max(maxU, h[u]);
|
|
264
|
-
minV = Math.min(minV, h[v]);
|
|
265
|
-
maxV = Math.max(maxV, h[v]);
|
|
266
|
-
}
|
|
267
|
-
const center = { x: 0, y: 0, z: 0 };
|
|
268
|
-
center[u] = (minU + maxU) / 2;
|
|
269
|
-
center[v] = (minV + maxV) / 2;
|
|
270
|
-
return opts.mint(
|
|
271
|
-
{
|
|
272
|
-
center,
|
|
273
|
-
width: Math.max(minExtent, maxU - minU),
|
|
274
|
-
depth: Math.max(minExtent, maxV - minV)
|
|
275
|
-
},
|
|
276
|
-
extras
|
|
277
|
-
);
|
|
444
|
+
begin: (req) => {
|
|
445
|
+
if (req.sources?.length) warnGestureSources();
|
|
446
|
+
const omit = new Set(req.omit);
|
|
447
|
+
const sources = listed.filter((s) => !omit.has(s.id));
|
|
448
|
+
return createSession3d({
|
|
449
|
+
req,
|
|
450
|
+
world,
|
|
451
|
+
sources,
|
|
452
|
+
store,
|
|
453
|
+
reachPx: opts.reachPx ?? opts.kitSettings?.get("targets.snapPx") ?? DEFAULT_SNAP_PX
|
|
454
|
+
});
|
|
278
455
|
}
|
|
279
456
|
};
|
|
280
457
|
}
|
|
@@ -387,7 +564,7 @@ function createPoseDescriptor(world) {
|
|
|
387
564
|
if (!box) return pose;
|
|
388
565
|
const from = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
389
566
|
const to = { x: from.x + dx, y: from.y + dy };
|
|
390
|
-
const offset = pose.position
|
|
567
|
+
const offset = dot(up, pose.position);
|
|
391
568
|
const ray = rayAt(vp, to);
|
|
392
569
|
if (!ray) return pose;
|
|
393
570
|
const t = intersectRayPlane(ray, up, offset);
|
|
@@ -413,12 +590,223 @@ function createPoseDescriptor(world) {
|
|
|
413
590
|
}
|
|
414
591
|
return build((pose) => aabbOfPose(pose));
|
|
415
592
|
}
|
|
593
|
+
function footprintPoints(bounds, ends, cast) {
|
|
594
|
+
if (ends) {
|
|
595
|
+
const start = cast(ends.start);
|
|
596
|
+
const end = cast(ends.end);
|
|
597
|
+
if (!start || !end) return [];
|
|
598
|
+
return ends.fromCenter ? [end, sub(scale(start, 2), end)] : [start, end];
|
|
599
|
+
}
|
|
600
|
+
const corners = [
|
|
601
|
+
{ x: bounds.x, y: bounds.y },
|
|
602
|
+
{ x: bounds.x + bounds.width, y: bounds.y },
|
|
603
|
+
{ x: bounds.x, y: bounds.y + bounds.height },
|
|
604
|
+
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height }
|
|
605
|
+
];
|
|
606
|
+
return corners.map(cast).filter((h) => h !== null);
|
|
607
|
+
}
|
|
608
|
+
function createInsert(opts) {
|
|
609
|
+
const up = opts.up ?? DEFAULT_UP;
|
|
610
|
+
const minExtent = opts.minExtent ?? 0.25;
|
|
611
|
+
const axes = up.y !== 0 ? ["x", "z"] : up.x !== 0 ? ["y", "z"] : ["x", "y"];
|
|
612
|
+
return {
|
|
613
|
+
commit(bounds, extras, ends) {
|
|
614
|
+
const vp = opts.viewport();
|
|
615
|
+
const cast = (p) => {
|
|
616
|
+
const ray = rayAt(vp, p);
|
|
617
|
+
const t = ray && intersectRayPlane(ray, up, 0);
|
|
618
|
+
return ray && t !== null ? pointOnRay(ray, t) : null;
|
|
619
|
+
};
|
|
620
|
+
const hits = footprintPoints(bounds, ends, cast);
|
|
621
|
+
if (hits.length === 0) return null;
|
|
622
|
+
const [u, v] = axes;
|
|
623
|
+
let minU = Infinity, maxU = -Infinity, minV = Infinity, maxV = -Infinity;
|
|
624
|
+
for (const h of hits) {
|
|
625
|
+
minU = Math.min(minU, h[u]);
|
|
626
|
+
maxU = Math.max(maxU, h[u]);
|
|
627
|
+
minV = Math.min(minV, h[v]);
|
|
628
|
+
maxV = Math.max(maxV, h[v]);
|
|
629
|
+
}
|
|
630
|
+
const center = { x: 0, y: 0, z: 0 };
|
|
631
|
+
center[u] = (minU + maxU) / 2;
|
|
632
|
+
center[v] = (minV + maxV) / 2;
|
|
633
|
+
return opts.mint(
|
|
634
|
+
{
|
|
635
|
+
center,
|
|
636
|
+
width: Math.max(minExtent, maxU - minU),
|
|
637
|
+
depth: Math.max(minExtent, maxV - minV)
|
|
638
|
+
},
|
|
639
|
+
extras
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
var AXES = ["x", "y", "z"];
|
|
645
|
+
var BITS = [1, 2, 4];
|
|
646
|
+
var UNIT = { x: { x: 1, y: 0, z: 0 }, y: { x: 0, y: 1, z: 0 }, z: { x: 0, y: 0, z: 1 } };
|
|
647
|
+
var ACROSS = { x: [UNIT.y, UNIT.z], y: [UNIT.x, UNIT.z], z: [UNIT.x, UNIT.y] };
|
|
648
|
+
function boxesOf(world, q) {
|
|
649
|
+
const excluded = new Set(q.exclude);
|
|
650
|
+
const out = [];
|
|
651
|
+
for (const node of world.scene.renderOrderNodes()) {
|
|
652
|
+
if (!excluded.has(node.id)) out.push(boundsOfNode(world, node));
|
|
653
|
+
}
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
function distanceToBox(p, box) {
|
|
657
|
+
const d = (a) => Math.max(box.min[a] - p[a], 0, p[a] - box.max[a]);
|
|
658
|
+
return Math.hypot(d("x"), d("y"), d("z"));
|
|
659
|
+
}
|
|
660
|
+
var centerOf = (box) => scale(add(box.min, box.max), 0.5);
|
|
661
|
+
var isZero = (v) => v.x === 0 && v.y === 0 && v.z === 0;
|
|
662
|
+
function boxSnapSource(world) {
|
|
663
|
+
return {
|
|
664
|
+
id: "box",
|
|
665
|
+
roles: ["geometry", "pointer"],
|
|
666
|
+
*collect(q) {
|
|
667
|
+
const r = q.reachWorld(q.reachPx);
|
|
668
|
+
for (const box of boxesOf(world, q)) {
|
|
669
|
+
for (const p of q.probes) {
|
|
670
|
+
if (distanceToBox(p.at, box) <= r) yield* boxLoci(p.id, p.at, box, r);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function* boxLoci(probe, at, box, r) {
|
|
677
|
+
for (let i = 0; i < 8; i++) {
|
|
678
|
+
const c = boxCorner(box, i);
|
|
679
|
+
if (distance(at, c) <= r) yield { probe, locus: { at: c, dirs: [] }, mark: { kind: "point", at: c, glyph: "corner" } };
|
|
680
|
+
}
|
|
681
|
+
const center = centerOf(box);
|
|
682
|
+
if (distance(at, center) <= r) {
|
|
683
|
+
yield { probe, locus: { at: center, dirs: [] }, mark: { kind: "point", at: center, glyph: "center" } };
|
|
684
|
+
}
|
|
685
|
+
for (let i = 0; i < 8; i++) {
|
|
686
|
+
for (const bit of BITS) {
|
|
687
|
+
if (i & bit) continue;
|
|
688
|
+
const a = boxCorner(box, i);
|
|
689
|
+
const b = boxCorner(box, i | bit);
|
|
690
|
+
const dir = sub(b, a);
|
|
691
|
+
if (isZero(dir) || distanceToBox(at, { min: a, max: b }) > r) continue;
|
|
692
|
+
yield { probe, locus: { at: a, dirs: [dir], span: [[0, 1]] }, mark: { kind: "line", from: a, to: b } };
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
for (const bit of BITS) {
|
|
696
|
+
const [b1, b2] = BITS.filter((b) => b !== bit);
|
|
697
|
+
for (const base of [0, bit]) {
|
|
698
|
+
const o = boxCorner(box, base);
|
|
699
|
+
const far = boxCorner(box, base | b1 | b2);
|
|
700
|
+
const d1 = sub(boxCorner(box, base | b1), o);
|
|
701
|
+
const d2 = sub(boxCorner(box, base | b2), o);
|
|
702
|
+
if (isZero(d1) || isZero(d2) || distanceToBox(at, { min: o, max: far }) > r) continue;
|
|
703
|
+
yield {
|
|
704
|
+
probe,
|
|
705
|
+
locus: { at: o, dirs: [d1, d2], span: [[0, 1], [0, 1]] },
|
|
706
|
+
mark: () => outline([o, boxCorner(box, base | b1), far, boxCorner(box, base | b2)])
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function outline(ring) {
|
|
712
|
+
return ring.map((from, i) => ({ kind: "line", from, to: ring[(i + 1) % ring.length] }));
|
|
713
|
+
}
|
|
714
|
+
function alignPlaneSource(world) {
|
|
715
|
+
return {
|
|
716
|
+
id: "align-plane",
|
|
717
|
+
roles: ["box", "pointer"],
|
|
718
|
+
*collect(q) {
|
|
719
|
+
const r = q.reachWorld(q.reachPx);
|
|
720
|
+
for (const [axis, planes] of planesOf(boxesOf(world, q))) {
|
|
721
|
+
for (const [value, ends] of planes) {
|
|
722
|
+
for (const p of q.probes) {
|
|
723
|
+
if (Math.abs(p.at[axis] - value) > r) continue;
|
|
724
|
+
yield {
|
|
725
|
+
probe: p.id,
|
|
726
|
+
locus: { at: ends[0], dirs: ACROSS[axis] },
|
|
727
|
+
mark: (delta) => {
|
|
728
|
+
const from = add(sub(p.at, q.delta0), delta);
|
|
729
|
+
return ends.map((to) => ({ kind: "line", from, to }));
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function planesOf(boxes) {
|
|
739
|
+
const out = new Map(AXES.map((a) => [a, /* @__PURE__ */ new Map()]));
|
|
740
|
+
for (const box of boxes) {
|
|
741
|
+
const center = centerOf(box);
|
|
742
|
+
for (const axis of AXES) {
|
|
743
|
+
const planes = out.get(axis);
|
|
744
|
+
for (const value of /* @__PURE__ */ new Set([box.min[axis], center[axis], box.max[axis]])) {
|
|
745
|
+
const on = { ...center, [axis]: value };
|
|
746
|
+
const ends = planes.get(value);
|
|
747
|
+
if (ends) ends.push(on);
|
|
748
|
+
else planes.set(value, [on]);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return out;
|
|
753
|
+
}
|
|
754
|
+
function groundGridSource(opts) {
|
|
755
|
+
const up = normalize(opts.up ?? DEFAULT_UP);
|
|
756
|
+
const [u, v] = planeBasis(up);
|
|
757
|
+
const lines = (c) => {
|
|
758
|
+
const near = Math.round(c / opts.step) * opts.step;
|
|
759
|
+
return [near, near + (c >= near ? opts.step : -opts.step)];
|
|
760
|
+
};
|
|
761
|
+
return {
|
|
762
|
+
id: "ground-grid",
|
|
763
|
+
tier: "fallback",
|
|
764
|
+
reachPx: Infinity,
|
|
765
|
+
roles: ["pointer", "geometry"],
|
|
766
|
+
*collect(q) {
|
|
767
|
+
for (const p of q.probes) {
|
|
768
|
+
if (Math.abs(dot(p.at, up)) > 1e-6 * Math.max(1, len(p.at))) continue;
|
|
769
|
+
const a = dot(p.at, u);
|
|
770
|
+
const b = dot(p.at, v);
|
|
771
|
+
for (const at of lines(a)) yield { probe: p.id, locus: { at: add(scale(u, at), scale(v, b)), dirs: [v] } };
|
|
772
|
+
for (const at of lines(b)) yield { probe: p.id, locus: { at: add(scale(u, a), scale(v, at)), dirs: [u] } };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/snap/marks.ts
|
|
779
|
+
function projectSnapMarks(marks, viewport) {
|
|
780
|
+
const lines = [];
|
|
781
|
+
const points = [];
|
|
782
|
+
for (const m of marks) {
|
|
783
|
+
if (m.kind === "point") {
|
|
784
|
+
const at = projectPoint(m.at, viewport);
|
|
785
|
+
if (at) {
|
|
786
|
+
points.push({
|
|
787
|
+
at,
|
|
788
|
+
glyph: m.glyph,
|
|
789
|
+
...m.label !== void 0 ? { label: m.label } : {},
|
|
790
|
+
...m.color ? { color: m.color } : {}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
const from = projectPoint(m.from, viewport);
|
|
796
|
+
const to = projectPoint(m.to, viewport);
|
|
797
|
+
if (!from || !to) continue;
|
|
798
|
+
lines.push({
|
|
799
|
+
from,
|
|
800
|
+
to,
|
|
801
|
+
..."value" in m ? { value: m.value } : {},
|
|
802
|
+
...m.color ? { color: m.color } : {}
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return { lines, points };
|
|
806
|
+
}
|
|
416
807
|
function collectOverlayBoxes(handles, origin) {
|
|
417
808
|
const overlays = [];
|
|
418
|
-
for (const handle of handles)
|
|
419
|
-
const ov = handle.overlay?.();
|
|
420
|
-
if (ov) overlays.push(ov);
|
|
421
|
-
}
|
|
809
|
+
for (const handle of handles) overlays.push(...overlaysOf(handle));
|
|
422
810
|
const boxes = [];
|
|
423
811
|
for (const resolved of resolveOverlays(overlays)) {
|
|
424
812
|
if (resolved.kind !== "marquee" && resolved.kind !== "insertPreview") continue;
|
|
@@ -429,41 +817,117 @@ function collectOverlayBoxes(handles, origin) {
|
|
|
429
817
|
}
|
|
430
818
|
var ORBIT_RATE = 6e-3;
|
|
431
819
|
var DOLLY_RATE = 15e-4;
|
|
820
|
+
function screenDelta(move) {
|
|
821
|
+
return move.drag?.screenDelta ?? move.drag?.delta;
|
|
822
|
+
}
|
|
823
|
+
function dragCamera(ctx, kind, step) {
|
|
824
|
+
const dep = ctx.deps.camera3d;
|
|
825
|
+
const origin = dep?.get();
|
|
826
|
+
return {
|
|
827
|
+
kind,
|
|
828
|
+
onMove(move) {
|
|
829
|
+
const delta = screenDelta(move);
|
|
830
|
+
if (!dep || !origin || !delta) return;
|
|
831
|
+
dep.set(step(origin, delta, dep));
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
432
835
|
var orbitAction = {
|
|
433
836
|
id: "camera.orbit",
|
|
434
837
|
label: "Orbit",
|
|
838
|
+
group: "viewport",
|
|
839
|
+
cursor: "grab",
|
|
840
|
+
activeCursor: "grabbing",
|
|
841
|
+
requires: ["camera3d", "kitSettings"],
|
|
842
|
+
invoker: {
|
|
843
|
+
timing: "ongoing",
|
|
844
|
+
start(ctx) {
|
|
845
|
+
const rate = ctx.deps.kitSettings?.get("orbit3d.orbitRate") ?? ORBIT_RATE;
|
|
846
|
+
return dragCamera(ctx, "orbit", (origin, d, dep) => orbitBy(origin, -d.x * rate, d.y * rate, dep.limits?.()));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
var panAction = {
|
|
851
|
+
id: "camera.pan",
|
|
852
|
+
label: "Pan",
|
|
853
|
+
group: "viewport",
|
|
854
|
+
cursor: "move",
|
|
855
|
+
requires: ["camera3d"],
|
|
856
|
+
invoker: {
|
|
857
|
+
timing: "ongoing",
|
|
858
|
+
start(ctx) {
|
|
859
|
+
return dragCamera(ctx, "pan", (origin, d, dep) => panBy(origin, d.x, d.y, dep.size().height));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
function anchorOf(params, pane) {
|
|
864
|
+
const x = params?.clientX;
|
|
865
|
+
const y = params?.clientY;
|
|
866
|
+
return typeof x === "number" && typeof y === "number" ? { x, y } : { x: pane.width / 2, y: pane.height / 2 };
|
|
867
|
+
}
|
|
868
|
+
var dollyAction = {
|
|
869
|
+
id: "camera.dolly",
|
|
870
|
+
label: "Dolly",
|
|
871
|
+
group: "viewport",
|
|
872
|
+
defaultBinding: [
|
|
873
|
+
{ spec: { kind: "wheel" }, opts: {} },
|
|
874
|
+
// Unbound, a trackpad pinch zooms the page instead.
|
|
875
|
+
{ spec: { kind: "wheel", mods: { ctrl: true } }, opts: {} },
|
|
876
|
+
{ spec: { kind: "pinch" }, opts: {} }
|
|
877
|
+
],
|
|
878
|
+
requires: ["camera3d", "kitSettings"],
|
|
879
|
+
invoker: {
|
|
880
|
+
timing: "immediate",
|
|
881
|
+
run(deps, params) {
|
|
882
|
+
const dep = deps.camera3d;
|
|
883
|
+
if (!dep) return;
|
|
884
|
+
const factor = typeof params?.scale === "number" ? 1 / params.scale : Math.exp(
|
|
885
|
+
(typeof params?.deltaY === "number" ? params.deltaY : 0) * (deps.kitSettings?.get("orbit3d.dollyRate") ?? DOLLY_RATE)
|
|
886
|
+
);
|
|
887
|
+
if (!(factor > 0) || !Number.isFinite(factor)) return;
|
|
888
|
+
const pane = dep.size();
|
|
889
|
+
dep.set(dollyToward(dep.get(), factor, anchorOf(params, pane), pane, dep.limits?.()));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
var pinchDollyAction = {
|
|
894
|
+
id: "camera.pinchDolly",
|
|
895
|
+
label: "Pinch Dolly",
|
|
896
|
+
group: "viewport",
|
|
897
|
+
defaultBinding: { kind: "multiTouch", fingers: 2 },
|
|
435
898
|
requires: ["camera3d"],
|
|
436
899
|
invoker: {
|
|
437
900
|
timing: "ongoing",
|
|
438
901
|
start(ctx) {
|
|
439
902
|
const dep = ctx.deps.camera3d;
|
|
440
903
|
const origin = dep?.get();
|
|
904
|
+
const startSpread = ctx.multiTouch?.spread ?? 0;
|
|
905
|
+
const at = ctx.multiTouch?.centroid;
|
|
441
906
|
return {
|
|
442
|
-
kind: "
|
|
907
|
+
kind: "pinch",
|
|
443
908
|
onMove(move) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
909
|
+
const spread = move.multiTouch?.pinch?.currentSpread ?? 0;
|
|
910
|
+
if (!dep || !origin || !(startSpread > 0) || !(spread > 0)) return;
|
|
911
|
+
const pane = dep.size();
|
|
912
|
+
const anchor = at ?? { x: pane.width / 2, y: pane.height / 2 };
|
|
913
|
+
dep.set(dollyToward(origin, startSpread / spread, anchor, pane, dep.limits?.()));
|
|
448
914
|
}
|
|
449
915
|
};
|
|
450
916
|
}
|
|
451
917
|
}
|
|
452
918
|
};
|
|
453
|
-
var
|
|
454
|
-
id: "camera.
|
|
455
|
-
label: "
|
|
919
|
+
var homeAction = {
|
|
920
|
+
id: "camera.home",
|
|
921
|
+
label: "Reset Camera",
|
|
456
922
|
group: "viewport",
|
|
457
|
-
|
|
458
|
-
defaultBinding: [{ spec: { kind: "wheel" }, opts: {} }],
|
|
923
|
+
defaultBinding: { kind: "doubleClick" },
|
|
459
924
|
requires: ["camera3d"],
|
|
460
925
|
invoker: {
|
|
461
926
|
timing: "immediate",
|
|
462
|
-
run(deps
|
|
927
|
+
run(deps) {
|
|
463
928
|
const dep = deps.camera3d;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
dep.set(dollyBy(dep.get(), Math.exp(amount * DOLLY_RATE)));
|
|
929
|
+
const home = dep?.home?.();
|
|
930
|
+
if (dep && home) dep.set(home);
|
|
467
931
|
}
|
|
468
932
|
}
|
|
469
933
|
};
|
|
@@ -473,10 +937,19 @@ function useOrbitTool() {
|
|
|
473
937
|
hookName: "useOrbitTool",
|
|
474
938
|
cursor: "grab",
|
|
475
939
|
presentation: { label: "Orbit", group: "viewport" },
|
|
476
|
-
bindings: [
|
|
940
|
+
bindings: [
|
|
941
|
+
{ spec: { kind: "drag" }, actionId: "camera.orbit" },
|
|
942
|
+
{ spec: { kind: "drag", mods: { shift: true } }, actionId: "camera.pan" }
|
|
943
|
+
]
|
|
477
944
|
}), []);
|
|
478
945
|
}
|
|
946
|
+
var kernel3dKitSettingsSchema = pref.group("3D camera", {
|
|
947
|
+
orbit3d: pref.group("Orbit", {
|
|
948
|
+
orbitRate: pref.number("Orbit speed", ORBIT_RATE, { min: 1e-3, max: 0.03, step: 1e-3 }),
|
|
949
|
+
dollyRate: pref.number("Dolly speed", DOLLY_RATE, { min: 2e-4, max: 0.01, step: 1e-4 })
|
|
950
|
+
})
|
|
951
|
+
});
|
|
479
952
|
|
|
480
|
-
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 };
|
|
953
|
+
export { DEFAULT_ORBIT_LIMITS, MAX_PITCH, UNIT_CUBE, aabbOfPose, alignPlaneSource, boxSnapSource, cameraEye, cameraView, cameraViewProjection, clampCamera, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, dollyToward, groundGridSource, homeAction, kernel3dKitSettingsSchema, ndcToScreen, orbitAction, orbitBy, panAction, panBy, pinchDollyAction, pose3, poseMatrix, projectAabbToScreen, projectPoint, projectSnapMarks, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool };
|
|
481
954
|
//# sourceMappingURL=index.js.map
|
|
482
955
|
//# sourceMappingURL=index.js.map
|