@viamrobotics/motion-tools 1.19.1 → 1.22.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.d.ts +0 -1
- package/dist/FrameConfigUpdater.svelte.js +6 -24
- package/dist/buf/draw/v1/metadata_pb.d.ts +39 -0
- package/dist/buf/draw/v1/metadata_pb.js +55 -0
- package/dist/buf/draw/v1/service_connect.d.ts +34 -1
- package/dist/buf/draw/v1/service_connect.js +34 -1
- package/dist/buf/draw/v1/service_pb.d.ts +136 -0
- package/dist/buf/draw/v1/service_pb.js +201 -0
- package/dist/components/Entities/Arrows/ArrowGroups.svelte +1 -0
- package/dist/components/Entities/Arrows/Arrows.svelte +1 -1
- package/dist/components/Entities/Points.svelte +23 -23
- package/dist/components/Entities/Pose.svelte +18 -13
- package/dist/components/Entities/hooks/useEntityEvents.svelte.js +18 -1
- package/dist/components/FileDrop/FileDrop.svelte +8 -1
- package/dist/components/FileDrop/useFileDrop.svelte.js +16 -2
- package/dist/components/PCD.svelte +9 -1
- package/dist/components/PCD.svelte.d.ts +2 -0
- package/dist/components/PointerMissBox.svelte +1 -1
- package/dist/components/Scene.svelte +2 -0
- package/dist/components/SceneProviders.svelte +4 -0
- package/dist/components/SelectedTransformControls.svelte +227 -0
- package/dist/components/SelectedTransformControls.svelte.d.ts +3 -0
- package/dist/components/Snapshot.svelte +12 -7
- package/dist/components/StaticGeometries.svelte +3 -56
- package/dist/components/overlay/AddRelationship.svelte +25 -3
- package/dist/components/overlay/Details.svelte +290 -229
- package/dist/components/overlay/dashboard/Button.svelte +4 -2
- package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
- package/dist/components/overlay/dashboard/Dashboard.svelte +43 -33
- package/dist/draw.d.ts +22 -9
- package/dist/draw.js +71 -41
- package/dist/ecs/relations.js +1 -1
- package/dist/ecs/traits.d.ts +17 -0
- package/dist/ecs/traits.js +9 -0
- package/dist/editing/FrameEditSession.d.ts +37 -0
- package/dist/editing/FrameEditSession.js +178 -0
- package/dist/hooks/useDrawService.svelte.d.ts +2 -0
- package/dist/hooks/useDrawService.svelte.js +139 -20
- package/dist/hooks/useFrameEditSession.svelte.d.ts +15 -0
- package/dist/hooks/useFrameEditSession.svelte.js +36 -0
- package/dist/hooks/useFrames.svelte.js +37 -2
- package/dist/hooks/usePartConfig.svelte.js +10 -0
- package/dist/hooks/useRelationships.svelte.d.ts +12 -0
- package/dist/hooks/useRelationships.svelte.js +78 -0
- package/dist/hooks/useSettings.svelte.d.ts +1 -2
- package/dist/hooks/useSettings.svelte.js +1 -2
- package/dist/hooks/useWorldState.svelte.js +10 -4
- package/dist/metadata.d.ts +7 -3
- package/dist/metadata.js +26 -2
- package/dist/snapshot.d.ts +6 -1
- package/dist/snapshot.js +10 -5
- package/dist/transform.js +13 -0
- package/package.json +7 -4
|
@@ -5,13 +5,17 @@ import { useThrelte } from '@threlte/core';
|
|
|
5
5
|
import {} from 'koota';
|
|
6
6
|
import { getContext, setContext } from 'svelte';
|
|
7
7
|
import { UuidTool } from 'uuid-tool';
|
|
8
|
+
import { writeBufferGeometryRange } from '../attribute';
|
|
8
9
|
import { DrawService } from '../buf/draw/v1/service_connect';
|
|
9
|
-
import { EntityChangeType, StreamEntityChangesResponse } from '../buf/draw/v1/service_pb';
|
|
10
|
-
import {
|
|
10
|
+
import { CreateRelationshipRequest, DeleteRelationshipRequest, EntityChangeType, StreamEntityChangesResponse, } from '../buf/draw/v1/service_pb';
|
|
11
|
+
import { asFloat32Array, inMeters, STRIDE } from '../buffer';
|
|
12
|
+
import { drawDrawing, drawTransform, updateDrawing, updateModel, updateTransform, uuidStringToBytes, } from '../draw';
|
|
11
13
|
import { traits, useWorld } from '../ecs';
|
|
12
14
|
import { useCameraControls } from './useControls.svelte';
|
|
13
15
|
import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
|
|
16
|
+
import { useRelationships } from './useRelationships.svelte';
|
|
14
17
|
const DRAW_SERVICE_KEY = Symbol('draw-service-context');
|
|
18
|
+
const FLOAT32_SIZE = 4;
|
|
15
19
|
const ConnectionStatus = {
|
|
16
20
|
CONNECTED: 'connected',
|
|
17
21
|
DISCONNECTED: 'disconnected',
|
|
@@ -22,6 +26,7 @@ export function provideDrawService() {
|
|
|
22
26
|
const world = useWorld();
|
|
23
27
|
const cameraControls = useCameraControls();
|
|
24
28
|
const drawConnectionConfig = useDrawConnectionConfig();
|
|
29
|
+
const relationships = useRelationships();
|
|
25
30
|
let connectionStatus = $state(ConnectionStatus.DISCONNECTED);
|
|
26
31
|
const url = $derived(drawConnectionConfig.current?.backendIP
|
|
27
32
|
? `http://${drawConnectionConfig.current.backendIP}:3030`
|
|
@@ -30,6 +35,9 @@ export function provideDrawService() {
|
|
|
30
35
|
const drawingEntities = new Map();
|
|
31
36
|
let pendingEvents = [];
|
|
32
37
|
let flushScheduled = false;
|
|
38
|
+
let activeClient;
|
|
39
|
+
let activeSignal;
|
|
40
|
+
const activeChunkPulls = new Set();
|
|
33
41
|
const destroyTransform = (uuidStr) => {
|
|
34
42
|
const entity = transformEntities.get(uuidStr);
|
|
35
43
|
if (!entity)
|
|
@@ -39,13 +47,11 @@ export function provideDrawService() {
|
|
|
39
47
|
transformEntities.delete(uuidStr);
|
|
40
48
|
};
|
|
41
49
|
const destroyDrawing = (uuidStr) => {
|
|
42
|
-
const
|
|
43
|
-
if (!
|
|
50
|
+
const entity = drawingEntities.get(uuidStr);
|
|
51
|
+
if (!entity)
|
|
44
52
|
return;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
entity.destroy();
|
|
48
|
-
}
|
|
53
|
+
if (world.has(entity))
|
|
54
|
+
entity.destroy();
|
|
49
55
|
drawingEntities.delete(uuidStr);
|
|
50
56
|
};
|
|
51
57
|
const processEvent = (event) => {
|
|
@@ -62,7 +68,9 @@ export function provideDrawService() {
|
|
|
62
68
|
if (changeType === EntityChangeType.ADDED) {
|
|
63
69
|
if (!transformEntities.has(uuid)) {
|
|
64
70
|
const spawned = drawTransform(world, transform, traits.DrawServiceAPI);
|
|
65
|
-
|
|
71
|
+
relationships.apply(spawned.entity, spawned.relationships);
|
|
72
|
+
transformEntities.set(uuid, spawned.entity);
|
|
73
|
+
relationships.flush(uuid);
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
76
|
else if (changeType === EntityChangeType.REMOVED) {
|
|
@@ -71,11 +79,76 @@ export function provideDrawService() {
|
|
|
71
79
|
else if (changeType === EntityChangeType.UPDATED) {
|
|
72
80
|
const existing = transformEntities.get(uuid);
|
|
73
81
|
if (existing) {
|
|
74
|
-
updateTransform(existing, transform);
|
|
82
|
+
const updated = updateTransform(existing, transform);
|
|
83
|
+
relationships.apply(updated.entity, updated.relationships);
|
|
75
84
|
}
|
|
76
85
|
else {
|
|
77
86
|
const spawned = drawTransform(world, transform, traits.DrawServiceAPI);
|
|
78
|
-
|
|
87
|
+
relationships.apply(spawned.entity, spawned.relationships);
|
|
88
|
+
transformEntities.set(uuid, spawned.entity);
|
|
89
|
+
relationships.flush(uuid);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const isChunkedDrawing = (drawing) => {
|
|
94
|
+
return drawing.metadata?.chunks !== undefined && drawing.metadata.chunks.total > 0;
|
|
95
|
+
};
|
|
96
|
+
const getChunkInfo = (drawing) => {
|
|
97
|
+
const meta = drawing.metadata?.chunks;
|
|
98
|
+
if (!meta || meta.total === 0)
|
|
99
|
+
return undefined;
|
|
100
|
+
const shape = drawing.physicalObject?.geometryType;
|
|
101
|
+
if (shape?.case === 'points') {
|
|
102
|
+
const chunkElements = shape.value.positions.length / (STRIDE.POSITIONS * FLOAT32_SIZE);
|
|
103
|
+
return {
|
|
104
|
+
total: meta.total,
|
|
105
|
+
firstEnd: chunkElements,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
};
|
|
110
|
+
const pullChunks = async (client, uuid, uuidBytes, entity, totalElements, firstChunkEnd, signal) => {
|
|
111
|
+
if (activeChunkPulls.has(uuid))
|
|
112
|
+
return;
|
|
113
|
+
activeChunkPulls.add(uuid);
|
|
114
|
+
try {
|
|
115
|
+
let nextStart = firstChunkEnd;
|
|
116
|
+
while (!signal.aborted) {
|
|
117
|
+
const response = await client.getEntityChunk({ uuid: uuidBytes, start: nextStart }, { signal });
|
|
118
|
+
// done with no payload is the server's "past end" sentinel (startByte >= posLen), not the final real chunk
|
|
119
|
+
if (response.done && !response.entity.value)
|
|
120
|
+
break;
|
|
121
|
+
const drawing = response.entity.case === 'drawing' ? response.entity.value : undefined;
|
|
122
|
+
if (!drawing)
|
|
123
|
+
break;
|
|
124
|
+
const shape = drawing.physicalObject?.geometryType;
|
|
125
|
+
if (shape?.case !== 'points')
|
|
126
|
+
break;
|
|
127
|
+
const buffer = entity.get(traits.BufferGeometry);
|
|
128
|
+
if (!buffer)
|
|
129
|
+
break;
|
|
130
|
+
const positions = asFloat32Array(shape.value.positions, inMeters);
|
|
131
|
+
const metadata = drawing.metadata;
|
|
132
|
+
if (!metadata)
|
|
133
|
+
break;
|
|
134
|
+
writeBufferGeometryRange(buffer, positions, response.start, metadata);
|
|
135
|
+
const chunkElements = positions.length / 3;
|
|
136
|
+
nextStart = response.start + chunkElements;
|
|
137
|
+
entity.set(traits.ChunkProgress, { loaded: nextStart, total: totalElements });
|
|
138
|
+
invalidate();
|
|
139
|
+
if (response.done)
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (!signal.aborted) {
|
|
145
|
+
console.error(`Chunk pull failed for entity ${uuid}:`, error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
activeChunkPulls.delete(uuid);
|
|
150
|
+
if (world.has(entity)) {
|
|
151
|
+
entity.remove(traits.ChunkProgress);
|
|
79
152
|
}
|
|
80
153
|
}
|
|
81
154
|
};
|
|
@@ -83,7 +156,17 @@ export function provideDrawService() {
|
|
|
83
156
|
if (changeType === EntityChangeType.ADDED) {
|
|
84
157
|
if (!drawingEntities.has(uuid)) {
|
|
85
158
|
const spawned = drawDrawing(world, drawing, traits.DrawServiceAPI);
|
|
86
|
-
|
|
159
|
+
relationships.apply(spawned.entity, spawned.relationships);
|
|
160
|
+
drawingEntities.set(uuid, spawned.entity);
|
|
161
|
+
relationships.flush(uuid);
|
|
162
|
+
if (isChunkedDrawing(drawing) && activeClient && activeSignal) {
|
|
163
|
+
const chunk = getChunkInfo(drawing);
|
|
164
|
+
if (chunk) {
|
|
165
|
+
spawned.entity.add(traits.ChunkProgress({ loaded: chunk.firstEnd, total: chunk.total }));
|
|
166
|
+
const uuidBytes = drawing.uuid ?? new Uint8Array();
|
|
167
|
+
void pullChunks(activeClient, uuid, uuidBytes, spawned.entity, chunk.total, chunk.firstEnd, activeSignal);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
87
170
|
}
|
|
88
171
|
}
|
|
89
172
|
else if (changeType === EntityChangeType.REMOVED) {
|
|
@@ -92,12 +175,18 @@ export function provideDrawService() {
|
|
|
92
175
|
else if (changeType === EntityChangeType.UPDATED) {
|
|
93
176
|
const existing = drawingEntities.get(uuid);
|
|
94
177
|
if (existing) {
|
|
95
|
-
const
|
|
96
|
-
|
|
178
|
+
const isModel = drawing.physicalObject?.geometryType?.case === 'model';
|
|
179
|
+
const result = isModel
|
|
180
|
+
? updateModel(world, existing, drawing, traits.DrawServiceAPI)
|
|
181
|
+
: updateDrawing(world, existing, drawing);
|
|
182
|
+
relationships.apply(result.entity, result.relationships);
|
|
183
|
+
drawingEntities.set(uuid, result.entity);
|
|
97
184
|
}
|
|
98
185
|
else {
|
|
99
186
|
const spawned = drawDrawing(world, drawing, traits.DrawServiceAPI);
|
|
100
|
-
|
|
187
|
+
relationships.apply(spawned.entity, spawned.relationships);
|
|
188
|
+
drawingEntities.set(uuid, spawned.entity);
|
|
189
|
+
relationships.flush(uuid);
|
|
101
190
|
}
|
|
102
191
|
}
|
|
103
192
|
};
|
|
@@ -201,38 +290,68 @@ export function provideDrawService() {
|
|
|
201
290
|
}
|
|
202
291
|
}
|
|
203
292
|
};
|
|
293
|
+
const createRelationship = async (sourceUuid, targetUuid, type, indexMapping) => {
|
|
294
|
+
if (!activeClient)
|
|
295
|
+
return;
|
|
296
|
+
const rel = {
|
|
297
|
+
targetUuid: uuidStringToBytes(targetUuid),
|
|
298
|
+
type,
|
|
299
|
+
};
|
|
300
|
+
if (indexMapping !== undefined) {
|
|
301
|
+
rel.indexMapping = indexMapping;
|
|
302
|
+
}
|
|
303
|
+
await activeClient.createRelationship(new CreateRelationshipRequest({
|
|
304
|
+
sourceUuid: uuidStringToBytes(sourceUuid),
|
|
305
|
+
relationship: rel,
|
|
306
|
+
}));
|
|
307
|
+
};
|
|
308
|
+
const deleteRelationship = async (sourceUuid, targetUuid) => {
|
|
309
|
+
if (!activeClient)
|
|
310
|
+
return;
|
|
311
|
+
await activeClient.deleteRelationship(new DeleteRelationshipRequest({
|
|
312
|
+
sourceUuid: uuidStringToBytes(sourceUuid),
|
|
313
|
+
targetUuid: uuidStringToBytes(targetUuid),
|
|
314
|
+
}));
|
|
315
|
+
};
|
|
204
316
|
$effect(() => {
|
|
205
317
|
if (!url) {
|
|
206
318
|
connectionStatus = ConnectionStatus.DISCONNECTED;
|
|
319
|
+
activeClient = undefined;
|
|
207
320
|
return;
|
|
208
321
|
}
|
|
209
322
|
const controller = new AbortController();
|
|
210
323
|
connectionStatus = ConnectionStatus.CONNECTING;
|
|
211
324
|
const transport = createConnectTransport({ baseUrl: url });
|
|
212
325
|
const client = createClient(DrawService, transport);
|
|
326
|
+
activeClient = client;
|
|
327
|
+
activeSignal = controller.signal;
|
|
213
328
|
void streamEntityChanges(client, controller.signal);
|
|
214
329
|
void streamSceneChanges(client, controller.signal);
|
|
215
330
|
return () => {
|
|
216
331
|
controller.abort();
|
|
332
|
+
activeClient = undefined;
|
|
333
|
+
activeSignal = undefined;
|
|
217
334
|
connectionStatus = ConnectionStatus.DISCONNECTED;
|
|
335
|
+
activeClient = undefined;
|
|
218
336
|
for (const entity of transformEntities.values()) {
|
|
219
337
|
if (world.has(entity))
|
|
220
338
|
entity.destroy();
|
|
221
339
|
}
|
|
222
340
|
transformEntities.clear();
|
|
223
|
-
for (const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
entity.destroy();
|
|
227
|
-
}
|
|
341
|
+
for (const entity of drawingEntities.values()) {
|
|
342
|
+
if (world.has(entity))
|
|
343
|
+
entity.destroy();
|
|
228
344
|
}
|
|
229
345
|
drawingEntities.clear();
|
|
346
|
+
relationships.clear();
|
|
230
347
|
};
|
|
231
348
|
});
|
|
232
349
|
setContext(DRAW_SERVICE_KEY, {
|
|
233
350
|
get connectionStatus() {
|
|
234
351
|
return connectionStatus;
|
|
235
352
|
},
|
|
353
|
+
createRelationship,
|
|
354
|
+
deleteRelationship,
|
|
236
355
|
});
|
|
237
356
|
}
|
|
238
357
|
export function useDrawService() {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Entity } from 'koota';
|
|
2
|
+
import { FrameEditSession } from '../editing/FrameEditSession';
|
|
3
|
+
interface FrameEditSessionContext {
|
|
4
|
+
/** The currently-active session, or undefined when no gesture is in flight. */
|
|
5
|
+
current: FrameEditSession | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Open a new session over the given entities. If a previous session is still
|
|
8
|
+
* active (e.g. selection changed mid-drag and onMouseUp never fired), it is
|
|
9
|
+
* aborted first so its snapshot is restored.
|
|
10
|
+
*/
|
|
11
|
+
begin: (entities: Entity[]) => FrameEditSession;
|
|
12
|
+
}
|
|
13
|
+
export declare const provideFrameEditSession: (partID: () => string) => void;
|
|
14
|
+
export declare const useFrameEditSession: () => FrameEditSessionContext;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
import { FrameEditSession } from '../editing/FrameEditSession';
|
|
3
|
+
import { usePartConfig } from './usePartConfig.svelte';
|
|
4
|
+
const key = Symbol('frame-edit-session-context');
|
|
5
|
+
export const provideFrameEditSession = (partID) => {
|
|
6
|
+
const partConfig = usePartConfig();
|
|
7
|
+
let active = $state(undefined);
|
|
8
|
+
const begin = (entities) => {
|
|
9
|
+
active?.abort();
|
|
10
|
+
const session = new FrameEditSession(entities, partConfig.updateFrame, partConfig.deleteFrame, () => {
|
|
11
|
+
if (active === session)
|
|
12
|
+
active = undefined;
|
|
13
|
+
});
|
|
14
|
+
active = session;
|
|
15
|
+
return session;
|
|
16
|
+
};
|
|
17
|
+
// Drop any in-flight session when the partID changes — its snapshots reference
|
|
18
|
+
// entities from the old world that useFrames will destroy, and aborting it
|
|
19
|
+
// after the swap would write old frame names into the new part's config.
|
|
20
|
+
let lastPartID;
|
|
21
|
+
$effect.pre(() => {
|
|
22
|
+
const id = partID();
|
|
23
|
+
if (lastPartID !== undefined && lastPartID !== id) {
|
|
24
|
+
active?.abort();
|
|
25
|
+
active = undefined;
|
|
26
|
+
}
|
|
27
|
+
lastPartID = id;
|
|
28
|
+
});
|
|
29
|
+
setContext(key, {
|
|
30
|
+
get current() {
|
|
31
|
+
return active;
|
|
32
|
+
},
|
|
33
|
+
begin,
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
export const useFrameEditSession = () => getContext(key);
|
|
@@ -7,6 +7,7 @@ import { traits, useWorld } from '../ecs';
|
|
|
7
7
|
import { createPose } from '../transform';
|
|
8
8
|
import { useConfigFrames } from './useConfigFrames.svelte';
|
|
9
9
|
import { useEnvironment } from './useEnvironment.svelte';
|
|
10
|
+
import { useFrameEditSession } from './useFrameEditSession.svelte';
|
|
10
11
|
import { useLogs } from './useLogs.svelte';
|
|
11
12
|
import { usePartConfig } from './usePartConfig.svelte';
|
|
12
13
|
import { useResourceByName } from './useResourceByName.svelte';
|
|
@@ -14,6 +15,7 @@ const key = Symbol('frames-context');
|
|
|
14
15
|
export const provideFrames = (partID) => {
|
|
15
16
|
const configFrames = useConfigFrames();
|
|
16
17
|
const partConfig = usePartConfig();
|
|
18
|
+
const editSession = useFrameEditSession();
|
|
17
19
|
const environment = useEnvironment();
|
|
18
20
|
const world = useWorld();
|
|
19
21
|
const resourceByName = useResourceByName();
|
|
@@ -23,6 +25,16 @@ export const provideFrames = (partID) => {
|
|
|
23
25
|
const logs = useLogs();
|
|
24
26
|
const pendingSaveKey = $derived(`viam-pending-save-revision:${partID()}`);
|
|
25
27
|
let didRecentlyEdit = $state(false);
|
|
28
|
+
let lastPartID;
|
|
29
|
+
$effect.pre(() => {
|
|
30
|
+
const id = partID();
|
|
31
|
+
if (lastPartID !== undefined && lastPartID !== id) {
|
|
32
|
+
// Stale across parts: keeps the configFrames-priority merge branch
|
|
33
|
+
// active when switching to a new part that hasn't been edited.
|
|
34
|
+
didRecentlyEdit = false;
|
|
35
|
+
}
|
|
36
|
+
lastPartID = id;
|
|
37
|
+
});
|
|
26
38
|
const isEditMode = $derived(environment.current.viewerMode === 'edit');
|
|
27
39
|
const query = createRobotQuery(client, 'frameSystemConfig', () => ({
|
|
28
40
|
refetchOnWindowFocus: false,
|
|
@@ -73,7 +85,7 @@ export const provideFrames = (partID) => {
|
|
|
73
85
|
});
|
|
74
86
|
const current = $derived(Object.values(frames));
|
|
75
87
|
const entities = new Map();
|
|
76
|
-
$effect
|
|
88
|
+
$effect(() => {
|
|
77
89
|
if (revision) {
|
|
78
90
|
untrack(() => query.refetch());
|
|
79
91
|
}
|
|
@@ -145,6 +157,13 @@ export const provideFrames = (partID) => {
|
|
|
145
157
|
subtypeToColor(currentComponentSubtypeByName[frame.referenceFrame]);
|
|
146
158
|
const existing = entities.get(entityKey);
|
|
147
159
|
if (existing) {
|
|
160
|
+
// Active edit session owns the entity's traits for the duration of
|
|
161
|
+
// the user's gesture. Skip the entire re-sync — re-setting Parent
|
|
162
|
+
// would re-evaluate the <Portal> id and re-mount the group,
|
|
163
|
+
// detaching the gizmo's drag target mid-stroke.
|
|
164
|
+
if (editSession.current?.owns(existing)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
148
167
|
traits.setParentTrait(existing, parent);
|
|
149
168
|
if (color) {
|
|
150
169
|
existing.set(traits.Color, color);
|
|
@@ -153,14 +172,30 @@ export const provideFrames = (partID) => {
|
|
|
153
172
|
existing.set(traits.Center, center);
|
|
154
173
|
}
|
|
155
174
|
traits.updateGeometryTrait(existing, frame.physicalObject);
|
|
156
|
-
|
|
175
|
+
if (!isEditMode && !partConfig.hasPendingSave) {
|
|
176
|
+
existing.set(traits.Pose, pose);
|
|
177
|
+
}
|
|
178
|
+
if (!existing.has(traits.LivePose)) {
|
|
179
|
+
existing.add(traits.LivePose(pose));
|
|
180
|
+
}
|
|
181
|
+
// Skip the EditedPose overwrite while in edit mode. The merged
|
|
182
|
+
// `frames` source can differ from query.data once didRecentlyEdit
|
|
183
|
+
// flips (fragment overrides, round-trip drift), and writing those
|
|
184
|
+
// values would shift entities whose parents the user is portaling
|
|
185
|
+
// into — the gizmo's drag target moves underneath it. Once we're
|
|
186
|
+
// back in monitor mode, the next sync resumes the overwrite.
|
|
187
|
+
if (!isEditMode) {
|
|
188
|
+
existing.set(traits.EditedPose, pose);
|
|
189
|
+
}
|
|
157
190
|
continue;
|
|
158
191
|
}
|
|
159
192
|
const entityTraits = [
|
|
160
193
|
traits.Name(name),
|
|
161
194
|
traits.Pose(pose),
|
|
162
195
|
traits.EditedPose(pose),
|
|
196
|
+
traits.LivePose(pose),
|
|
163
197
|
traits.FramesAPI,
|
|
198
|
+
traits.Transformable,
|
|
164
199
|
traits.ShowAxesHelper,
|
|
165
200
|
...traits.getParentTrait(parent),
|
|
166
201
|
];
|
|
@@ -273,7 +273,17 @@ const useStandalonePartConfig = (partID) => {
|
|
|
273
273
|
}
|
|
274
274
|
return results;
|
|
275
275
|
});
|
|
276
|
+
let lastPartID;
|
|
276
277
|
$effect.pre(() => {
|
|
278
|
+
const id = partID();
|
|
279
|
+
if (lastPartID !== undefined && lastPartID !== id) {
|
|
280
|
+
// Part changed: drop any in-memory edits/pending-save state from the
|
|
281
|
+
// previous part. `current` is left for the existing sync below to
|
|
282
|
+
// repopulate once the new part's networkPartConfig arrives.
|
|
283
|
+
isDirty = false;
|
|
284
|
+
hasPendingSave = false;
|
|
285
|
+
}
|
|
286
|
+
lastPartID = id;
|
|
277
287
|
if (!networkPartConfig || isDirty) {
|
|
278
288
|
return;
|
|
279
289
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Entity } from 'koota';
|
|
2
|
+
import type { Relationship } from '../metadata';
|
|
3
|
+
export declare const provideRelationships: () => {
|
|
4
|
+
apply(entity: Entity, relationships: Relationship[] | undefined): void;
|
|
5
|
+
flush(targetUuid: string): void;
|
|
6
|
+
clear(): void;
|
|
7
|
+
};
|
|
8
|
+
export declare const useRelationships: () => {
|
|
9
|
+
apply(entity: Entity, relationships: Relationship[] | undefined): void;
|
|
10
|
+
flush(targetUuid: string): void;
|
|
11
|
+
clear(): void;
|
|
12
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
import { uuidBytesToString } from '../draw';
|
|
3
|
+
import { relations, traits, useQuery } from '../ecs';
|
|
4
|
+
const RELATIONSHIPS_CONTEXT_KEY = Symbol('relationships');
|
|
5
|
+
export const provideRelationships = () => {
|
|
6
|
+
const uuids = useQuery(traits.UUID);
|
|
7
|
+
const pending = new Map();
|
|
8
|
+
const addPending = (targetUuid, relationship) => {
|
|
9
|
+
const next = pending.get(targetUuid) ?? [];
|
|
10
|
+
next.push(relationship);
|
|
11
|
+
pending.set(targetUuid, next);
|
|
12
|
+
};
|
|
13
|
+
return setContext(RELATIONSHIPS_CONTEXT_KEY, {
|
|
14
|
+
apply(entity, relationships) {
|
|
15
|
+
const desired = relationships ?? [];
|
|
16
|
+
const currentTargets = entity.targetsFor(relations.SubEntityLink);
|
|
17
|
+
const desiredByUuid = new Map();
|
|
18
|
+
for (const rel of desired) {
|
|
19
|
+
const targetUUID = uuidBytesToString(rel.targetUuid);
|
|
20
|
+
if (!targetUUID)
|
|
21
|
+
continue;
|
|
22
|
+
desiredByUuid.set(targetUUID, rel);
|
|
23
|
+
}
|
|
24
|
+
for (const target of currentTargets) {
|
|
25
|
+
if (!target.isAlive())
|
|
26
|
+
continue;
|
|
27
|
+
const targetUuid = target.get(traits.UUID);
|
|
28
|
+
if (!targetUuid || !desiredByUuid.has(targetUuid)) {
|
|
29
|
+
entity.remove(relations.SubEntityLink(target));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const [uuid, relationship] of desiredByUuid) {
|
|
33
|
+
const targetEntity = uuids.current.find((e) => e.get(traits.UUID) === uuid);
|
|
34
|
+
if (!targetEntity) {
|
|
35
|
+
addPending(uuid, {
|
|
36
|
+
entity: entity,
|
|
37
|
+
type: relationship.type,
|
|
38
|
+
indexMapping: relationship.indexMapping,
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const existing = entity.get(relations.SubEntityLink(targetEntity));
|
|
43
|
+
if (existing &&
|
|
44
|
+
existing.type === relationship.type &&
|
|
45
|
+
existing.indexMapping === (relationship.indexMapping ?? 'index')) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
entity.add(relations.SubEntityLink(targetEntity, {
|
|
49
|
+
type: relationship.type,
|
|
50
|
+
indexMapping: relationship.indexMapping ?? 'index',
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
flush(targetUuid) {
|
|
55
|
+
const relationship = pending.get(targetUuid);
|
|
56
|
+
if (!relationship)
|
|
57
|
+
return;
|
|
58
|
+
pending.delete(targetUuid);
|
|
59
|
+
const targetEntity = uuids.current.find((e) => e.get(traits.UUID) === targetUuid);
|
|
60
|
+
if (!targetEntity)
|
|
61
|
+
return;
|
|
62
|
+
for (const { entity, type, indexMapping } of relationship) {
|
|
63
|
+
if (!entity.isAlive())
|
|
64
|
+
continue;
|
|
65
|
+
entity.add(relations.SubEntityLink(targetEntity, {
|
|
66
|
+
type,
|
|
67
|
+
indexMapping: indexMapping ?? 'index',
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
clear() {
|
|
72
|
+
pending.clear();
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
export const useRelationships = () => {
|
|
77
|
+
return getContext(RELATIONSHIPS_CONTEXT_KEY);
|
|
78
|
+
};
|
|
@@ -9,9 +9,8 @@ export interface Settings {
|
|
|
9
9
|
};
|
|
10
10
|
disabledCameras: Record<string, boolean>;
|
|
11
11
|
disabledVisionServices: Record<string, boolean>;
|
|
12
|
-
transforming: boolean;
|
|
13
12
|
snapping: boolean;
|
|
14
|
-
transformMode: 'translate' | 'rotate' | 'scale';
|
|
13
|
+
transformMode: 'none' | 'translate' | 'rotate' | 'scale';
|
|
15
14
|
grid: boolean;
|
|
16
15
|
gridCellSize: number;
|
|
17
16
|
gridSectionSize: number;
|
|
@@ -9,6 +9,7 @@ import { isPointCloud } from '../geometry';
|
|
|
9
9
|
import { metadataFromStruct } from '../metadata';
|
|
10
10
|
import { createPose } from '../transform';
|
|
11
11
|
import { usePartID } from './usePartID.svelte';
|
|
12
|
+
import { useRelationships } from './useRelationships.svelte';
|
|
12
13
|
export const provideWorldStates = () => {
|
|
13
14
|
const partID = usePartID();
|
|
14
15
|
const resourceNames = useResourceNames(() => partID.current, 'world_state_store');
|
|
@@ -83,6 +84,7 @@ const decodeWorldStateChunk = (response, fallbackStart) => {
|
|
|
83
84
|
const createWorldState = (client) => {
|
|
84
85
|
const { invalidate } = useThrelte();
|
|
85
86
|
const world = useWorld();
|
|
87
|
+
const relationships = useRelationships();
|
|
86
88
|
const entities = new Map();
|
|
87
89
|
const chunkLoader = createChunkLoader({
|
|
88
90
|
world,
|
|
@@ -105,10 +107,12 @@ const createWorldState = (client) => {
|
|
|
105
107
|
if (entities.has(transform.uuidString)) {
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
108
|
-
const
|
|
109
|
-
entities.set(transform.uuidString, entity);
|
|
110
|
+
const spawned = drawTransform(world, transform, traits.WorldStateStoreAPI, { removable: false });
|
|
111
|
+
entities.set(transform.uuidString, spawned.entity);
|
|
112
|
+
relationships.apply(spawned.entity, spawned.relationships);
|
|
110
113
|
const parsedMetadata = metadataFromStruct(transform.metadata?.fields);
|
|
111
|
-
chunkLoader.start(transform.uuidString, entity, parsedMetadata);
|
|
114
|
+
chunkLoader.start(transform.uuidString, spawned.entity, parsedMetadata);
|
|
115
|
+
relationships.flush(transform.uuidString);
|
|
112
116
|
if (isPointCloud(transform.physicalObject?.geometryType))
|
|
113
117
|
invalidate();
|
|
114
118
|
};
|
|
@@ -140,9 +144,11 @@ const createWorldState = (client) => {
|
|
|
140
144
|
}
|
|
141
145
|
}
|
|
142
146
|
if (metadataDirty) {
|
|
143
|
-
|
|
147
|
+
const parsedMetadata = metadataFromStruct(transform.metadata?.fields);
|
|
148
|
+
updateMetadata(entity, parsedMetadata, {
|
|
144
149
|
pointCloud: isPointCloud(transform.physicalObject?.geometryType),
|
|
145
150
|
});
|
|
151
|
+
relationships.apply(entity, parsedMetadata.relationships);
|
|
146
152
|
}
|
|
147
153
|
};
|
|
148
154
|
let initialized = false;
|
package/dist/metadata.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { PlainMessage, Struct } from '@viamrobotics/sdk';
|
|
2
|
-
import { Metadata as MetadataProto } from './buf/draw/v1/metadata_pb';
|
|
3
|
-
/** Metadata for a `Drawing` or `Transform`. */
|
|
4
|
-
export type Metadata = PlainMessage<MetadataProto
|
|
2
|
+
import { Metadata as MetadataProto, type Relationship as RelationshipProto } from './buf/draw/v1/metadata_pb';
|
|
3
|
+
/** Metadata for a `Drawing` or `Transform`. Relationships default to empty. */
|
|
4
|
+
export type Metadata = Omit<PlainMessage<MetadataProto>, 'relationships'> & {
|
|
5
|
+
relationships?: PlainMessage<MetadataProto>['relationships'];
|
|
6
|
+
};
|
|
7
|
+
/** Plain-object representation of a Relationship, usable outside proto classes. */
|
|
8
|
+
export type Relationship = PlainMessage<RelationshipProto>;
|
|
5
9
|
/** Type guard that checks whether a string is a recognised metadata wire key. */
|
|
6
10
|
export declare const isMetadataField: (key: string) => boolean;
|
|
7
11
|
/**
|
package/dist/metadata.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ColorFormat, Metadata as MetadataProto } from './buf/draw/v1/metadata_pb';
|
|
1
|
+
import { ColorFormat, Metadata as MetadataProto, } from './buf/draw/v1/metadata_pb';
|
|
2
2
|
/** Type guard that checks whether a string is a recognised metadata wire key. */
|
|
3
3
|
export const isMetadataField = (key) => {
|
|
4
4
|
return (key === 'colors' ||
|
|
@@ -6,7 +6,8 @@ export const isMetadataField = (key) => {
|
|
|
6
6
|
key === 'opacities' ||
|
|
7
7
|
key === 'show_axes_helper' ||
|
|
8
8
|
key === 'invisible' ||
|
|
9
|
-
key === 'chunks'
|
|
9
|
+
key === 'chunks' ||
|
|
10
|
+
key === 'relationships');
|
|
10
11
|
};
|
|
11
12
|
/**
|
|
12
13
|
* Extracts typed {@link Metadata} from a proto `Struct` fields map.
|
|
@@ -77,6 +78,29 @@ export const metadataFromStruct = (fields = {}) => {
|
|
|
77
78
|
}
|
|
78
79
|
break;
|
|
79
80
|
}
|
|
81
|
+
case 'relationships': {
|
|
82
|
+
if (Array.isArray(unwrappedValue)) {
|
|
83
|
+
json.relationships = unwrappedValue
|
|
84
|
+
.filter((item) => typeof item === 'object' && item !== null)
|
|
85
|
+
.map((item) => {
|
|
86
|
+
const targetUuidStr = item['target_uuid'];
|
|
87
|
+
let targetUuid = new Uint8Array();
|
|
88
|
+
if (typeof targetUuidStr === 'string' && targetUuidStr.length > 0) {
|
|
89
|
+
const binary = atob(targetUuidStr);
|
|
90
|
+
targetUuid = new Uint8Array(binary.length);
|
|
91
|
+
for (let i = 0; i < binary.length; i++) {
|
|
92
|
+
targetUuid[i] = binary.charCodeAt(i);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
targetUuid,
|
|
97
|
+
type: typeof item['type'] === 'string' ? item['type'] : '',
|
|
98
|
+
indexMapping: typeof item['index_mapping'] === 'string' ? item['index_mapping'] : undefined,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
80
104
|
}
|
|
81
105
|
}
|
|
82
106
|
return json;
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { Entity, World } from 'koota';
|
|
|
2
2
|
import type { Snapshot } from './buf/draw/v1/snapshot_pb';
|
|
3
3
|
import type { Settings } from './hooks/useSettings.svelte';
|
|
4
4
|
import { type SceneMetadata } from './buf/draw/v1/scene_pb';
|
|
5
|
+
import type { Relationship } from './metadata';
|
|
6
|
+
export type SnapshotEntity = {
|
|
7
|
+
entity: Entity;
|
|
8
|
+
relationships: Relationship[] | undefined;
|
|
9
|
+
};
|
|
5
10
|
/**
|
|
6
11
|
* Merges scene-level metadata (grid, camera, point/line settings) into the
|
|
7
12
|
* current viewer settings. Millimeter values from the proto are converted
|
|
@@ -18,4 +23,4 @@ export declare const applySceneMetadata: (settings: Settings, metadata: SceneMet
|
|
|
18
23
|
*
|
|
19
24
|
* @returns The spawned entities
|
|
20
25
|
*/
|
|
21
|
-
export declare const spawnSnapshotEntities: (world: World, snapshot: Snapshot) =>
|
|
26
|
+
export declare const spawnSnapshotEntities: (world: World, snapshot: Snapshot) => SnapshotEntity[];
|