@viamrobotics/motion-tools 1.26.1 → 1.27.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/dist/FrameConfigUpdater.svelte.js +42 -29
- package/dist/assert.d.ts +13 -0
- package/dist/assert.js +20 -0
- package/dist/buf/common/v1/common_pb.d.ts +19 -0
- package/dist/buf/common/v1/common_pb.js +32 -0
- package/dist/components/BatchedArrows.svelte +43 -45
- package/dist/components/Entities/Arrows/Arrows.svelte +35 -29
- package/dist/components/Entities/Entities.svelte +3 -8
- package/dist/components/Entities/Frame.svelte +31 -32
- package/dist/components/Entities/Frame.svelte.d.ts +0 -2
- package/dist/components/Entities/GLTF.svelte +27 -36
- package/dist/components/Entities/Geometry.svelte +35 -24
- package/dist/components/Entities/Line.svelte +37 -43
- package/dist/components/Entities/Mesh.svelte +12 -18
- package/dist/components/Entities/Points.svelte +25 -28
- package/dist/components/Entities/Pose.svelte +17 -24
- package/dist/components/Entities/Pose.svelte.d.ts +1 -4
- package/dist/components/Entities/hooks/useEntityEvents.svelte.js +40 -41
- package/dist/components/Scene.svelte +7 -1
- package/dist/components/SceneProviders.svelte +2 -1
- package/dist/components/SelectedTransformControls.svelte +57 -34
- package/dist/components/StaticGeometries.svelte +1 -1
- package/dist/components/hover/HoveredEntity.svelte +33 -3
- package/dist/components/hover/LinkedHoveredEntity.svelte +2 -3
- package/dist/components/overlay/Details.svelte +72 -94
- package/dist/components/overlay/__tests__/__fixtures__/entity.js +14 -17
- package/dist/components/overlay/left-pane/Tree.svelte +9 -9
- package/dist/components/overlay/left-pane/Tree.svelte.d.ts +1 -2
- package/dist/components/overlay/left-pane/TreeContainer.svelte +4 -15
- package/dist/components/overlay/left-pane/TreeNode.svelte +1 -1
- package/dist/components/overlay/left-pane/TreeNode.svelte.d.ts +1 -1
- package/dist/components/overlay/left-pane/useTree.svelte.d.ts +14 -0
- package/dist/components/overlay/left-pane/useTree.svelte.js +63 -0
- package/dist/draw.js +24 -9
- package/dist/ecs/index.d.ts +1 -0
- package/dist/ecs/index.js +1 -0
- package/dist/ecs/provideWorldMatrix.svelte.d.ts +8 -0
- package/dist/ecs/provideWorldMatrix.svelte.js +13 -0
- package/dist/ecs/traits.d.ts +41 -50
- package/dist/ecs/traits.js +57 -29
- package/dist/ecs/useTrait.svelte.d.ts +1 -6
- package/dist/ecs/useTrait.svelte.js +21 -13
- package/dist/ecs/worldMatrix.d.ts +10 -0
- package/dist/ecs/worldMatrix.js +138 -0
- package/dist/editing/FrameEditSession.js +31 -18
- package/dist/hooks/use3DModels.svelte.js +1 -1
- package/dist/hooks/useConfigFrames.svelte.js +12 -0
- package/dist/hooks/useDrawAPI.svelte.js +14 -6
- package/dist/hooks/useDrawService.svelte.js +4 -7
- package/dist/hooks/useFrames.svelte.js +23 -11
- package/dist/hooks/useGeometries.svelte.js +11 -3
- package/dist/hooks/usePartConfig.svelte.js +43 -6
- package/dist/hooks/useWorldState.svelte.js +10 -2
- package/dist/plugins/bvh.svelte.js +37 -26
- package/dist/transform.js +55 -21
- package/package.json +3 -3
- package/dist/components/overlay/left-pane/buildTree.d.ts +0 -13
- package/dist/components/overlay/left-pane/buildTree.js +0 -48
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type World } from 'koota';
|
|
2
|
+
/**
|
|
3
|
+
* Wire up listeners that maintain `WorldMatrix` reactively. Subscribes to
|
|
4
|
+
* add/change/remove on `Matrix`, `EditedMatrix`, `LiveMatrix`, and `ChildOf`;
|
|
5
|
+
* enqueues affected entities and flushes on the next microtask.
|
|
6
|
+
*
|
|
7
|
+
* Returns an unsubscribe function. Plain function (not a rune hook) so tests
|
|
8
|
+
* can drive the lifecycle without mounting Svelte.
|
|
9
|
+
*/
|
|
10
|
+
export declare const installWorldMatrixListeners: (world: World) => (() => void);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {} from 'koota';
|
|
2
|
+
import { Matrix4 } from 'three';
|
|
3
|
+
import { composeLocalMatrix } from '../transform';
|
|
4
|
+
import { ChildOf } from './relations';
|
|
5
|
+
import { EditedMatrix, LiveMatrix, Matrix, WorldMatrix } from './traits';
|
|
6
|
+
/**
|
|
7
|
+
* Compute the entity's local-to-parent transform into `out`. Mirrors the
|
|
8
|
+
* blend used by `Frame.svelte` so `WorldMatrix` agrees with the displayed
|
|
9
|
+
* scenegraph.
|
|
10
|
+
*
|
|
11
|
+
* - All three matrix traits present: `live × baseline⁻¹ × edited`.
|
|
12
|
+
* - Otherwise: prefer `EditedMatrix` over `Matrix`.
|
|
13
|
+
*
|
|
14
|
+
* Returns `true` after writing to `out`; returns `false` and leaves `out`
|
|
15
|
+
* untouched when the entity has no matrix-shaped trait.
|
|
16
|
+
*/
|
|
17
|
+
const toLocalMatrix = (entity, out) => {
|
|
18
|
+
const matrix = entity.get(Matrix);
|
|
19
|
+
const editedMatrix = entity.get(EditedMatrix);
|
|
20
|
+
const liveMatrix = entity.get(LiveMatrix);
|
|
21
|
+
if (liveMatrix && matrix && editedMatrix) {
|
|
22
|
+
composeLocalMatrix(liveMatrix, matrix, editedMatrix, out);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (editedMatrix) {
|
|
26
|
+
out.copy(editedMatrix);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (matrix) {
|
|
30
|
+
out.copy(matrix);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Synchronously compute and write `WorldMatrix` for every entity in `dirty`
|
|
37
|
+
* and every descendant via `ChildOf`. Memoizes per-entity world matrices in
|
|
38
|
+
* `cache` so siblings reuse a parent's result. Caller passes a fresh `cache`
|
|
39
|
+
* map per flush.
|
|
40
|
+
*/
|
|
41
|
+
const recomputeWorldMatrix = (world, entity, cache) => {
|
|
42
|
+
if (!entity.isAlive())
|
|
43
|
+
return undefined;
|
|
44
|
+
const cached = cache.get(entity);
|
|
45
|
+
if (cached)
|
|
46
|
+
return cached;
|
|
47
|
+
// Reuse the entity's existing `WorldMatrix` storage when present so a
|
|
48
|
+
// flush doesn't allocate a throwaway matrix per entity. First-time
|
|
49
|
+
// entities get a fresh `Matrix4` that's added as the trait below.
|
|
50
|
+
const out = entity.get(WorldMatrix) ?? new Matrix4();
|
|
51
|
+
const hasLocal = toLocalMatrix(entity, out);
|
|
52
|
+
if (!hasLocal)
|
|
53
|
+
out.identity();
|
|
54
|
+
const parent = entity.targetFor(ChildOf);
|
|
55
|
+
if (parent && parent.isAlive()) {
|
|
56
|
+
const parentWorld = recomputeWorldMatrix(world, parent, cache);
|
|
57
|
+
if (parentWorld)
|
|
58
|
+
out.premultiply(parentWorld);
|
|
59
|
+
}
|
|
60
|
+
cache.set(entity, out);
|
|
61
|
+
return out;
|
|
62
|
+
};
|
|
63
|
+
const flushDirty = (world, dirty) => {
|
|
64
|
+
if (dirty.size === 0)
|
|
65
|
+
return;
|
|
66
|
+
const cache = new Map();
|
|
67
|
+
const expanded = new Set();
|
|
68
|
+
const collect = (entity) => {
|
|
69
|
+
if (expanded.has(entity))
|
|
70
|
+
return;
|
|
71
|
+
expanded.add(entity);
|
|
72
|
+
for (const child of world.query(ChildOf(entity))) {
|
|
73
|
+
collect(child);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
for (const entity of dirty)
|
|
77
|
+
collect(entity);
|
|
78
|
+
dirty.clear();
|
|
79
|
+
for (const entity of expanded) {
|
|
80
|
+
if (!entity.isAlive())
|
|
81
|
+
continue;
|
|
82
|
+
const worldMat = recomputeWorldMatrix(world, entity, cache);
|
|
83
|
+
if (!worldMat)
|
|
84
|
+
continue;
|
|
85
|
+
if (entity.has(WorldMatrix)) {
|
|
86
|
+
entity.changed(WorldMatrix);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
entity.add(WorldMatrix(worldMat));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Wire up listeners that maintain `WorldMatrix` reactively. Subscribes to
|
|
95
|
+
* add/change/remove on `Matrix`, `EditedMatrix`, `LiveMatrix`, and `ChildOf`;
|
|
96
|
+
* enqueues affected entities and flushes on the next microtask.
|
|
97
|
+
*
|
|
98
|
+
* Returns an unsubscribe function. Plain function (not a rune hook) so tests
|
|
99
|
+
* can drive the lifecycle without mounting Svelte.
|
|
100
|
+
*/
|
|
101
|
+
export const installWorldMatrixListeners = (world) => {
|
|
102
|
+
const dirty = new Set();
|
|
103
|
+
let scheduled = false;
|
|
104
|
+
const enqueue = (entity) => {
|
|
105
|
+
dirty.add(entity);
|
|
106
|
+
if (scheduled)
|
|
107
|
+
return;
|
|
108
|
+
scheduled = true;
|
|
109
|
+
queueMicrotask(() => {
|
|
110
|
+
scheduled = false;
|
|
111
|
+
flushDirty(world, dirty);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
for (const entity of world.query(Matrix))
|
|
115
|
+
enqueue(entity);
|
|
116
|
+
for (const entity of world.query(EditedMatrix))
|
|
117
|
+
enqueue(entity);
|
|
118
|
+
for (const entity of world.query(LiveMatrix))
|
|
119
|
+
enqueue(entity);
|
|
120
|
+
const unsubs = [
|
|
121
|
+
world.onAdd(Matrix, enqueue),
|
|
122
|
+
world.onChange(Matrix, enqueue),
|
|
123
|
+
world.onRemove(Matrix, enqueue),
|
|
124
|
+
world.onAdd(EditedMatrix, enqueue),
|
|
125
|
+
world.onChange(EditedMatrix, enqueue),
|
|
126
|
+
world.onRemove(EditedMatrix, enqueue),
|
|
127
|
+
world.onAdd(LiveMatrix, enqueue),
|
|
128
|
+
world.onChange(LiveMatrix, enqueue),
|
|
129
|
+
world.onRemove(LiveMatrix, enqueue),
|
|
130
|
+
world.onAdd(ChildOf, enqueue),
|
|
131
|
+
world.onChange(ChildOf, enqueue),
|
|
132
|
+
world.onRemove(ChildOf, enqueue),
|
|
133
|
+
];
|
|
134
|
+
return () => {
|
|
135
|
+
for (const unsub of unsubs)
|
|
136
|
+
unsub();
|
|
137
|
+
};
|
|
138
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { hierarchy, traits } from '../ecs';
|
|
2
|
-
import { isFinitePose } from '../transform';
|
|
2
|
+
import { createPose, isFinitePose, matrixToPose, poseToMatrix } from '../transform';
|
|
3
|
+
const tempPose = createPose();
|
|
3
4
|
const captureGeometry = (entity) => {
|
|
4
5
|
const box = entity.get(traits.Box);
|
|
5
6
|
if (box)
|
|
@@ -71,13 +72,14 @@ export class FrameEditSession {
|
|
|
71
72
|
this.onClose = onClose;
|
|
72
73
|
for (const entity of entities) {
|
|
73
74
|
const name = entity.get(traits.Name);
|
|
74
|
-
const
|
|
75
|
-
if (!name || !
|
|
75
|
+
const editedMatrix = entity.get(traits.EditedMatrix);
|
|
76
|
+
if (!name || !editedMatrix)
|
|
76
77
|
continue;
|
|
78
|
+
matrixToPose(editedMatrix, tempPose);
|
|
77
79
|
this.snapshots.set(entity, {
|
|
78
80
|
name,
|
|
79
81
|
parent: hierarchy.getParentName(entity) ?? 'world',
|
|
80
|
-
editedPose: { ...
|
|
82
|
+
editedPose: { ...tempPose },
|
|
81
83
|
geometry: captureGeometry(entity),
|
|
82
84
|
});
|
|
83
85
|
}
|
|
@@ -92,11 +94,13 @@ export class FrameEditSession {
|
|
|
92
94
|
const snap = this.snapshots.get(entity);
|
|
93
95
|
if (!snap || this.#closed)
|
|
94
96
|
return;
|
|
95
|
-
const current = entity.get(traits.
|
|
97
|
+
const current = entity.get(traits.EditedMatrix);
|
|
96
98
|
if (!current)
|
|
97
99
|
return;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
matrixToPose(current, tempPose);
|
|
101
|
+
const next = { ...tempPose, ...pose };
|
|
102
|
+
poseToMatrix(next, current);
|
|
103
|
+
entity.changed(traits.EditedMatrix);
|
|
100
104
|
this.updateFrame(snap.name, hierarchy.getParentName(entity) ?? 'world', next, liveGeometry(entity));
|
|
101
105
|
};
|
|
102
106
|
stageGeometry = (entity, geometry) => {
|
|
@@ -116,9 +120,10 @@ export class FrameEditSession {
|
|
|
116
120
|
else if (geometry.type === 'capsule') {
|
|
117
121
|
restoreGeometryTrait(entity, { type: 'capsule', capsule: { r: geometry.r, l: geometry.l } });
|
|
118
122
|
}
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
123
|
+
const editedMatrix = entity.get(traits.EditedMatrix);
|
|
124
|
+
if (editedMatrix) {
|
|
125
|
+
matrixToPose(editedMatrix, tempPose);
|
|
126
|
+
this.updateFrame(snap.name, hierarchy.getParentName(entity) ?? 'world', { ...tempPose }, geometry);
|
|
122
127
|
}
|
|
123
128
|
};
|
|
124
129
|
stageParent = (entity, parent) => {
|
|
@@ -126,9 +131,10 @@ export class FrameEditSession {
|
|
|
126
131
|
if (!snap || this.#closed)
|
|
127
132
|
return;
|
|
128
133
|
hierarchy.setParent(entity, parent === 'world' ? undefined : parent);
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
134
|
+
const editedMatrix = entity.get(traits.EditedMatrix);
|
|
135
|
+
if (editedMatrix) {
|
|
136
|
+
matrixToPose(editedMatrix, tempPose);
|
|
137
|
+
this.updateFrame(snap.name, parent, { ...tempPose }, liveGeometry(entity));
|
|
132
138
|
}
|
|
133
139
|
};
|
|
134
140
|
stageDelete = (entity) => {
|
|
@@ -145,10 +151,13 @@ export class FrameEditSession {
|
|
|
145
151
|
if (this.#closed)
|
|
146
152
|
return false;
|
|
147
153
|
for (const [entity] of this.snapshots) {
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
const matrix = entity.get(traits.EditedMatrix);
|
|
155
|
+
if (matrix) {
|
|
156
|
+
matrixToPose(matrix, tempPose);
|
|
157
|
+
if (!isFinitePose(tempPose)) {
|
|
158
|
+
this.abort();
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
152
161
|
}
|
|
153
162
|
}
|
|
154
163
|
this.#close();
|
|
@@ -163,7 +172,11 @@ export class FrameEditSession {
|
|
|
163
172
|
return;
|
|
164
173
|
for (const [entity, snap] of this.snapshots) {
|
|
165
174
|
if (entity.isAlive()) {
|
|
166
|
-
entity.
|
|
175
|
+
const matrix = entity.get(traits.EditedMatrix);
|
|
176
|
+
if (matrix) {
|
|
177
|
+
poseToMatrix(snap.editedPose, matrix);
|
|
178
|
+
entity.changed(traits.EditedMatrix);
|
|
179
|
+
}
|
|
167
180
|
hierarchy.setParent(entity, snap.parent === 'world' ? undefined : snap.parent);
|
|
168
181
|
restoreGeometryTrait(entity, snap.geometry);
|
|
169
182
|
}
|
|
@@ -48,13 +48,13 @@ export const provide3DModels = (partID) => {
|
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
-
current = next;
|
|
52
51
|
}
|
|
53
52
|
catch (error) {
|
|
54
53
|
// some arms may not implement this api yet
|
|
55
54
|
console.warn(`${client.current.name} returned an error: ${error} when getting 3D models`);
|
|
56
55
|
}
|
|
57
56
|
}
|
|
57
|
+
current = next;
|
|
58
58
|
};
|
|
59
59
|
$effect(() => {
|
|
60
60
|
const shouldFetchModels = settings.isLoaded && settings.current.renderArmModels.includes('model');
|
|
@@ -57,6 +57,18 @@ export const provideConfigFrames = () => {
|
|
|
57
57
|
const frameValues = $derived(Object.values(frames));
|
|
58
58
|
const getParentFrameOptions = (componentName) => {
|
|
59
59
|
const validFrames = new Set(frameValues.map((frame) => frame.referenceFrame));
|
|
60
|
+
/**
|
|
61
|
+
* Fragment components without a mod don't appear in frameValues (we only
|
|
62
|
+
* track frames with explicit $set mods), but the fragment itself supplies
|
|
63
|
+
* their frame so they render in the scene and are valid parents. Exclude
|
|
64
|
+
* any whose frame the user has $unset.
|
|
65
|
+
*/
|
|
66
|
+
const unsetFragmentNames = new Set(fragmentUnsetFrameNames);
|
|
67
|
+
for (const name of Object.keys(partConfig.componentNameToFragmentId)) {
|
|
68
|
+
if (!unsetFragmentNames.has(name)) {
|
|
69
|
+
validFrames.add(name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
60
72
|
validFrames.add('world');
|
|
61
73
|
const frameNameQueue = [componentName];
|
|
62
74
|
while (frameNameQueue.length > 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useThrelte } from '@threlte/core';
|
|
2
2
|
import {} from 'koota';
|
|
3
3
|
import { getContext, setContext } from 'svelte';
|
|
4
|
-
import { Color, Vector3, Vector4 } from 'three';
|
|
4
|
+
import { Color, Matrix4, Vector3, Vector4 } from 'three';
|
|
5
5
|
import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
|
|
6
6
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
7
7
|
import { UuidTool } from 'uuid-tool';
|
|
@@ -11,7 +11,7 @@ import { asRGB, STRIDE } from '../buffer';
|
|
|
11
11
|
import { hierarchy, traits, useWorld } from '../ecs';
|
|
12
12
|
import { createBox, createCapsule, createSphere } from '../geometry';
|
|
13
13
|
import { parsePlyInput } from '../ply';
|
|
14
|
-
import { createPose, createPoseFromFrame } from '../transform';
|
|
14
|
+
import { createPose, createPoseFromFrame, poseToMatrix } from '../transform';
|
|
15
15
|
import { useCameraControls } from './useControls.svelte';
|
|
16
16
|
import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
|
|
17
17
|
import { useLogs } from './useLogs.svelte';
|
|
@@ -126,7 +126,11 @@ export const provideDrawAPI = () => {
|
|
|
126
126
|
const parent = frame.parent;
|
|
127
127
|
const existing = entities.get(name);
|
|
128
128
|
if (existing) {
|
|
129
|
-
existing.
|
|
129
|
+
const matrix = existing.get(traits.Matrix);
|
|
130
|
+
if (matrix) {
|
|
131
|
+
poseToMatrix(pose, matrix);
|
|
132
|
+
existing.changed(traits.Matrix);
|
|
133
|
+
}
|
|
130
134
|
hierarchy.setParent(existing, parent);
|
|
131
135
|
continue;
|
|
132
136
|
}
|
|
@@ -146,7 +150,7 @@ export const provideDrawAPI = () => {
|
|
|
146
150
|
if (frame.geometry) {
|
|
147
151
|
entityTraits.push(geometryTrait());
|
|
148
152
|
}
|
|
149
|
-
entityTraits.push(traits.Name(name), traits.
|
|
153
|
+
entityTraits.push(traits.Name(name), traits.Matrix(poseToMatrix(pose, new Matrix4())), traits.DrawAPI, traits.ReferenceFrame, traits.Removable, traits.ShowAxesHelper);
|
|
150
154
|
const entity = world.spawn(...entityTraits);
|
|
151
155
|
entities.set(name, entity);
|
|
152
156
|
}
|
|
@@ -157,7 +161,11 @@ export const provideDrawAPI = () => {
|
|
|
157
161
|
const pose = createPose(data.center);
|
|
158
162
|
const existing = entities.get(name);
|
|
159
163
|
if (existing) {
|
|
160
|
-
existing.
|
|
164
|
+
const matrix = existing.get(traits.Matrix);
|
|
165
|
+
if (matrix) {
|
|
166
|
+
poseToMatrix(pose, matrix);
|
|
167
|
+
existing.changed(traits.Matrix);
|
|
168
|
+
}
|
|
161
169
|
return;
|
|
162
170
|
}
|
|
163
171
|
const geometryTrait = () => {
|
|
@@ -179,7 +187,7 @@ export const provideDrawAPI = () => {
|
|
|
179
187
|
const entityTraits = [
|
|
180
188
|
traits.Name(data.label ?? ++geometryIndex),
|
|
181
189
|
...hierarchy.parentTraits(parent),
|
|
182
|
-
traits.
|
|
190
|
+
traits.Matrix(poseToMatrix(pose, new Matrix4())),
|
|
183
191
|
traits.Color(colorUtil.set(color)),
|
|
184
192
|
geometryTrait(),
|
|
185
193
|
traits.DrawAPI,
|
|
@@ -10,7 +10,7 @@ import { DrawService } from '../buf/draw/v1/service_connect';
|
|
|
10
10
|
import { CreateRelationshipRequest, DeleteRelationshipRequest, EntityChangeType, StreamEntityChangesResponse, } from '../buf/draw/v1/service_pb';
|
|
11
11
|
import { asFloat32Array, inMeters, STRIDE } from '../buffer';
|
|
12
12
|
import { drawDrawing, drawTransform, updateDrawing, updateModel, updateTransform, uuidStringToBytes, } from '../draw';
|
|
13
|
-
import { traits, useWorld } from '../ecs';
|
|
13
|
+
import { hierarchy, traits, useWorld } from '../ecs';
|
|
14
14
|
import { useCameraControls } from './useControls.svelte';
|
|
15
15
|
import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
|
|
16
16
|
import { useRelationships } from './useRelationships.svelte';
|
|
@@ -50,8 +50,7 @@ export function provideDrawService() {
|
|
|
50
50
|
const entity = drawingEntities.get(uuidStr);
|
|
51
51
|
if (!entity)
|
|
52
52
|
return;
|
|
53
|
-
|
|
54
|
-
entity.destroy();
|
|
53
|
+
hierarchy.destroyEntityTree(world, entity);
|
|
55
54
|
drawingEntities.delete(uuidStr);
|
|
56
55
|
};
|
|
57
56
|
const processEvent = (event) => {
|
|
@@ -334,13 +333,11 @@ export function provideDrawService() {
|
|
|
334
333
|
connectionStatus = ConnectionStatus.DISCONNECTED;
|
|
335
334
|
activeClient = undefined;
|
|
336
335
|
for (const entity of transformEntities.values()) {
|
|
337
|
-
|
|
338
|
-
entity.destroy();
|
|
336
|
+
hierarchy.destroyEntityTree(world, entity);
|
|
339
337
|
}
|
|
340
338
|
transformEntities.clear();
|
|
341
339
|
for (const entity of drawingEntities.values()) {
|
|
342
|
-
|
|
343
|
-
entity.destroy();
|
|
340
|
+
hierarchy.destroyEntityTree(world, entity);
|
|
344
341
|
}
|
|
345
342
|
drawingEntities.clear();
|
|
346
343
|
relationships.clear();
|
|
@@ -2,9 +2,10 @@ import { MachineConnectionEvent, Transform } from '@viamrobotics/sdk';
|
|
|
2
2
|
import { createRobotQuery, useConnectionStatus, useMachineStatus, useRobotClient, } from '@viamrobotics/svelte-sdk';
|
|
3
3
|
import {} from 'koota';
|
|
4
4
|
import { getContext, setContext, untrack } from 'svelte';
|
|
5
|
+
import { Matrix4 } from 'three';
|
|
5
6
|
import { resourceNameToColor, subtypeToColor } from '../color';
|
|
6
7
|
import { hierarchy, traits, useWorld } from '../ecs';
|
|
7
|
-
import { createPose } from '../transform';
|
|
8
|
+
import { createPose, isPoseEqual, poseToMatrix } from '../transform';
|
|
8
9
|
import { useConfigFrames } from './useConfigFrames.svelte';
|
|
9
10
|
import { useEnvironment } from './useEnvironment.svelte';
|
|
10
11
|
import { useFrameEditSession } from './useFrameEditSession.svelte';
|
|
@@ -171,34 +172,45 @@ export const provideFrames = (partID) => {
|
|
|
171
172
|
}
|
|
172
173
|
hierarchy.setParent(existing, parent);
|
|
173
174
|
if (color) {
|
|
174
|
-
existing.
|
|
175
|
+
const cur = existing.get(traits.Color);
|
|
176
|
+
if (!cur || cur.r !== color.r || cur.g !== color.g || cur.b !== color.b) {
|
|
177
|
+
existing.set(traits.Color, color);
|
|
178
|
+
}
|
|
175
179
|
}
|
|
176
|
-
if (center) {
|
|
180
|
+
if (center && !isPoseEqual(existing.get(traits.Center), center)) {
|
|
177
181
|
existing.set(traits.Center, center);
|
|
178
182
|
}
|
|
179
183
|
traits.updateGeometryTrait(existing, frame.physicalObject);
|
|
180
184
|
if (!isEditMode && !partConfig.hasPendingSave) {
|
|
181
|
-
existing.
|
|
185
|
+
const baseline = existing.get(traits.Matrix);
|
|
186
|
+
if (baseline) {
|
|
187
|
+
poseToMatrix(pose, baseline);
|
|
188
|
+
existing.changed(traits.Matrix);
|
|
189
|
+
}
|
|
182
190
|
}
|
|
183
|
-
if (!existing.has(traits.
|
|
184
|
-
existing.add(traits.
|
|
191
|
+
if (!existing.has(traits.LiveMatrix)) {
|
|
192
|
+
existing.add(traits.LiveMatrix(poseToMatrix(pose, new Matrix4())));
|
|
185
193
|
}
|
|
186
|
-
// Skip the
|
|
194
|
+
// Skip the EditedMatrix overwrite while in edit mode. The merged
|
|
187
195
|
// `frames` source can differ from query.data once didRecentlyEdit
|
|
188
196
|
// flips (fragment overrides, round-trip drift), and writing those
|
|
189
197
|
// values would shift entities whose parents the user is portaling
|
|
190
198
|
// into — the gizmo's drag target moves underneath it. Once we're
|
|
191
199
|
// back in monitor mode, the next sync resumes the overwrite.
|
|
192
200
|
if (!isEditMode) {
|
|
193
|
-
existing.
|
|
201
|
+
const edited = existing.get(traits.EditedMatrix);
|
|
202
|
+
if (edited) {
|
|
203
|
+
poseToMatrix(pose, edited);
|
|
204
|
+
existing.changed(traits.EditedMatrix);
|
|
205
|
+
}
|
|
194
206
|
}
|
|
195
207
|
continue;
|
|
196
208
|
}
|
|
197
209
|
const entityTraits = [
|
|
198
210
|
traits.Name(name),
|
|
199
|
-
traits.
|
|
200
|
-
traits.
|
|
201
|
-
traits.
|
|
211
|
+
traits.Matrix(poseToMatrix(pose, new Matrix4())),
|
|
212
|
+
traits.EditedMatrix(poseToMatrix(pose, new Matrix4())),
|
|
213
|
+
traits.LiveMatrix(poseToMatrix(pose, new Matrix4())),
|
|
202
214
|
traits.FramesAPI,
|
|
203
215
|
traits.Transformable,
|
|
204
216
|
traits.ShowAxesHelper,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArmClient, BaseClient, CameraClient, GantryClient, GripperClient } from '@viamrobotics/sdk';
|
|
1
|
+
import { ArmClient, BaseClient, CameraClient, GantryClient, GenericComponentClient, GripperClient, } from '@viamrobotics/sdk';
|
|
2
2
|
import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
|
|
3
3
|
import {} from 'koota';
|
|
4
4
|
import { getContext, setContext, untrack } from 'svelte';
|
|
@@ -7,7 +7,7 @@ import { resourceColors } from '../color';
|
|
|
7
7
|
import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
|
|
8
8
|
import { hierarchy, traits, useWorld } from '../ecs';
|
|
9
9
|
import { updateGeometryTrait } from '../ecs/traits';
|
|
10
|
-
import { createPose } from '../transform';
|
|
10
|
+
import { createPose, isPoseEqual } from '../transform';
|
|
11
11
|
import { useEnvironment } from './useEnvironment.svelte';
|
|
12
12
|
import { useLogs } from './useLogs.svelte';
|
|
13
13
|
import { useResourceByName } from './useResourceByName.svelte';
|
|
@@ -24,6 +24,7 @@ export const provideGeometries = (partID) => {
|
|
|
24
24
|
const cameras = useResourceNames(partID, 'camera');
|
|
25
25
|
const grippers = useResourceNames(partID, 'gripper');
|
|
26
26
|
const gantries = useResourceNames(partID, 'gantry');
|
|
27
|
+
const generics = useResourceNames(partID, 'generic');
|
|
27
28
|
const settings = useSettings();
|
|
28
29
|
const { refreshRates } = $derived(settings.current);
|
|
29
30
|
const armClients = $derived(arms.current.map((arm) => createResourceClient(ArmClient, partID, () => arm.name)));
|
|
@@ -31,6 +32,9 @@ export const provideGeometries = (partID) => {
|
|
|
31
32
|
const gripperClients = $derived(grippers.current.map((gripper) => createResourceClient(GripperClient, partID, () => gripper.name)));
|
|
32
33
|
const cameraClients = $derived(cameras.current.map((camera) => createResourceClient(CameraClient, partID, () => camera.name)));
|
|
33
34
|
const gantryClients = $derived(gantries.current.map((gantry) => createResourceClient(GantryClient, partID, () => gantry.name)));
|
|
35
|
+
const genericClients = $derived(generics.current
|
|
36
|
+
.filter((generic) => generic.type === 'component')
|
|
37
|
+
.map((generic) => createResourceClient(GenericComponentClient, partID, () => generic.name)));
|
|
34
38
|
const interval = $derived(refreshRates[RefreshRates.poses]);
|
|
35
39
|
const options = $derived({
|
|
36
40
|
enabled: interval !== RefetchRates.OFF && environment.current.viewerMode === 'monitor',
|
|
@@ -41,12 +45,14 @@ export const provideGeometries = (partID) => {
|
|
|
41
45
|
const gripperQueries = $derived(gripperClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
|
|
42
46
|
const cameraQueries = $derived(cameraClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
|
|
43
47
|
const gantryQueries = $derived(gantryClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
|
|
48
|
+
const genericQueries = $derived(genericClients.map((client) => [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)]));
|
|
44
49
|
const queries = $derived([
|
|
45
50
|
...armQueries,
|
|
46
51
|
...baseQueries,
|
|
47
52
|
...gripperQueries,
|
|
48
53
|
...cameraQueries,
|
|
49
54
|
...gantryQueries,
|
|
55
|
+
...genericQueries,
|
|
50
56
|
]);
|
|
51
57
|
$effect(() => {
|
|
52
58
|
if (interval === RefetchRates.FPS_30 || interval === RefetchRates.FPS_60) {
|
|
@@ -91,7 +97,9 @@ export const provideGeometries = (partID) => {
|
|
|
91
97
|
const existing = entities.get(entityKey);
|
|
92
98
|
if (existing) {
|
|
93
99
|
hierarchy.setParent(existing, name);
|
|
94
|
-
existing.
|
|
100
|
+
if (!isPoseEqual(existing.get(traits.Center), center)) {
|
|
101
|
+
existing.set(traits.Center, center);
|
|
102
|
+
}
|
|
95
103
|
updateGeometryTrait(existing, geometry);
|
|
96
104
|
continue;
|
|
97
105
|
}
|
|
@@ -8,7 +8,7 @@ export const providePartConfig = (partID, params) => {
|
|
|
8
8
|
const props = $derived(params());
|
|
9
9
|
const config = $derived(props ? useEmbeddedPartConfig(props) : useStandalonePartConfig(partID));
|
|
10
10
|
const getCurrent = () => {
|
|
11
|
-
return (config.current
|
|
11
|
+
return (config.current?.toJson?.() ?? { components: [] });
|
|
12
12
|
};
|
|
13
13
|
const current = $derived(getCurrent());
|
|
14
14
|
const createFragmentFrame = (fragmentId, componentName) => {
|
|
@@ -204,9 +204,40 @@ export const usePartConfig = () => {
|
|
|
204
204
|
return getContext(key);
|
|
205
205
|
};
|
|
206
206
|
const useEmbeddedPartConfig = (props) => {
|
|
207
|
+
let hasPendingSave = $state(false);
|
|
208
|
+
let prevIsDirty = false;
|
|
209
|
+
let cleanSnapshot;
|
|
210
|
+
const snapshot = (current) => {
|
|
211
|
+
const json = current?.toJson?.();
|
|
212
|
+
return json === undefined ? undefined : JSON.stringify(json);
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* The host app owns saving, and we aren't notified directly. Set hasPendingSave
|
|
216
|
+
* to watch isDirty: true -> false transitions, representing a save.
|
|
217
|
+
*
|
|
218
|
+
* `useFrames` clears the flag on the next `revision` change
|
|
219
|
+
* once the server reports the new framesystem.
|
|
220
|
+
*/
|
|
221
|
+
$effect.pre(() => {
|
|
222
|
+
const dirty = props.isDirty;
|
|
223
|
+
const current = props.current;
|
|
224
|
+
if (prevIsDirty && !dirty) {
|
|
225
|
+
const next = snapshot(current);
|
|
226
|
+
if (next !== undefined && cleanSnapshot !== undefined && next !== cleanSnapshot) {
|
|
227
|
+
hasPendingSave = true;
|
|
228
|
+
}
|
|
229
|
+
cleanSnapshot = next;
|
|
230
|
+
}
|
|
231
|
+
else if (!prevIsDirty && !dirty) {
|
|
232
|
+
cleanSnapshot = snapshot(current);
|
|
233
|
+
}
|
|
234
|
+
prevIsDirty = dirty;
|
|
235
|
+
});
|
|
207
236
|
return {
|
|
208
237
|
hasEditPermissions: true,
|
|
209
|
-
hasPendingSave
|
|
238
|
+
get hasPendingSave() {
|
|
239
|
+
return hasPendingSave;
|
|
240
|
+
},
|
|
210
241
|
get isDirty() {
|
|
211
242
|
return props.isDirty;
|
|
212
243
|
},
|
|
@@ -220,8 +251,12 @@ const useEmbeddedPartConfig = (props) => {
|
|
|
220
251
|
const struct = Struct.fromJson(config);
|
|
221
252
|
return props.setLocalPartConfig(struct);
|
|
222
253
|
},
|
|
223
|
-
clearPendingSave() {
|
|
224
|
-
|
|
254
|
+
clearPendingSave() {
|
|
255
|
+
hasPendingSave = false;
|
|
256
|
+
},
|
|
257
|
+
setPendingSave() {
|
|
258
|
+
hasPendingSave = true;
|
|
259
|
+
},
|
|
225
260
|
};
|
|
226
261
|
};
|
|
227
262
|
const useStandalonePartConfig = (partID) => {
|
|
@@ -278,10 +313,12 @@ const useStandalonePartConfig = (partID) => {
|
|
|
278
313
|
const id = partID();
|
|
279
314
|
if (lastPartID !== undefined && lastPartID !== id) {
|
|
280
315
|
// Part changed: drop any in-memory edits/pending-save state from the
|
|
281
|
-
// previous part
|
|
282
|
-
//
|
|
316
|
+
// previous part, and clear `current` so consumers don't keep
|
|
317
|
+
// rendering the old config's frames while the new part loads
|
|
318
|
+
// (offline parts may never load, leaving the old frames forever).
|
|
283
319
|
isDirty = false;
|
|
284
320
|
hasPendingSave = false;
|
|
321
|
+
current = undefined;
|
|
285
322
|
}
|
|
286
323
|
lastPartID = id;
|
|
287
324
|
if (!networkPartConfig || isDirty) {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { useThrelte } from '@threlte/core';
|
|
2
2
|
import { Struct, TransformChangeType, WorldStateStoreClient, } from '@viamrobotics/sdk';
|
|
3
3
|
import { createResourceClient, createResourceQuery, createResourceStream, useResourceNames, } from '@viamrobotics/svelte-sdk';
|
|
4
|
+
import { Matrix4 } from 'three';
|
|
4
5
|
import { asFloat32Array, inMeters } from '../buffer';
|
|
5
6
|
import { createChunkLoader } from '../chunking';
|
|
6
7
|
import { drawTransform, updateMetadata } from '../draw';
|
|
7
8
|
import { traits, useWorld } from '../ecs';
|
|
8
9
|
import { isPointCloud } from '../geometry';
|
|
9
10
|
import { metadataFromStruct } from '../metadata';
|
|
10
|
-
import { createPose } from '../transform';
|
|
11
|
+
import { createPose, poseToMatrix } from '../transform';
|
|
11
12
|
import { usePartID } from './usePartID.svelte';
|
|
12
13
|
import { useRelationships } from './useRelationships.svelte';
|
|
13
14
|
export const provideWorldStates = () => {
|
|
@@ -133,7 +134,14 @@ const createWorldState = (client) => {
|
|
|
133
134
|
for (const path of changes) {
|
|
134
135
|
if (typeof path === 'string') {
|
|
135
136
|
if (path.startsWith('poseInObserverFrame.pose')) {
|
|
136
|
-
entity.
|
|
137
|
+
const matrix = entity.get(traits.Matrix);
|
|
138
|
+
if (matrix) {
|
|
139
|
+
poseToMatrix(createPose(transform.poseInObserverFrame?.pose), matrix);
|
|
140
|
+
entity.changed(traits.Matrix);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
entity.add(traits.Matrix(poseToMatrix(createPose(transform.poseInObserverFrame?.pose), new Matrix4())));
|
|
144
|
+
}
|
|
137
145
|
}
|
|
138
146
|
else if (path.startsWith('physicalObject') && transform.physicalObject) {
|
|
139
147
|
traits.updateGeometryTrait(entity, transform.physicalObject);
|