@viamrobotics/motion-tools 1.33.0 → 1.33.2
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/dist/components/Entities/Entities.svelte +18 -25
- package/dist/components/Entities/Entities.svelte.d.ts +2 -17
- package/dist/components/Entities/Label.svelte +79 -13
- package/dist/components/Entities/Label.svelte.d.ts +2 -1
- package/dist/components/Entities/Labels.svelte +36 -0
- package/dist/components/Entities/Labels.svelte.d.ts +3 -0
- package/dist/components/Entities/LineDots.svelte +8 -3
- package/dist/components/Entities/labelLayout/applyTeleports.d.ts +9 -0
- package/dist/components/Entities/labelLayout/applyTeleports.js +39 -0
- package/dist/components/Entities/labelLayout/buildNeighborhood.d.ts +8 -0
- package/dist/components/Entities/labelLayout/buildNeighborhood.js +26 -0
- package/dist/components/Entities/labelLayout/cameraHash.d.ts +8 -0
- package/dist/components/Entities/labelLayout/cameraHash.js +25 -0
- package/dist/components/Entities/labelLayout/cost.d.ts +44 -0
- package/dist/components/Entities/labelLayout/cost.js +126 -0
- package/dist/components/Entities/labelLayout/createLabelLayout.d.ts +27 -0
- package/dist/components/Entities/labelLayout/createLabelLayout.js +194 -0
- package/dist/components/Entities/labelLayout/geometry.d.ts +20 -0
- package/dist/components/Entities/labelLayout/geometry.js +151 -0
- package/dist/components/Entities/labelLayout/labelStore.svelte.d.ts +17 -0
- package/dist/components/Entities/labelLayout/labelStore.svelte.js +28 -0
- package/dist/components/Entities/labelLayout/measure.d.ts +13 -0
- package/dist/components/Entities/labelLayout/measure.js +42 -0
- package/dist/components/Entities/labelLayout/slots.d.ts +11 -0
- package/dist/components/Entities/labelLayout/slots.js +47 -0
- package/dist/components/Entities/labelLayout/solve.d.ts +11 -0
- package/dist/components/Entities/labelLayout/solve.js +93 -0
- package/dist/components/Entities/labelLayout/spatialHash.d.ts +15 -0
- package/dist/components/Entities/labelLayout/spatialHash.js +53 -0
- package/dist/components/Entities/labelLayout/types.d.ts +105 -0
- package/dist/components/Entities/labelLayout/types.js +19 -0
- package/dist/components/Entities/labelLayout/writeBack.d.ts +20 -0
- package/dist/components/Entities/labelLayout/writeBack.js +51 -0
- package/dist/components/Scene.svelte +2 -1
- package/dist/components/SelectedTransformControls.svelte +65 -47
- package/dist/components/overlay/Details.svelte +210 -226
- package/dist/components/overlay/Details.svelte.d.ts +1 -1
- package/dist/components/overlay/Popover.svelte +6 -4
- package/dist/components/overlay/Popover.svelte.d.ts +6 -2
- package/dist/components/overlay/dashboard/Button.svelte +7 -2
- package/dist/components/overlay/dashboard/Button.svelte.d.ts +2 -1
- package/dist/components/overlay/details/AxesHelperDetails.svelte +32 -0
- package/dist/components/overlay/details/AxesHelperDetails.svelte.d.ts +7 -0
- package/dist/components/overlay/details/ColorDetails.svelte +35 -0
- package/dist/components/overlay/details/ColorDetails.svelte.d.ts +7 -0
- package/dist/components/overlay/details/GeometryDetails.svelte +104 -0
- package/dist/components/overlay/details/GeometryDetails.svelte.d.ts +7 -0
- package/dist/components/overlay/details/LineDetails/LineDetails.svelte +196 -0
- package/dist/components/overlay/details/LineDetails/LineDetails.svelte.d.ts +7 -0
- package/dist/components/overlay/details/LineDetails/linePositions.d.ts +3 -0
- package/dist/components/overlay/details/LineDetails/linePositions.js +30 -0
- package/dist/components/overlay/details/OpacityDetails.svelte +44 -0
- package/dist/components/overlay/details/OpacityDetails.svelte.d.ts +7 -0
- package/dist/components/overlay/details/PoseDetails.svelte +189 -0
- package/dist/components/overlay/details/PoseDetails.svelte.d.ts +14 -0
- package/dist/ecs/traits.d.ts +1 -1
- package/dist/ecs/traits.js +1 -1
- package/dist/hooks/usePartConfig.svelte.js +8 -6
- package/dist/hooks/useWorldState.svelte.js +94 -69
- package/package.json +4 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uniform grid over label anchors, used to prune the O(n^2) cost evaluation to a
|
|
3
|
+
* bounded neighborhood. The cell size is chosen so any two labels whose boxes
|
|
4
|
+
* could possibly interact share or border a cell, so scanning the 3x3 block
|
|
5
|
+
* around a node finds every candidate.
|
|
6
|
+
*/
|
|
7
|
+
const KEY_OFFSET = 2048;
|
|
8
|
+
const KEY_STRIDE = 4096;
|
|
9
|
+
export class SpatialHash {
|
|
10
|
+
cell = 1;
|
|
11
|
+
buckets = new Map();
|
|
12
|
+
static key(gx, gy) {
|
|
13
|
+
return (gx + KEY_OFFSET) * KEY_STRIDE + (gy + KEY_OFFSET);
|
|
14
|
+
}
|
|
15
|
+
build(nodes, cell) {
|
|
16
|
+
this.cell = Math.max(cell, 1);
|
|
17
|
+
this.buckets.clear();
|
|
18
|
+
for (const node of nodes) {
|
|
19
|
+
const k = SpatialHash.key(Math.floor(node.ax / this.cell), Math.floor(node.ay / this.cell));
|
|
20
|
+
const bucket = this.buckets.get(k);
|
|
21
|
+
if (bucket)
|
|
22
|
+
bucket.push(node);
|
|
23
|
+
else
|
|
24
|
+
this.buckets.set(k, [node]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Nearest `max` nodes (by anchor distance) in the 3x3 cell block around `node`, excluding itself. */
|
|
28
|
+
queryNeighbors(node, max) {
|
|
29
|
+
const gx = Math.floor(node.ax / this.cell);
|
|
30
|
+
const gy = Math.floor(node.ay / this.cell);
|
|
31
|
+
const found = [];
|
|
32
|
+
for (let ox = -1; ox <= 1; ox++) {
|
|
33
|
+
for (let oy = -1; oy <= 1; oy++) {
|
|
34
|
+
const bucket = this.buckets.get(SpatialHash.key(gx + ox, gy + oy));
|
|
35
|
+
if (!bucket)
|
|
36
|
+
continue;
|
|
37
|
+
for (const other of bucket) {
|
|
38
|
+
if (other !== node)
|
|
39
|
+
found.push(other);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (found.length <= max)
|
|
44
|
+
return found;
|
|
45
|
+
found.sort((a, b) => {
|
|
46
|
+
const da = (a.ax - node.ax) ** 2 + (a.ay - node.ay) ** 2;
|
|
47
|
+
const db = (b.ax - node.ax) ** 2 + (b.ay - node.ay) ** 2;
|
|
48
|
+
return da - db;
|
|
49
|
+
});
|
|
50
|
+
found.length = max;
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the label layout engine.
|
|
3
|
+
*
|
|
4
|
+
* All spatial fields are in viewport pixels (one shared screen-space for every
|
|
5
|
+
* label) except where noted. Slots store dot-relative offsets so a camera pan
|
|
6
|
+
* never invalidates them.
|
|
7
|
+
*/
|
|
8
|
+
/** Axis-aligned rectangle as center + half-extents (viewport px). */
|
|
9
|
+
export interface Rect {
|
|
10
|
+
cx: number;
|
|
11
|
+
cy: number;
|
|
12
|
+
hw: number;
|
|
13
|
+
hh: number;
|
|
14
|
+
}
|
|
15
|
+
/** Line segment (viewport px). */
|
|
16
|
+
export interface Segment {
|
|
17
|
+
x1: number;
|
|
18
|
+
y1: number;
|
|
19
|
+
x2: number;
|
|
20
|
+
y2: number;
|
|
21
|
+
}
|
|
22
|
+
/** A candidate placement for a label box, expressed as the box-center offset from the dot. */
|
|
23
|
+
export interface Slot {
|
|
24
|
+
/** Box-center offset from the dot, viewport px. */
|
|
25
|
+
dx: number;
|
|
26
|
+
dy: number;
|
|
27
|
+
/** Direction of the slot from the dot, radians. */
|
|
28
|
+
angle: number;
|
|
29
|
+
/** Distance from the dot to the box center, viewport px. */
|
|
30
|
+
radius: number;
|
|
31
|
+
/** Ring index (0 = innermost). */
|
|
32
|
+
ring: number;
|
|
33
|
+
/** Length-only cost (`W.len * radius`); slots are sorted ascending by this for early-out. */
|
|
34
|
+
baseCost: number;
|
|
35
|
+
}
|
|
36
|
+
export interface LabelNode {
|
|
37
|
+
/** Stable per-element id, assigned once. */
|
|
38
|
+
id: string;
|
|
39
|
+
/** FNV hash of `id`, used for deterministic tie-breaks and per-node slot phase. */
|
|
40
|
+
idHash: number;
|
|
41
|
+
ax: number;
|
|
42
|
+
ay: number;
|
|
43
|
+
dotR: number;
|
|
44
|
+
w: number;
|
|
45
|
+
h: number;
|
|
46
|
+
/** Per-island CSS scale (screenPx / localPx). */
|
|
47
|
+
scale: number;
|
|
48
|
+
/** Cached computed CSS width of the dot (local px); read once. */
|
|
49
|
+
cssDotW: number;
|
|
50
|
+
/** Dot center relative to the island origin, island-local px. Fixed CSS offset, measured once (NaN until then). */
|
|
51
|
+
dotLocalX: number;
|
|
52
|
+
dotLocalY: number;
|
|
53
|
+
slots: Slot[];
|
|
54
|
+
/** Hash of the box/dot geometry; slots regenerate only when this or `crowded` changes. */
|
|
55
|
+
geomKey: string;
|
|
56
|
+
crowded: boolean;
|
|
57
|
+
/** Committed slot (target). -1 before first placement. */
|
|
58
|
+
slotIndex: number;
|
|
59
|
+
/** Slot committed by the previous solve, for stickiness. */
|
|
60
|
+
prevSlotIndex: number;
|
|
61
|
+
cx: number;
|
|
62
|
+
cy: number;
|
|
63
|
+
tx: number;
|
|
64
|
+
ty: number;
|
|
65
|
+
settled: boolean;
|
|
66
|
+
/** Conflict cost of the current slot — the local-search loop key. */
|
|
67
|
+
conflict: number;
|
|
68
|
+
/** Locked at a local optimum for the current solve. */
|
|
69
|
+
locked: boolean;
|
|
70
|
+
/** Local cluster centroid (self + neighbors) for the outward-fan term. */
|
|
71
|
+
centroidX: number;
|
|
72
|
+
centroidY: number;
|
|
73
|
+
/** Anchor at the previous solve, for teleport detection. NaN before first solve. */
|
|
74
|
+
prevAx: number;
|
|
75
|
+
prevAy: number;
|
|
76
|
+
/** Pruned interaction set for the current solve (symmetric). */
|
|
77
|
+
neighbors: LabelNode[];
|
|
78
|
+
labelEl: HTMLElement;
|
|
79
|
+
textEl: HTMLElement;
|
|
80
|
+
dotEl: HTMLElement;
|
|
81
|
+
lineEl: SVGLineElement;
|
|
82
|
+
}
|
|
83
|
+
export interface SolverConfig {
|
|
84
|
+
/** Label-label clearance (viewport px). */
|
|
85
|
+
labelPadding: number;
|
|
86
|
+
/** Dot clearance (viewport px). */
|
|
87
|
+
dotPadding: number;
|
|
88
|
+
/** Candidate angles per ring (doubled for crowded nodes). */
|
|
89
|
+
anglesPerRing: number;
|
|
90
|
+
/** Ring radius multipliers for sparse nodes. */
|
|
91
|
+
ringRadii: number[];
|
|
92
|
+
/** Ring radius multipliers for crowded nodes (more escape room). */
|
|
93
|
+
ringRadiiCrowded: number[];
|
|
94
|
+
/** Max accepted local-search moves per solve. */
|
|
95
|
+
polishBudget: number;
|
|
96
|
+
/** Max neighbors considered per node (caps cost in dense clusters). */
|
|
97
|
+
maxNeighbors: number;
|
|
98
|
+
/** Distance (viewport px) under which a box is considered arrived. */
|
|
99
|
+
settleEps: number;
|
|
100
|
+
/** Neighbor count above which a node is "crowded". */
|
|
101
|
+
crowdedThreshold: number;
|
|
102
|
+
/** Fraction of the viewport diagonal that counts as a camera teleport. */
|
|
103
|
+
teleportFrac: number;
|
|
104
|
+
}
|
|
105
|
+
export declare const defaultSolverConfig: SolverConfig;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the label layout engine.
|
|
3
|
+
*
|
|
4
|
+
* All spatial fields are in viewport pixels (one shared screen-space for every
|
|
5
|
+
* label) except where noted. Slots store dot-relative offsets so a camera pan
|
|
6
|
+
* never invalidates them.
|
|
7
|
+
*/
|
|
8
|
+
export const defaultSolverConfig = {
|
|
9
|
+
labelPadding: 6,
|
|
10
|
+
dotPadding: 6,
|
|
11
|
+
anglesPerRing: 12,
|
|
12
|
+
ringRadii: [1, 1.55, 2.2],
|
|
13
|
+
ringRadiiCrowded: [1, 1.45, 1.95, 2.6],
|
|
14
|
+
polishBudget: 240,
|
|
15
|
+
maxNeighbors: 24,
|
|
16
|
+
settleEps: 0.4,
|
|
17
|
+
crowdedThreshold: 8,
|
|
18
|
+
teleportFrac: 0.5,
|
|
19
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-frame animation and DOM write-back. The solver produces a target box
|
|
3
|
+
* center; the animated center eases toward it (framerate-independent), and the
|
|
4
|
+
* eased position is written to the label as a scale-corrected local transform
|
|
5
|
+
* plus leader-line endpoints — the same coordinate handling the old engine used.
|
|
6
|
+
*/
|
|
7
|
+
import type { LabelNode } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* Ease `node`'s animated center toward its target. Returns true while still
|
|
10
|
+
* moving, false once arrived (and snapped). `delta` is seconds, pre-clamped by
|
|
11
|
+
* the engine so a long idle gap can't produce an overshoot.
|
|
12
|
+
*/
|
|
13
|
+
export declare function lerpStep(node: LabelNode, delta: number, settleEps: number): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Write the eased position to the DOM. The label island is positioned at the dot
|
|
16
|
+
* by Threlte's <HTML>, so we work in island-local px: convert the viewport-space
|
|
17
|
+
* offset by the island's CSS scale, place the text box, and draw the leader from
|
|
18
|
+
* the dot's island-local position to the box center.
|
|
19
|
+
*/
|
|
20
|
+
export declare const writeBack: (node: LabelNode) => void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-frame animation and DOM write-back. The solver produces a target box
|
|
3
|
+
* center; the animated center eases toward it (framerate-independent), and the
|
|
4
|
+
* eased position is written to the label as a scale-corrected local transform
|
|
5
|
+
* plus leader-line endpoints — the same coordinate handling the old engine used.
|
|
6
|
+
*/
|
|
7
|
+
/** Easing time constant (seconds): ~3-4 frames to arrive at 60fps. */
|
|
8
|
+
const TAU = 0.08;
|
|
9
|
+
/**
|
|
10
|
+
* Ease `node`'s animated center toward its target. Returns true while still
|
|
11
|
+
* moving, false once arrived (and snapped). `delta` is seconds, pre-clamped by
|
|
12
|
+
* the engine so a long idle gap can't produce an overshoot.
|
|
13
|
+
*/
|
|
14
|
+
export function lerpStep(node, delta, settleEps) {
|
|
15
|
+
const dx = node.tx - node.cx;
|
|
16
|
+
const dy = node.ty - node.cy;
|
|
17
|
+
if (dx * dx + dy * dy <= settleEps * settleEps) {
|
|
18
|
+
node.cx = node.tx;
|
|
19
|
+
node.cy = node.ty;
|
|
20
|
+
node.settled = true;
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const alpha = 1 - Math.exp(-delta / TAU);
|
|
24
|
+
node.cx += dx * alpha;
|
|
25
|
+
node.cy += dy * alpha;
|
|
26
|
+
node.settled = false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Write the eased position to the DOM. The label island is positioned at the dot
|
|
31
|
+
* by Threlte's <HTML>, so we work in island-local px: convert the viewport-space
|
|
32
|
+
* offset by the island's CSS scale, place the text box, and draw the leader from
|
|
33
|
+
* the dot's island-local position to the box center.
|
|
34
|
+
*/
|
|
35
|
+
export const writeBack = (node) => {
|
|
36
|
+
const inv = 1 / node.scale;
|
|
37
|
+
// Box-center offset from the dot, in island-local px.
|
|
38
|
+
const dx = (node.cx - node.ax) * inv;
|
|
39
|
+
const dy = (node.cy - node.ay) * inv;
|
|
40
|
+
const wL = node.w * inv;
|
|
41
|
+
const hL = node.h * inv;
|
|
42
|
+
// The dot may not sit at the island origin, so anchor everything at the dot's
|
|
43
|
+
// measured local position rather than assuming (0, 0).
|
|
44
|
+
const ox = node.dotLocalX;
|
|
45
|
+
const oy = node.dotLocalY;
|
|
46
|
+
node.textEl.style.transform = `translate(${ox + dx - wL / 2}px, ${oy + dy - hL / 2}px)`;
|
|
47
|
+
node.lineEl.setAttribute('x1', `${ox}`);
|
|
48
|
+
node.lineEl.setAttribute('y1', `${oy}`);
|
|
49
|
+
node.lineEl.setAttribute('x2', `${ox + dx}`);
|
|
50
|
+
node.lineEl.setAttribute('y2', `${oy + dy}`);
|
|
51
|
+
};
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
const bvhEnabled = $derived(
|
|
50
50
|
settings.current.renderSubEntityHoverDetail ||
|
|
51
51
|
settings.current.interactionMode === 'measure' ||
|
|
52
|
-
settings.current.interactionMode === 'select'
|
|
52
|
+
settings.current.interactionMode === 'select' ||
|
|
53
|
+
settings.current.interactionMode === 'gizmo'
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
bvh(raycaster, () => ({ helper: false, enabled: bvhEnabled }))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useThrelte } from '@threlte/core'
|
|
3
3
|
import { TransformControls } from '@threlte/extras'
|
|
4
|
-
import { Matrix4
|
|
4
|
+
import { Matrix4 } from 'three'
|
|
5
5
|
|
|
6
6
|
import type { FrameEditSession } from '../editing/FrameEditSession'
|
|
7
7
|
|
|
@@ -11,14 +11,7 @@
|
|
|
11
11
|
import { useFrameEditSession } from '../hooks/useFrameEditSession.svelte'
|
|
12
12
|
import { usePartConfig } from '../hooks/usePartConfig.svelte'
|
|
13
13
|
import { useSettings } from '../hooks/useSettings.svelte'
|
|
14
|
-
import {
|
|
15
|
-
createPose,
|
|
16
|
-
matrixToPose,
|
|
17
|
-
poseToMatrix,
|
|
18
|
-
quaternionToPose,
|
|
19
|
-
solveEditedMatrix,
|
|
20
|
-
vector3ToPose,
|
|
21
|
-
} from '../transform'
|
|
14
|
+
import { createPose, matrixToPose, poseToMatrix, solveEditedMatrix } from '../transform'
|
|
22
15
|
|
|
23
16
|
const { scene } = useThrelte()
|
|
24
17
|
const settings = useSettings()
|
|
@@ -64,9 +57,10 @@
|
|
|
64
57
|
})
|
|
65
58
|
const isSphereScale = $derived(activeMode === 'scale' && sphere.current !== undefined)
|
|
66
59
|
const isCapsuleScale = $derived(activeMode === 'scale' && capsule.current !== undefined)
|
|
60
|
+
const transforming = $derived(
|
|
61
|
+
ref && entity && activeMode && !isFragmentComponentWithVariables && !invisible.current
|
|
62
|
+
)
|
|
67
63
|
|
|
68
|
-
const quaternion = new Quaternion()
|
|
69
|
-
const vector3 = new Vector3()
|
|
70
64
|
const refPose = createPose()
|
|
71
65
|
const tempRefMatrix = new Matrix4()
|
|
72
66
|
const tempEditedMatrix = new Matrix4()
|
|
@@ -119,28 +113,14 @@
|
|
|
119
113
|
}
|
|
120
114
|
|
|
121
115
|
const onChange = () => {
|
|
122
|
-
if (!ref || !entity || !activeMode)
|
|
123
|
-
return
|
|
124
|
-
}
|
|
116
|
+
if (!ref || !entity || !activeMode) return
|
|
125
117
|
|
|
126
118
|
const isFrameEntity = entity.has(traits.FramesAPI)
|
|
127
|
-
|
|
128
119
|
if (activeMode === 'translate' || activeMode === 'rotate') {
|
|
129
120
|
if (isFrameEntity) {
|
|
130
121
|
stageFrameTransform()
|
|
131
122
|
} else {
|
|
132
|
-
|
|
133
|
-
if (matrix) {
|
|
134
|
-
matrixToPose(matrix, tempPose)
|
|
135
|
-
if (activeMode === 'translate') {
|
|
136
|
-
vector3ToPose(ref.getWorldPosition(vector3), tempPose)
|
|
137
|
-
} else {
|
|
138
|
-
quaternionToPose(ref.getWorldQuaternion(quaternion), tempPose)
|
|
139
|
-
ref.quaternion.copy(quaternion)
|
|
140
|
-
}
|
|
141
|
-
poseToMatrix(tempPose, matrix)
|
|
142
|
-
entity.changed(traits.Matrix)
|
|
143
|
-
}
|
|
123
|
+
stageLocalTransform()
|
|
144
124
|
}
|
|
145
125
|
} else {
|
|
146
126
|
// scale → bake the gizmo's scale factor into the geometry trait,
|
|
@@ -194,40 +174,46 @@
|
|
|
194
174
|
}
|
|
195
175
|
|
|
196
176
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* Three.js parent has identity world, so `ref.position` / `ref.quaternion`
|
|
200
|
-
* are world-space values. Matrix and EditedMatrix store local-to-parent
|
|
201
|
-
* transforms, so we left-multiply by the parent's inverted WorldMatrix
|
|
202
|
-
* before staging — otherwise WorldMatrix recomposition (parent × edited)
|
|
203
|
-
* re-applies the parent's rotation/translation and the frame ends up at
|
|
204
|
-
* parent × where-the-user-pulled-it.
|
|
177
|
+
* Build the entity's parent-relative drag target from the gizmo's world-space
|
|
178
|
+
* `ref` transform into `out`.
|
|
205
179
|
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
180
|
+
* Entity renderers mount at the scene root with `matrixAutoUpdate = false`
|
|
181
|
+
* and recompose `group.matrix` from the `WorldMatrix` trait, so
|
|
182
|
+
* `ref.position` / `ref.quaternion` are world-space. Matrix-shaped traits
|
|
183
|
+
* store local-to-parent, so we left-multiply by the parent's inverted
|
|
184
|
+
* WorldMatrix. Otherwise recomposition (parentWorld × local) re-applies the
|
|
185
|
+
* parent transform and the entity lands at parentWorld × where-it-was-dragged.
|
|
209
186
|
*/
|
|
210
|
-
const
|
|
187
|
+
const computeLocalDragTarget = (out: Matrix4) => {
|
|
211
188
|
if (!ref || !entity) return
|
|
212
189
|
|
|
213
|
-
|
|
214
|
-
|
|
190
|
+
out.makeRotationFromQuaternion(ref.quaternion)
|
|
191
|
+
out.setPosition(ref.position)
|
|
215
192
|
|
|
216
|
-
const
|
|
217
|
-
const parentWorld = parentEntity?.get(traits.WorldMatrix)
|
|
193
|
+
const parentWorld = entity.targetFor(relations.ChildOf)?.get(traits.WorldMatrix)
|
|
218
194
|
if (parentWorld) {
|
|
219
195
|
tempParentInverse.copy(parentWorld).invert()
|
|
220
|
-
|
|
196
|
+
out.premultiply(tempParentInverse)
|
|
221
197
|
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Stages a translate/rotate drag for a frame system entity into the edit
|
|
202
|
+
* session. With a kinematic offset (LiveMatrix + Matrix both present), the
|
|
203
|
+
* parent-relative target feeds solveEditedMatrix to back out the EditedMatrix
|
|
204
|
+
* satisfying live × baseline⁻¹ × edited = local. Without one, Frame.svelte's
|
|
205
|
+
* blend short-circuits to EditedMatrix, so we stage the target pose directly.
|
|
206
|
+
*/
|
|
207
|
+
const stageFrameTransform = () => {
|
|
208
|
+
if (!ref || !entity) return
|
|
222
209
|
|
|
210
|
+
computeLocalDragTarget(tempRefMatrix)
|
|
223
211
|
matrixToPose(tempRefMatrix, refPose)
|
|
224
212
|
|
|
225
213
|
const live = liveMatrix.current
|
|
226
214
|
const config = configMatrix.current
|
|
227
215
|
|
|
228
216
|
if (!live || !config) {
|
|
229
|
-
// No live matrix available — Frame.svelte's blend short-circuits to
|
|
230
|
-
// editedMatrix, so the parent-relative target is what we stage.
|
|
231
217
|
if (activeMode === 'translate') {
|
|
232
218
|
session?.stagePose(entity, {
|
|
233
219
|
x: refPose.x,
|
|
@@ -249,9 +235,41 @@
|
|
|
249
235
|
matrixToPose(tempEditedMatrix, tempPose)
|
|
250
236
|
session?.stagePose(entity, { ...tempPose })
|
|
251
237
|
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Stages a translate/rotate drag for a non-frame-system entity (e.g. a gizmo)
|
|
241
|
+
* by writing the dragged component into the Matrix trait. Gizmos carry no
|
|
242
|
+
* LiveMatrix, so there's no live-pose blend to invert — the parent-relative
|
|
243
|
+
* target is the new local transform.
|
|
244
|
+
*/
|
|
245
|
+
const stageLocalTransform = () => {
|
|
246
|
+
if (!ref || !entity) return
|
|
247
|
+
|
|
248
|
+
const matrix = entity.get(traits.Matrix)
|
|
249
|
+
if (!matrix) return
|
|
250
|
+
|
|
251
|
+
computeLocalDragTarget(tempRefMatrix)
|
|
252
|
+
|
|
253
|
+
// update only the dragged component
|
|
254
|
+
matrixToPose(matrix, tempPose)
|
|
255
|
+
matrixToPose(tempRefMatrix, refPose)
|
|
256
|
+
if (activeMode === 'translate') {
|
|
257
|
+
tempPose.x = refPose.x
|
|
258
|
+
tempPose.y = refPose.y
|
|
259
|
+
tempPose.z = refPose.z
|
|
260
|
+
} else {
|
|
261
|
+
tempPose.oX = refPose.oX
|
|
262
|
+
tempPose.oY = refPose.oY
|
|
263
|
+
tempPose.oZ = refPose.oZ
|
|
264
|
+
tempPose.theta = refPose.theta
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
poseToMatrix(tempPose, matrix)
|
|
268
|
+
entity.changed(traits.Matrix)
|
|
269
|
+
}
|
|
252
270
|
</script>
|
|
253
271
|
|
|
254
|
-
{#if
|
|
272
|
+
{#if transforming}
|
|
255
273
|
{#key entity}
|
|
256
274
|
<TransformControls
|
|
257
275
|
object={ref}
|