@displayxr/inline3d 1.49.3 → 1.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +6 -1
- package/call.d.ts +16 -0
- package/dist/call.js +6 -6
- package/docs/making-3d-look-right.md +14 -7
- package/docs/woven-canvas-rules.md +30 -1
- package/hands.d.ts +198 -0
- package/index.d.ts +4 -0
- package/js/call/share.js +90 -0
- package/js/camera/capture.js +9 -0
- package/js/camera/half-sbs.js +109 -0
- package/js/inline3d-call.js +1 -1
- package/js/inline3d-hands.js +618 -0
- package/js/inline3d-model-entry.js +3 -1
- package/js/inline3d-splat-playcanvas.js +71 -16
- package/js/inline3d-splat-shared.js +12 -1
- package/js/inline3d-viewer.js +39 -8
- package/llms.txt +2 -2
- package/model.d.ts +10 -0
- package/package.json +7 -1
- package/splat.d.ts +16 -0
- package/viewer.d.ts +12 -0
|
@@ -380,13 +380,15 @@ const FLIP_Q = [1, 0, 0, 0];
|
|
|
380
380
|
/**
|
|
381
381
|
* SceneViewer's pivot as ONE matrix: content space → display space.
|
|
382
382
|
*
|
|
383
|
-
* M = T(orbitCentre + (0,0,depthOffset)) · Rx(pitch) · Ry(yaw) · S(fit × zoom) · T(−focus)
|
|
383
|
+
* M = T(orbitCentre + (0,0,depthOffset)) · Rz(roll) · Rx(pitch) · Ry(yaw) · S(fit × zoom) · T(−focus)
|
|
384
384
|
*
|
|
385
|
-
* which is exactly the product of its `_pivot` (position, '
|
|
385
|
+
* which is exactly the product of its `_pivot` (position, 'ZXY' rotation, scale) and
|
|
386
386
|
* `_centering` (−focus) groups. Kept as a pure function so a test can hold the two together.
|
|
387
|
+
* Roll (the two-hand twist) is about the VIEW axis, so it sits outside yaw and pitch; at roll 0
|
|
388
|
+
* this is the old Rx · Ry product bit for bit.
|
|
387
389
|
*/
|
|
388
|
-
export function pivotMatrix({ yaw = 0, pitch = 0, scale = 1, focus = [0, 0, 0], orbitCentre = [0, 0, 0], depthOffset = 0 }) {
|
|
389
|
-
const q = pivotQuat(yaw, pitch);
|
|
390
|
+
export function pivotMatrix({ yaw = 0, pitch = 0, roll = 0, scale = 1, focus = [0, 0, 0], orbitCentre = [0, 0, 0], depthOffset = 0 }) {
|
|
391
|
+
const q = pivotQuat(yaw, pitch, roll);
|
|
390
392
|
const R = poseMatrix([0, 0, 0], q);
|
|
391
393
|
const out = new Float64Array(16);
|
|
392
394
|
// columns of R·S
|
|
@@ -400,19 +402,25 @@ export function pivotMatrix({ yaw = 0, pitch = 0, scale = 1, focus = [0, 0, 0],
|
|
|
400
402
|
return out;
|
|
401
403
|
}
|
|
402
404
|
|
|
403
|
-
/**
|
|
404
|
-
|
|
405
|
+
/**
|
|
406
|
+
* R = Rz(roll)·Rx(pitch)·Ry(yaw) — three's Euler order 'ZXY', SceneViewer's — as a quaternion.
|
|
407
|
+
* With roll 0 the Rz factor is the identity quaternion and the product is exactly Rx·Ry.
|
|
408
|
+
*/
|
|
409
|
+
function pivotQuat(yawDeg, pitchDeg, rollDeg = 0) {
|
|
405
410
|
const a = (pitchDeg * DEG) / 2;
|
|
406
411
|
const b = (yawDeg * DEG) / 2;
|
|
407
|
-
|
|
412
|
+
const q = qmul([Math.sin(a), 0, 0, Math.cos(a)], [0, Math.sin(b), 0, Math.cos(b)]);
|
|
413
|
+
if (!rollDeg) return q;
|
|
414
|
+
const c = (rollDeg * DEG) / 2;
|
|
415
|
+
return qmul([0, 0, Math.sin(c), Math.cos(c)], q);
|
|
408
416
|
}
|
|
409
417
|
|
|
410
418
|
/**
|
|
411
419
|
* The INVERSE pivot, as the TRS the engine's rig node takes. M⁻¹ = T(f)·S(1/s)·Rᵀ·T(−t), and a
|
|
412
420
|
* uniform scale commutes with a rotation, so it is position f − Rᵀt/s, rotation Rᵀ, scale 1/s.
|
|
413
421
|
*/
|
|
414
|
-
export function pivotInverseTRS({ yaw = 0, pitch = 0, scale = 1, focus = [0, 0, 0], orbitCentre = [0, 0, 0], depthOffset = 0 }) {
|
|
415
|
-
const q = pivotQuat(yaw, pitch);
|
|
422
|
+
export function pivotInverseTRS({ yaw = 0, pitch = 0, roll = 0, scale = 1, focus = [0, 0, 0], orbitCentre = [0, 0, 0], depthOffset = 0 }) {
|
|
423
|
+
const q = pivotQuat(yaw, pitch, roll);
|
|
416
424
|
const qi = [-q[0], -q[1], -q[2], q[3]];
|
|
417
425
|
const t = [orbitCentre[0], orbitCentre[1], orbitCentre[2] + depthOffset];
|
|
418
426
|
const rt = qrot(qi, t);
|
|
@@ -579,14 +587,22 @@ export function capturePose(rest, flipY) {
|
|
|
579
587
|
* (epic #36, P4d); until then the anchors are regexes (the npm build re-indents chunks with
|
|
580
588
|
* tabs) and a miss warns once and renders unpatched rather than failing.
|
|
581
589
|
*
|
|
590
|
+
* PlayCanvas 2.23 fixed it upstream: its perspective branch takes the focal length per axis
|
|
591
|
+
* (`vec2 focal = viewport_size.xy * vec2(center.projMat00, center.projMat11)`). That chunk is
|
|
592
|
+
* returned untouched with `native: true` and counts as fixed — it is not a miss, and must not
|
|
593
|
+
* warn that splats will draw at half height.
|
|
594
|
+
*
|
|
582
595
|
* The rewritten lines are adapted from PlayCanvas engine src/scene/shader-lib/glsl/chunks/gsplat/vert/
|
|
583
596
|
* gsplatCorner.js (the `gsplatCornerVS` chunk) @ v2.22.3, MIT (THIRD_PARTY_NOTICES.md).
|
|
584
597
|
*
|
|
585
|
-
* @returns {{src:string, ok:boolean}}
|
|
598
|
+
* @returns {{src:string, ok:boolean, native?:boolean}}
|
|
586
599
|
*/
|
|
587
600
|
export function patchGsplatFootprint(src) {
|
|
588
601
|
if (typeof src !== 'string') return { src, ok: false };
|
|
589
602
|
if (src.includes('dxrFocalY')) return { src, ok: true };
|
|
603
|
+
if (/vec2\s+focal\s*=\s*viewport_size\.xy\s*\*\s*vec2\s*\(\s*center\.projMat00\s*,\s*center\.projMat11\s*\)/.test(src)) {
|
|
604
|
+
return { src, ok: true, native: true };
|
|
605
|
+
}
|
|
590
606
|
const r1 = /vec2\s+J2\s*=\s*-J1\s*\/\s*vp\.z\s*\*\s*vp\.xy\s*;/;
|
|
591
607
|
const r2 = /0\.0\s*,\s*J1\s*,\s*J2\.y\s*,/;
|
|
592
608
|
if (!r1.test(src) || !r2.test(src)) return { src, ok: false };
|
|
@@ -952,6 +968,11 @@ export class PlayCanvasSplatViewer {
|
|
|
952
968
|
this._pitch = 0;
|
|
953
969
|
this._targetYaw = 0;
|
|
954
970
|
this._targetPitch = 0;
|
|
971
|
+
// Roll about the VIEW axis, degrees — SceneViewer's. Only the two-hand gesture
|
|
972
|
+
// (./inline3d-hands.js) and an explicit setPose({ roll }) write it, so every existing page is
|
|
973
|
+
// bit-identical at 0.
|
|
974
|
+
this._roll = 0;
|
|
975
|
+
this._targetRoll = 0;
|
|
955
976
|
this._focus = { x: 0, y: 0, z: 0 };
|
|
956
977
|
this._targetFocus = { x: 0, y: 0, z: 0 };
|
|
957
978
|
this._orbitCentre = { x: 0, y: 0, z: 0 };
|
|
@@ -1066,7 +1087,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1066
1087
|
this._placeMonoForFit();
|
|
1067
1088
|
}
|
|
1068
1089
|
|
|
1069
|
-
setPose({ yaw, pitch, zoom, depthOffset } = {}) {
|
|
1090
|
+
setPose({ yaw, pitch, zoom, roll, depthOffset } = {}) {
|
|
1070
1091
|
if (this.pageCamera) throw new Error(PAGE_POSE_ERROR);
|
|
1071
1092
|
// A snap, as on SceneViewer; it also ends an orbit relax (the page is driving now).
|
|
1072
1093
|
if (yaw !== undefined || pitch !== undefined) this._orbitMode = null;
|
|
@@ -1074,6 +1095,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1074
1095
|
if (pitch !== undefined) {
|
|
1075
1096
|
this._targetPitch = this._pitch = clamp(pitch, this.pitchLimit[0], this.pitchLimit[1]);
|
|
1076
1097
|
}
|
|
1098
|
+
if (roll !== undefined) this._targetRoll = this._roll = roll;
|
|
1077
1099
|
if (zoom !== undefined) {
|
|
1078
1100
|
// A snap, and the new rest: a relax comes home HERE, not to 1×.
|
|
1079
1101
|
this._targetZoom = this._zoom = this._restZoom = clamp(zoom, this.zoomOpts.min, this.zoomOpts.max);
|
|
@@ -1088,13 +1110,35 @@ export class PlayCanvasSplatViewer {
|
|
|
1088
1110
|
yaw: target ? this._targetYaw : this._yaw,
|
|
1089
1111
|
pitch: target ? this._targetPitch : this._pitch,
|
|
1090
1112
|
zoom: target ? this._targetZoom : this._zoom,
|
|
1113
|
+
roll: target ? this._targetRoll : this._roll,
|
|
1091
1114
|
depthOffset: this._depthOffset,
|
|
1092
1115
|
};
|
|
1093
1116
|
}
|
|
1094
1117
|
|
|
1118
|
+
/**
|
|
1119
|
+
* Move the pose by a delta, eased — SceneViewer.nudgePose's twin, what the hands
|
|
1120
|
+
* (`@displayxr/inline3d/hands`) drive. Degrees to add, zoom a ratio to multiply (within
|
|
1121
|
+
* `zoomOpts`, and it becomes the new REST, as a setPose zoom does, so a relax does not undo a
|
|
1122
|
+
* two-hand zoom). Roll (the two-hand twist) is degrees about the view axis, as on SceneViewer.
|
|
1123
|
+
* Counts as interaction for the idle turntable and ends an orbit relax, like a drag.
|
|
1124
|
+
*/
|
|
1125
|
+
nudgePose({ yaw = 0, pitch = 0, zoom = 1, roll = 0 } = {}) {
|
|
1126
|
+
if (this.pageCamera) throw new Error(PAGE_POSE_ERROR);
|
|
1127
|
+
if (yaw || pitch) this._orbitMode = null;
|
|
1128
|
+
if (yaw) this._targetYaw += yaw;
|
|
1129
|
+
if (pitch) this._targetPitch = clamp(this._targetPitch + pitch, this.pitchLimit[0], this.pitchLimit[1]);
|
|
1130
|
+
if (roll) this._targetRoll += roll;
|
|
1131
|
+
if (zoom && zoom !== 1) {
|
|
1132
|
+
const from = this._zoomMode === 'rest' ? this._zoom : this._targetZoom;
|
|
1133
|
+
this._targetZoom = this._restZoom = clamp(from * zoom, this.zoomOpts.min, this.zoomOpts.max);
|
|
1134
|
+
this._zoomMode = null;
|
|
1135
|
+
}
|
|
1136
|
+
this._lastInput = now();
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1095
1139
|
resetPose() {
|
|
1096
1140
|
if (this.pageCamera) throw new Error(PAGE_POSE_ERROR);
|
|
1097
|
-
this.setPose({ yaw: 0, pitch: 0, zoom: 1, depthOffset: 0 });
|
|
1141
|
+
this.setPose({ yaw: 0, pitch: 0, zoom: 1, roll: 0, depthOffset: 0 });
|
|
1098
1142
|
this._lastInput = now();
|
|
1099
1143
|
}
|
|
1100
1144
|
|
|
@@ -1103,12 +1147,19 @@ export class PlayCanvasSplatViewer {
|
|
|
1103
1147
|
const s = this._fitScale * this._zoom;
|
|
1104
1148
|
const [hx, hy, hz] = this._subjectHalf;
|
|
1105
1149
|
// SceneViewer's exact arithmetic (degrees → radians as `(d * Math.PI) / 180`), so the two
|
|
1106
|
-
// viewers report bit-identical boxes (pinned by the behavioural-trace test).
|
|
1150
|
+
// viewers report bit-identical boxes (pinned by the behavioural-trace test). Rows of
|
|
1151
|
+
// R = Rz(roll) · Rx(pitch) · Ry(yaw), |row| · half = the AABB half-extent along that axis;
|
|
1152
|
+
// at roll 0 the first two rows are the old Rx · Ry rows exactly.
|
|
1107
1153
|
const p = (this._pitch * Math.PI) / 180;
|
|
1108
1154
|
const y = (this._yaw * Math.PI) / 180;
|
|
1155
|
+
const r = (this._roll * Math.PI) / 180;
|
|
1109
1156
|
const cp = Math.cos(p), sp = Math.sin(p), cy = Math.cos(y), sy = Math.sin(y);
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1157
|
+
const cr = Math.cos(r), sr = Math.sin(r);
|
|
1158
|
+
// Rx . Ry rows
|
|
1159
|
+
const a0 = cy, a1 = 0, a2 = sy;
|
|
1160
|
+
const b0 = sp * sy, b1 = cp, b2 = -sp * cy;
|
|
1161
|
+
const ex = s * (Math.abs(cr * a0 - sr * b0) * hx + Math.abs(cr * a1 - sr * b1) * hy + Math.abs(cr * a2 - sr * b2) * hz);
|
|
1162
|
+
const ey = s * (Math.abs(sr * a0 + cr * b0) * hx + Math.abs(sr * a1 + cr * b1) * hy + Math.abs(sr * a2 + cr * b2) * hz);
|
|
1112
1163
|
const ez = s * (Math.abs(cp * sy) * hx + Math.abs(sp) * hy + Math.abs(cp * cy) * hz);
|
|
1113
1164
|
const cz = this._depthOffset;
|
|
1114
1165
|
return {
|
|
@@ -1499,7 +1550,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1499
1550
|
// handle.engine.root on a model tile gets the engine's stock footprint (documented).
|
|
1500
1551
|
const chunks = patchSplats ? pc.ShaderChunks.get(device, pc.SHADERLANGUAGE_GLSL) : null;
|
|
1501
1552
|
const corner = chunks ? patchGsplatFootprint(chunks.get('gsplatCornerVS')) : { ok: false, skipped: true };
|
|
1502
|
-
if (corner.ok) chunks.set('gsplatCornerVS', corner.src);
|
|
1553
|
+
if (corner.ok && !corner.native) chunks.set('gsplatCornerVS', corner.src);
|
|
1503
1554
|
else if (!corner.skipped && !warnedFootprint) {
|
|
1504
1555
|
warnedFootprint = true;
|
|
1505
1556
|
console.warn(
|
|
@@ -2472,6 +2523,8 @@ export class PlayCanvasSplatViewer {
|
|
|
2472
2523
|
const ko = tau > 0 ? (dt > 0 ? 1 - Math.exp(-dt / tau) : 0) : k;
|
|
2473
2524
|
this._yaw += (this._targetYaw - this._yaw) * ko;
|
|
2474
2525
|
this._pitch += (this._targetPitch - this._pitch) * ko;
|
|
2526
|
+
// Roll has no drag / relax phase (only the hands and setPose write it): the ordinary damping.
|
|
2527
|
+
if (this._roll !== this._targetRoll) this._roll += (this._targetRoll - this._roll) * k;
|
|
2475
2528
|
if (
|
|
2476
2529
|
this._orbitMode === 'rest' &&
|
|
2477
2530
|
Math.abs(this._targetYaw - this._yaw) < 0.01 &&
|
|
@@ -2608,6 +2661,7 @@ export class PlayCanvasSplatViewer {
|
|
|
2608
2661
|
return {
|
|
2609
2662
|
yaw: this._yaw,
|
|
2610
2663
|
pitch: this._pitch,
|
|
2664
|
+
roll: this._roll,
|
|
2611
2665
|
scale: this._fitScale * this._zoom,
|
|
2612
2666
|
focus: [f.x, f.y, f.z],
|
|
2613
2667
|
orbitCentre: [o.x, o.y, o.z],
|
|
@@ -4922,6 +4976,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4922
4976
|
mono: { fov: v.mono.fov, near: v.mono.near, far: v.mono.far, pose: Float64Array.from(v.mono.pose), capture: v.mono.capture },
|
|
4923
4977
|
pose: {
|
|
4924
4978
|
_yaw: v._yaw, _pitch: v._pitch, _targetYaw: v._targetYaw, _targetPitch: v._targetPitch,
|
|
4979
|
+
_roll: v._roll, _targetRoll: v._targetRoll,
|
|
4925
4980
|
_zoom: v._zoom, _targetZoom: v._targetZoom, _depthOffset: v._depthOffset,
|
|
4926
4981
|
_focusSettled: v._focusSettled, _focusRecentres: v._focusRecentres, _orbitMode: v._orbitMode, _lastInput: v._lastInput,
|
|
4927
4982
|
},
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// test/splat-playcanvas.test.mjs (values, and a behavioural trace of both viewers side by side).
|
|
14
14
|
|
|
15
15
|
import { comfortDepthFactor, isComfortZone, DEPTH_DEFAULT_VIEWER_M } from './inline3d-depth-budget.js';
|
|
16
|
+
import { attachHandsOption } from './inline3d-hands.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Double-attach detection around a scene subpath's public entry (`addSplat`, `addModel`).
|
|
@@ -25,9 +26,19 @@ import { comfortDepthFactor, isComfortZone, DEPTH_DEFAULT_VIEWER_M } from './inl
|
|
|
25
26
|
*/
|
|
26
27
|
export function guardedAttach(wall, canvas, method, opts, attach) {
|
|
27
28
|
const g = wall && typeof wall._attachGuard === 'function' ? wall._attachGuard(canvas, method, opts) : null;
|
|
28
|
-
if (!g) return attach(opts);
|
|
29
|
+
if (!g) return withHands(wall, attach(opts), opts);
|
|
29
30
|
const out = attach(g.opts);
|
|
30
31
|
g.own(out);
|
|
32
|
+
return withHands(wall, out, opts);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The `hands` option (`./inline3d-hands.js`): pinch to orbit, two pinches to zoom. Wired here so
|
|
37
|
+
* every addModel / addSplat entry, on either engine, gets it from one place, after the core has
|
|
38
|
+
* claimed the handle (the hands' stop rides the same `remove` accessor).
|
|
39
|
+
*/
|
|
40
|
+
function withHands(wall, out, opts) {
|
|
41
|
+
if (opts && opts.hands) attachHandsOption(wall, out, opts);
|
|
31
42
|
return out;
|
|
32
43
|
}
|
|
33
44
|
|
package/js/inline3d-viewer.js
CHANGED
|
@@ -379,6 +379,10 @@ export class SceneViewer {
|
|
|
379
379
|
this._pitch = 0;
|
|
380
380
|
this._targetYaw = 0;
|
|
381
381
|
this._targetPitch = 0;
|
|
382
|
+
// Roll about the VIEW axis, degrees. Only the two-hand gesture (./inline3d-hands.js) and an
|
|
383
|
+
// explicit setPose({ roll }) ever write it, so every existing page is bit-identical at 0.
|
|
384
|
+
this._roll = 0;
|
|
385
|
+
this._targetRoll = 0;
|
|
382
386
|
this._lastInput = now(); // so the turntable waits out the load-in rather than starting mid-pop
|
|
383
387
|
this._lastTick = 0;
|
|
384
388
|
this._monoRaf = 0;
|
|
@@ -524,12 +528,13 @@ export class SceneViewer {
|
|
|
524
528
|
* This SNAPS — it writes the eased value and its target together. The easing in _tick exists
|
|
525
529
|
* for input, not for programmatic placement.
|
|
526
530
|
*/
|
|
527
|
-
setPose({ yaw, pitch, zoom, depthOffset } = {}) {
|
|
531
|
+
setPose({ yaw, pitch, zoom, roll, depthOffset } = {}) {
|
|
528
532
|
if (yaw !== undefined) this._targetYaw = this._yaw = yaw;
|
|
529
533
|
if (pitch !== undefined) {
|
|
530
534
|
this._targetPitch = this._pitch = clamp(pitch, this.pitchLimit[0], this.pitchLimit[1]);
|
|
531
535
|
}
|
|
532
536
|
if (zoom !== undefined) this._targetZoom = this._zoom = clamp(zoom, ZOOM_MIN, ZOOM_MAX);
|
|
537
|
+
if (roll !== undefined) this._targetRoll = this._roll = roll;
|
|
533
538
|
if (depthOffset !== undefined) this._depthOffset = finite(depthOffset, this._depthOffset);
|
|
534
539
|
this._applyTransform();
|
|
535
540
|
}
|
|
@@ -553,6 +558,7 @@ export class SceneViewer {
|
|
|
553
558
|
yaw: target ? this._targetYaw : this._yaw,
|
|
554
559
|
pitch: target ? this._targetPitch : this._pitch,
|
|
555
560
|
zoom: target ? this._targetZoom : this._zoom,
|
|
561
|
+
roll: target ? this._targetRoll : this._roll,
|
|
556
562
|
depthOffset: this._depthOffset,
|
|
557
563
|
};
|
|
558
564
|
}
|
|
@@ -588,15 +594,22 @@ export class SceneViewer {
|
|
|
588
594
|
const [hx, hy, hz] = this._subjectHalf;
|
|
589
595
|
const p = (this._pitch * Math.PI) / 180;
|
|
590
596
|
const y = (this._yaw * Math.PI) / 180;
|
|
591
|
-
|
|
592
|
-
//
|
|
593
|
-
//
|
|
597
|
+
const r = (this._roll * Math.PI) / 180;
|
|
598
|
+
// Rows of R = Rz(roll) . Rx(pitch) . Ry(yaw) — the same product _applyTransform builds, and
|
|
599
|
+
// the same order, which is the part that matters (see its comment on why pitch is outside
|
|
600
|
+
// yaw, and roll outside both). |row| . half gives the AABB half-extent along that world axis.
|
|
601
|
+
// At roll 0 the first two rows reduce to the old Rx . Ry rows exactly.
|
|
594
602
|
const cp = Math.cos(p);
|
|
595
603
|
const sp = Math.sin(p);
|
|
596
604
|
const cy = Math.cos(y);
|
|
597
605
|
const sy = Math.sin(y);
|
|
598
|
-
const
|
|
599
|
-
const
|
|
606
|
+
const cr = Math.cos(r);
|
|
607
|
+
const sr = Math.sin(r);
|
|
608
|
+
// Rx . Ry rows
|
|
609
|
+
const a0 = cy, a1 = 0, a2 = sy;
|
|
610
|
+
const b0 = sp * sy, b1 = cp, b2 = -sp * cy;
|
|
611
|
+
const ex = s * (Math.abs(cr * a0 - sr * b0) * hx + Math.abs(cr * a1 - sr * b1) * hy + Math.abs(cr * a2 - sr * b2) * hz);
|
|
612
|
+
const ey = s * (Math.abs(sr * a0 + cr * b0) * hx + Math.abs(sr * a1 + cr * b1) * hy + Math.abs(sr * a2 + cr * b2) * hz);
|
|
600
613
|
const ez = s * (Math.abs(cp * sy) * hx + Math.abs(sp) * hy + Math.abs(cp * cy) * hz);
|
|
601
614
|
const cz = this._depthOffset;
|
|
602
615
|
return {
|
|
@@ -675,8 +688,22 @@ export class SceneViewer {
|
|
|
675
688
|
}
|
|
676
689
|
|
|
677
690
|
/** Return to the framed default pose, depth slide included. */
|
|
691
|
+
/**
|
|
692
|
+
* Move the pose by a delta, EASED — what an input device does, as opposed to `setPose`'s snap.
|
|
693
|
+
* Angles in degrees to add, `zoom` a ratio to multiply; pitch is clamped, zoom bounded. Counts
|
|
694
|
+
* as interaction for the idle turntable, exactly like a pointer drag. The hands
|
|
695
|
+
* (`@displayxr/inline3d/hands`) drive the viewer through this; a page may too.
|
|
696
|
+
*/
|
|
697
|
+
nudgePose({ yaw = 0, pitch = 0, zoom = 1, roll = 0 } = {}) {
|
|
698
|
+
if (yaw) this._targetYaw += yaw;
|
|
699
|
+
if (pitch) this._targetPitch = clamp(this._targetPitch + pitch, this.pitchLimit[0], this.pitchLimit[1]);
|
|
700
|
+
if (zoom && zoom !== 1) this._targetZoom = clamp(this._targetZoom * zoom, ZOOM_MIN, ZOOM_MAX);
|
|
701
|
+
if (roll) this._targetRoll += roll;
|
|
702
|
+
this._lastInput = now();
|
|
703
|
+
}
|
|
704
|
+
|
|
678
705
|
resetPose() {
|
|
679
|
-
this.setPose({ yaw: 0, pitch: 0, zoom: 1, depthOffset: 0 });
|
|
706
|
+
this.setPose({ yaw: 0, pitch: 0, zoom: 1, roll: 0, depthOffset: 0 });
|
|
680
707
|
this._lastInput = now();
|
|
681
708
|
}
|
|
682
709
|
|
|
@@ -1035,7 +1062,10 @@ export class SceneViewer {
|
|
|
1035
1062
|
// axis is itself yawed. At yaw 90° that axis has swung onto world Z and dragging up/down
|
|
1036
1063
|
// rolls the subject instead of tilting it. Correct head-on, wrong the moment you turn it —
|
|
1037
1064
|
// which is why it survived review and only showed up when two controls were combined.
|
|
1038
|
-
|
|
1065
|
+
//
|
|
1066
|
+
// Roll (the two-hand twist) is about the VIEW axis, so it goes OUTSIDE both: 'ZXY' == R =
|
|
1067
|
+
// Rz(roll) · Rx(pitch) · Ry(yaw). At roll 0 that is the 'XYZ' product above, bit for bit.
|
|
1068
|
+
this._pivot.rotation.set((this._pitch * Math.PI) / 180, (this._yaw * Math.PI) / 180, (this._roll * Math.PI) / 180, 'ZXY');
|
|
1039
1069
|
}
|
|
1040
1070
|
|
|
1041
1071
|
/**
|
|
@@ -1136,6 +1166,7 @@ export class SceneViewer {
|
|
|
1136
1166
|
const k = dt > 0 ? 1 - Math.pow(DAMP_BASE, dt) : 1;
|
|
1137
1167
|
this._yaw += (this._targetYaw - this._yaw) * k;
|
|
1138
1168
|
this._pitch += (this._targetPitch - this._pitch) * k;
|
|
1169
|
+
if (this._roll !== this._targetRoll) this._roll += (this._targetRoll - this._roll) * k;
|
|
1139
1170
|
// Zoom eases on the same curve. Multiplicatively, because zoom is a ratio: approaching 2x
|
|
1140
1171
|
// linearly spends most of its time near the start and then lurches, while a ratio approach
|
|
1141
1172
|
// covers equal PERCEPTUAL steps per frame.
|
package/llms.txt
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
> canvas, the DisplayXR Browser weaves them for the panel; an ordinary browser shows the page in 2D.
|
|
5
5
|
|
|
6
6
|
Read first: docs/making-3d-look-right.md — nine depth and comfort rules from real panels
|
|
7
|
-
(
|
|
8
|
-
|
|
7
|
+
(the default depth is the glass: centre the subject on z = 0 and frame the first view; tilt flat
|
|
8
|
+
arrangements about an axis through the glass, don't push them back; keep moving things' forward swing small, fixed convergence and no near geometry in worlds,
|
|
9
9
|
low contrast behind the subject, text as HTML near the glass, no coplanar surfaces, no sub-pixel
|
|
10
10
|
detail, a cover that always comes off). The SDK's defaults do the rest.
|
|
11
11
|
|
package/model.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { CursorOptions } from './cursor-depth.js';
|
|
5
5
|
import type { SceneViewer, SubjectBounds, OrbitPose } from './viewer.js';
|
|
6
|
+
import type { HandControlOptions, HandControls } from './hands.js';
|
|
6
7
|
import type { FirstWovenResult, RectCoverOption, ViewerEaseOption } from './index.js';
|
|
7
8
|
import type { PlayCanvasSplatViewer, SplatCameraPose, SplatFrameInfo } from './splat.js';
|
|
8
9
|
|
|
@@ -25,6 +26,13 @@ export interface ModelOptions {
|
|
|
25
26
|
/** Degrees/second of turntable once idle (default 8). */
|
|
26
27
|
idleSpin?: number;
|
|
27
28
|
orbit?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Tracked-hand controls (`@displayxr/inline3d/hands`): one pinch dragged turns the model, two
|
|
31
|
+
* pinches spread / dragged / twisted zoom / turn / roll it (either engine). Needs the
|
|
32
|
+
* DisplayXR Browser 1.8.0+ with a hand provider; inert elsewhere, and pointer / wheel keep
|
|
33
|
+
* working alongside. `true` or the controller options; exposed as `handle.hands`. Default off.
|
|
34
|
+
*/
|
|
35
|
+
hands?: boolean | HandControlOptions;
|
|
28
36
|
fit?: 'contain' | 'height' | 'cover' | 'none';
|
|
29
37
|
/** Fraction of the tile the subject may occupy (default 0.8) — width AND height. */
|
|
30
38
|
margin?: number;
|
|
@@ -212,6 +220,8 @@ export interface ModelHandle {
|
|
|
212
220
|
* returns (1.11 returned it synchronously; `@displayxr/inline3d/model/three` still does).
|
|
213
221
|
*/
|
|
214
222
|
readonly viewer: SceneViewer | PlayCanvasSplatViewer | null;
|
|
223
|
+
/** The hand controls, when the `hands` option asked for them (`stop()`, `state.gate`, `controller`). */
|
|
224
|
+
readonly hands?: HandControls;
|
|
215
225
|
/**
|
|
216
226
|
* The loaded glTF root — a three `Object3D`, or on PlayCanvas the render `pc.Entity`
|
|
217
227
|
* (`instantiateRenderEntity()`). Null until `ready` resolves.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.51.0",
|
|
4
4
|
"description": "Turn any HTML <canvas> into a 3D window on a spatial display, inside an ordinary web page, for the DisplayXR Browser. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"types": "./viewer.d.ts",
|
|
28
28
|
"import": "./js/inline3d-viewer.js"
|
|
29
29
|
},
|
|
30
|
+
"./hands": {
|
|
31
|
+
"types": "./hands.d.ts",
|
|
32
|
+
"import": "./js/inline3d-hands.js"
|
|
33
|
+
},
|
|
30
34
|
"./splat": {
|
|
31
35
|
"types": "./splat.d.ts",
|
|
32
36
|
"import": "./js/inline3d-splat.js"
|
|
@@ -91,6 +95,7 @@
|
|
|
91
95
|
"js/inline3d-cursor-shape.js",
|
|
92
96
|
"js/vendor/",
|
|
93
97
|
"js/inline3d-viewer.js",
|
|
98
|
+
"js/inline3d-hands.js",
|
|
94
99
|
"js/inline3d-splat.js",
|
|
95
100
|
"js/inline3d-splat-pc-entry.js",
|
|
96
101
|
"js/inline3d-splat-deferred.js",
|
|
@@ -133,6 +138,7 @@
|
|
|
133
138
|
"depth.d.ts",
|
|
134
139
|
"cursor-depth.d.ts",
|
|
135
140
|
"viewer.d.ts",
|
|
141
|
+
"hands.d.ts",
|
|
136
142
|
"splat.d.ts",
|
|
137
143
|
"splat-playcanvas.d.ts",
|
|
138
144
|
"model.d.ts",
|
package/splat.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { CursorOptions } from './cursor-depth.js';
|
|
5
5
|
import type { SceneViewer, SubjectBounds, OrbitPose } from './viewer.js';
|
|
6
|
+
import type { HandControlOptions, HandControls } from './hands.js';
|
|
6
7
|
import type { FirstWovenResult, RectCoverOption, ViewerEaseOption, DisplayMetrics } from './index.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -78,6 +79,12 @@ export interface PlayCanvasSplatViewer {
|
|
|
78
79
|
fitTo(center: number[], extent: number[]): void;
|
|
79
80
|
setPose(pose?: OrbitPose): void;
|
|
80
81
|
getPose(opts?: { target?: boolean }): Required<OrbitPose>;
|
|
82
|
+
/**
|
|
83
|
+
* Move the pose by a delta, eased (SceneViewer.nudgePose's twin): degrees to add (`roll` about
|
|
84
|
+
* the view axis), zoom a ratio to multiply within `zoomOpts` (it becomes the new rest, as a
|
|
85
|
+
* `setPose` zoom does). Counts as interaction for the idle turntable and ends an orbit relax.
|
|
86
|
+
*/
|
|
87
|
+
nudgePose(delta?: { yaw?: number; pitch?: number; zoom?: number; roll?: number }): void;
|
|
81
88
|
resetPose(): void;
|
|
82
89
|
getSubjectBounds(): SubjectBounds & { front: number; back: number; scale: number };
|
|
83
90
|
setFocus(point: number[] | { x: number; y: number; z: number } | null, opts?: { snap?: boolean; recentre?: boolean }): PlayCanvasSplatViewer;
|
|
@@ -391,6 +398,13 @@ export interface SplatOptions {
|
|
|
391
398
|
* it is still the cumulative turntable (a full-width drag = 180°).
|
|
392
399
|
*/
|
|
393
400
|
orbit?: boolean;
|
|
401
|
+
/**
|
|
402
|
+
* Tracked-hand controls (`@displayxr/inline3d/hands`): one pinch dragged turns the splat, two
|
|
403
|
+
* pinches spread / dragged zoom / turn it (and twist rolls it on Spark). Needs the DisplayXR
|
|
404
|
+
* Browser 1.8.0+ with a hand provider; inert elsewhere. `true` or the controller options;
|
|
405
|
+
* exposed as `handle.hands`. Default off.
|
|
406
|
+
*/
|
|
407
|
+
hands?: boolean | HandControlOptions;
|
|
394
408
|
/** PlayCanvas: the largest drag tilt, degrees, either axis (default 15). */
|
|
395
409
|
orbitMaxDeg?: number;
|
|
396
410
|
/** PlayCanvas: easing time constants, seconds — `drag` while held (0.2), `rest` after release (0.6). */
|
|
@@ -1007,6 +1021,8 @@ export interface SplatHandle {
|
|
|
1007
1021
|
* there until the backend module has loaded — one module fetch after addSplat returns).
|
|
1008
1022
|
*/
|
|
1009
1023
|
readonly viewer: SceneViewer | PlayCanvasSplatViewer;
|
|
1024
|
+
/** The hand controls, when the `hands` option asked for them (`stop()`, `state.gate`, `controller`). */
|
|
1025
|
+
readonly hands?: HandControls;
|
|
1010
1026
|
/**
|
|
1011
1027
|
* Spark's SplatMesh; on `engine: 'playcanvas'` a `{ numSplats, entity, asset, resource }`
|
|
1012
1028
|
* record of the engine objects. Null until the asset is loaded.
|
package/viewer.d.ts
CHANGED
|
@@ -66,6 +66,12 @@ export interface OrbitPose {
|
|
|
66
66
|
pitch?: number;
|
|
67
67
|
/** Multiplier on the fit scale. */
|
|
68
68
|
zoom?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Degrees about the VIEW axis (counter-clockwise as the viewer sees it), applied outside yaw and
|
|
71
|
+
* pitch. Written only by the two-hand gesture (`@displayxr/inline3d/hands`) or an explicit call;
|
|
72
|
+
* 0 for every page that never touches it. Both viewers (three.js and PlayCanvas) carry it.
|
|
73
|
+
*/
|
|
74
|
+
roll?: number;
|
|
69
75
|
/** Display metres along the depth axis, `+` toward the viewer (out of the glass). */
|
|
70
76
|
depthOffset?: number;
|
|
71
77
|
}
|
|
@@ -136,6 +142,12 @@ export declare class SceneViewer {
|
|
|
136
142
|
|
|
137
143
|
/** Snap the pose. Writes the eased value and its target together. */
|
|
138
144
|
setPose(pose?: OrbitPose): void;
|
|
145
|
+
/**
|
|
146
|
+
* Move the pose by a delta, EASED — what an input device does. Degrees to add, `zoom` a ratio
|
|
147
|
+
* to multiply (pitch clamped, zoom bounded). Counts as interaction for the idle turntable, like
|
|
148
|
+
* a pointer drag. `@displayxr/inline3d/hands` drives the viewer through this.
|
|
149
|
+
*/
|
|
150
|
+
nudgePose(delta?: { yaw?: number; pitch?: number; zoom?: number; roll?: number }): void;
|
|
139
151
|
/**
|
|
140
152
|
* Point the viewer at something: the orbit centre, the pivot plane and (on a camera rig) the
|
|
141
153
|
* convergence distance are ONE point, and this is it. `recentre: true` (a display rig) brings
|