@viamrobotics/motion-tools 1.34.4 → 1.34.6
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/App.svelte +14 -2
- package/dist/components/App.svelte.d.ts +6 -2
- package/dist/components/AxesHelper.svelte +3 -1
- package/dist/components/AxesHelper.svelte.d.ts +1 -1
- package/dist/components/Entities/Arrows/Arrows.svelte +0 -9
- package/dist/components/Entities/AxesHelper.svelte +38 -0
- package/dist/components/Entities/AxesHelper.svelte.d.ts +8 -0
- package/dist/components/Entities/AxesHelpers.svelte +13 -0
- package/dist/{plugins/LLMSceneBuilder/AISettings.svelte.d.ts → components/Entities/AxesHelpers.svelte.d.ts} +6 -14
- package/dist/components/Entities/Boxes.svelte +290 -0
- package/dist/components/Entities/Boxes.svelte.d.ts +14 -0
- package/dist/components/Entities/Entities.svelte +10 -5
- package/dist/components/Entities/GLTF.svelte +0 -9
- package/dist/components/Entities/Line.svelte +0 -9
- package/dist/components/Entities/Mesh.svelte +5 -23
- package/dist/components/Entities/Points.svelte +1 -9
- package/dist/components/Entities/composeBoxMatrix.d.ts +12 -0
- package/dist/components/Entities/composeBoxMatrix.js +29 -0
- package/dist/components/Entities/hooks/useEntityEvents.svelte.d.ts +27 -0
- package/dist/components/Entities/hooks/useEntityEvents.svelte.js +87 -39
- package/dist/components/Scene.svelte +0 -1
- package/dist/components/Selected.svelte +14 -3
- package/dist/components/SelectedTransformControls.svelte +3 -5
- package/dist/components/overlay/Details.svelte +9 -4
- package/dist/hooks/plugins/bvh.svelte.js +9 -0
- package/dist/hooks/useConfigFrames.svelte.js +5 -3
- package/dist/hooks/useFragmentInfo.svelte.d.ts +24 -0
- package/dist/hooks/useFragmentInfo.svelte.js +86 -0
- package/dist/hooks/useFramelessComponents.svelte.js +3 -1
- package/dist/hooks/usePartConfig.svelte.d.ts +0 -6
- package/dist/hooks/usePartConfig.svelte.js +5 -60
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugins/Focus/FocusBox.svelte +12 -1
- package/dist/plugins/LLMSceneBuilder/frameDeltaAdapter.d.ts +9 -2
- package/dist/plugins/LLMSceneBuilder/frameDeltaAdapter.js +65 -10
- package/dist/plugins/LLMSceneBuilder/useSceneBuilder.svelte.js +29 -5
- package/dist/plugins/Selection/Ellipse.svelte +9 -26
- package/dist/plugins/Selection/Ellipse.svelte.d.ts +2 -1
- package/dist/plugins/Selection/Lasso.svelte +9 -26
- package/dist/plugins/Selection/Lasso.svelte.d.ts +2 -1
- package/dist/plugins/Selection/SelectionTool.svelte +18 -3
- package/dist/plugins/Selection/SelectionTool.svelte.d.ts +2 -0
- package/dist/plugins/TopDownLock/TopDownLock.svelte +25 -0
- package/dist/plugins/TopDownLock/TopDownLock.svelte.d.ts +3 -0
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.js +1 -0
- package/dist/three/OBBHelper.d.ts +8 -1
- package/dist/three/OBBHelper.js +11 -1
- package/package.json +3 -2
- package/dist/plugins/LLMSceneBuilder/AISettings.svelte +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { getContext, setContext } from 'svelte';
|
|
2
|
+
import { useFragmentInfo } from './useFragmentInfo.svelte';
|
|
2
3
|
import { useFrames } from './useFrames.svelte';
|
|
3
4
|
import { usePartConfig } from './usePartConfig.svelte';
|
|
4
5
|
const key = Symbol('frameless-components-context');
|
|
5
6
|
export const provideFramelessComponents = () => {
|
|
6
7
|
const partConfig = usePartConfig();
|
|
8
|
+
const fragmentInfo = useFragmentInfo();
|
|
7
9
|
const frames = useFrames();
|
|
8
10
|
const current = $derived.by(() => {
|
|
9
11
|
const { components } = partConfig.current;
|
|
@@ -11,7 +13,7 @@ export const provideFramelessComponents = () => {
|
|
|
11
13
|
?.filter((component) => component.frame === undefined)
|
|
12
14
|
.map((component) => component.name) ?? [];
|
|
13
15
|
const fragmentComponentsWithNoFrame = new Set(partComponentsWIthNoFrame);
|
|
14
|
-
for (const fragmentComponentName of Object.keys(
|
|
16
|
+
for (const fragmentComponentName of Object.keys(fragmentInfo.current)) {
|
|
15
17
|
if (frames.current.some((frame) => frame.referenceFrame === fragmentComponentName)) {
|
|
16
18
|
continue;
|
|
17
19
|
}
|
|
@@ -11,16 +11,11 @@ export interface PartConfig {
|
|
|
11
11
|
mods: any[];
|
|
12
12
|
}[];
|
|
13
13
|
}
|
|
14
|
-
export type FragmentInfo = {
|
|
15
|
-
id: string;
|
|
16
|
-
variables: Record<string, string>;
|
|
17
|
-
};
|
|
18
14
|
interface PartConfigContext {
|
|
19
15
|
current: PartConfig;
|
|
20
16
|
isDirty: boolean;
|
|
21
17
|
hasPendingSave: boolean;
|
|
22
18
|
hasEditPermissions: boolean;
|
|
23
|
-
componentNameToFragmentInfo: Record<string, FragmentInfo>;
|
|
24
19
|
updateFrame: (componentName: string, referenceFrame: string, pose: Pose, geometry?: Frame['geometry']) => void;
|
|
25
20
|
deleteFrame: (componentName: string) => void;
|
|
26
21
|
createFrame: (componentName: string) => void;
|
|
@@ -34,7 +29,6 @@ export declare const usePartConfig: () => PartConfigContext;
|
|
|
34
29
|
interface AppEmbeddedPartConfigProps {
|
|
35
30
|
current: Struct;
|
|
36
31
|
isDirty: boolean;
|
|
37
|
-
componentNameToFragmentInfo: Record<string, FragmentInfo>;
|
|
38
32
|
setLocalPartConfig: (config: Struct) => void;
|
|
39
33
|
}
|
|
40
34
|
export {};
|
|
@@ -2,11 +2,13 @@ import { Pose, Struct } from '@viamrobotics/sdk';
|
|
|
2
2
|
import { createAppMutation, createAppQuery } from '@viamrobotics/svelte-sdk';
|
|
3
3
|
import { getContext, setContext } from 'svelte';
|
|
4
4
|
import { createFrame } from '../frame';
|
|
5
|
+
import { useFragmentInfo } from './useFragmentInfo.svelte';
|
|
5
6
|
import { createPoseFromFrame } from '../transform';
|
|
6
7
|
const key = Symbol('part-config-context');
|
|
7
8
|
export const providePartConfig = (partID, params) => {
|
|
8
9
|
const props = $derived(params());
|
|
9
10
|
const config = $derived(props ? useEmbeddedPartConfig(props) : useStandalonePartConfig(partID));
|
|
11
|
+
const fragmentInfo = useFragmentInfo();
|
|
10
12
|
const getCurrent = () => {
|
|
11
13
|
return (config.current?.toJson?.() ?? { components: [] });
|
|
12
14
|
};
|
|
@@ -157,9 +159,6 @@ export const providePartConfig = (partID, params) => {
|
|
|
157
159
|
get current() {
|
|
158
160
|
return current;
|
|
159
161
|
},
|
|
160
|
-
get componentNameToFragmentInfo() {
|
|
161
|
-
return config.componentNameToFragmentInfo;
|
|
162
|
-
},
|
|
163
162
|
get isDirty() {
|
|
164
163
|
return config.isDirty;
|
|
165
164
|
},
|
|
@@ -170,7 +169,7 @@ export const providePartConfig = (partID, params) => {
|
|
|
170
169
|
return config.hasEditPermissions;
|
|
171
170
|
},
|
|
172
171
|
updateFrame: (componentName, referenceFrame, framePosition, frameGeometry) => {
|
|
173
|
-
const fragmentId =
|
|
172
|
+
const fragmentId = fragmentInfo.current[componentName]?.id;
|
|
174
173
|
if (fragmentId === undefined) {
|
|
175
174
|
updatePartFrame(componentName, referenceFrame, framePosition, frameGeometry);
|
|
176
175
|
}
|
|
@@ -179,7 +178,7 @@ export const providePartConfig = (partID, params) => {
|
|
|
179
178
|
}
|
|
180
179
|
},
|
|
181
180
|
deleteFrame: (componentName) => {
|
|
182
|
-
const fragmentId =
|
|
181
|
+
const fragmentId = fragmentInfo.current[componentName]?.id;
|
|
183
182
|
if (fragmentId === undefined) {
|
|
184
183
|
deletePartFrame(componentName);
|
|
185
184
|
}
|
|
@@ -188,7 +187,7 @@ export const providePartConfig = (partID, params) => {
|
|
|
188
187
|
}
|
|
189
188
|
},
|
|
190
189
|
createFrame: (componentName) => {
|
|
191
|
-
const fragmentId =
|
|
190
|
+
const fragmentId = fragmentInfo.current[componentName]?.id;
|
|
192
191
|
if (fragmentId === undefined) {
|
|
193
192
|
createPartFrame(componentName);
|
|
194
193
|
}
|
|
@@ -246,9 +245,6 @@ const useEmbeddedPartConfig = (props) => {
|
|
|
246
245
|
get current() {
|
|
247
246
|
return props.current ?? new Struct();
|
|
248
247
|
},
|
|
249
|
-
get componentNameToFragmentInfo() {
|
|
250
|
-
return props.componentNameToFragmentInfo;
|
|
251
|
-
},
|
|
252
248
|
set(config) {
|
|
253
249
|
const struct = Struct.fromJson(config);
|
|
254
250
|
return props.setLocalPartConfig(struct);
|
|
@@ -274,54 +270,6 @@ const useStandalonePartConfig = (partID) => {
|
|
|
274
270
|
let isDirty = $state(false);
|
|
275
271
|
let hasPendingSave = $state(false);
|
|
276
272
|
const hasEditPermissions = $derived(networkPartConfig !== undefined);
|
|
277
|
-
const configJSON = $derived.by(() => {
|
|
278
|
-
if (!networkPartConfig) {
|
|
279
|
-
return undefined;
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
return networkPartConfig.toJson();
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
return undefined;
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
const fragmentQueries = $derived((configJSON?.fragments ?? []).map((fragmentId) => {
|
|
289
|
-
const id = typeof fragmentId === 'string' ? fragmentId : fragmentId.id;
|
|
290
|
-
return createAppQuery('getFragment', () => [id], { refetchInterval: false });
|
|
291
|
-
}));
|
|
292
|
-
const fragmentIdToVariables = $derived.by(() => {
|
|
293
|
-
const results = {};
|
|
294
|
-
for (const fragment of configJSON?.fragments ?? []) {
|
|
295
|
-
const id = typeof fragment === 'string' ? fragment : fragment.id;
|
|
296
|
-
const variables = typeof fragment === 'string' ? {} : fragment.variables;
|
|
297
|
-
results[id] = variables;
|
|
298
|
-
}
|
|
299
|
-
return results;
|
|
300
|
-
});
|
|
301
|
-
const componentNameToFragmentInfo = $derived.by(() => {
|
|
302
|
-
const results = {};
|
|
303
|
-
for (const query of fragmentQueries) {
|
|
304
|
-
if (!query.data) {
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
const fragmentId = query.data.id;
|
|
308
|
-
const components = query.data?.fragment?.fields['components']?.kind;
|
|
309
|
-
if (components?.case === 'listValue') {
|
|
310
|
-
for (const component of components.value.values) {
|
|
311
|
-
if (component.kind.case === 'structValue') {
|
|
312
|
-
const componentName = component.kind.value.fields['name']?.kind;
|
|
313
|
-
if (componentName.case === 'stringValue') {
|
|
314
|
-
results[componentName.value] = {
|
|
315
|
-
id: fragmentId,
|
|
316
|
-
variables: fragmentIdToVariables[fragmentId] ?? {},
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return results;
|
|
324
|
-
});
|
|
325
273
|
let lastPartID;
|
|
326
274
|
$effect.pre(() => {
|
|
327
275
|
const id = partID();
|
|
@@ -354,9 +302,6 @@ const useStandalonePartConfig = (partID) => {
|
|
|
354
302
|
get hasEditPermissions() {
|
|
355
303
|
return hasEditPermissions;
|
|
356
304
|
},
|
|
357
|
-
get componentNameToFragmentInfo() {
|
|
358
|
-
return componentNameToFragmentInfo;
|
|
359
|
-
},
|
|
360
305
|
set(config) {
|
|
361
306
|
current = Struct.fromJson(config);
|
|
362
307
|
isDirty = true;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @deprecated MotionTools has been renamed to Visualizer. This export will be removed in v2. */
|
|
2
2
|
export { default as MotionTools } from './components/App.svelte';
|
|
3
3
|
export { default as Visualizer } from './components/App.svelte';
|
|
4
|
+
export { useSettings } from './hooks/useSettings.svelte';
|
|
4
5
|
export { default as PCD } from './components/PCD.svelte';
|
|
5
6
|
export * as relations from './ecs/relations';
|
|
6
7
|
export * as traits from './ecs/traits';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @deprecated MotionTools has been renamed to Visualizer. This export will be removed in v2. */
|
|
2
2
|
export { default as MotionTools } from './components/App.svelte';
|
|
3
3
|
export { default as Visualizer } from './components/App.svelte';
|
|
4
|
+
export { useSettings } from './hooks/useSettings.svelte';
|
|
4
5
|
// Plugins
|
|
5
6
|
export { default as PCD } from './components/PCD.svelte';
|
|
6
7
|
// ECS
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { T, useThrelte } from '@threlte/core'
|
|
3
3
|
import { Gizmo, TrackballControls } from '@threlte/extras'
|
|
4
4
|
import { untrack } from 'svelte'
|
|
5
|
-
import { Box3, Vector3 } from 'three'
|
|
5
|
+
import { Box3, Matrix4, Vector3 } from 'three'
|
|
6
6
|
|
|
7
7
|
import Camera from '../../components/Camera.svelte'
|
|
8
|
+
import { composeBoxMatrix } from '../../components/Entities/composeBoxMatrix'
|
|
8
9
|
import { traits, useQuery } from '../../ecs'
|
|
9
10
|
import { useCameraControls } from '../../hooks/useControls.svelte'
|
|
11
|
+
import { expandBoxByTransformedBox } from '../../three/OBBHelper'
|
|
10
12
|
|
|
11
13
|
const { scene } = useThrelte()
|
|
12
14
|
const cameraControls = useCameraControls()
|
|
@@ -35,6 +37,8 @@
|
|
|
35
37
|
|
|
36
38
|
const box = new Box3()
|
|
37
39
|
const vec = new Vector3()
|
|
40
|
+
const unitBox = new Box3(new Vector3(-0.5, -0.5, -0.5), new Vector3(0.5, 0.5, 0.5))
|
|
41
|
+
const boxMatrix = new Matrix4()
|
|
38
42
|
|
|
39
43
|
let center = $state.raw<[number, number, number]>([0, 0, 0])
|
|
40
44
|
let size = $state.raw<[number, number, number]>([0, 0, 0])
|
|
@@ -48,6 +52,13 @@
|
|
|
48
52
|
$effect(() => {
|
|
49
53
|
box.makeEmpty()
|
|
50
54
|
for (const entity of untrack(() => selected.current)) {
|
|
55
|
+
// Boxes render instanced, so the entity's named scene object
|
|
56
|
+
// carries no geometry — frame them from traits instead.
|
|
57
|
+
if (composeBoxMatrix(entity, boxMatrix)) {
|
|
58
|
+
expandBoxByTransformedBox(box, unitBox, boxMatrix)
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
const object3d = scene.getObjectByName(entity as unknown as string)
|
|
52
63
|
if (object3d) {
|
|
53
64
|
box.expandByObject(object3d)
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type { Pose } from '@viamrobotics/sdk';
|
|
1
|
+
import type { Pose, Transform } from '@viamrobotics/sdk';
|
|
2
2
|
import type { Frame } from '../../frame';
|
|
3
|
+
import type { FragmentInfo } from '../../hooks/useFragmentInfo.svelte';
|
|
3
4
|
import type { PartConfig } from '../../hooks/usePartConfig.svelte';
|
|
5
|
+
/**
|
|
6
|
+
* Resolves current frames for fragment-defined components from live framesystem
|
|
7
|
+
* data and any config $set overrides. Returns a FragmentInfo map suitable for
|
|
8
|
+
* validateProposedFrameDeltas and LLM inference.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveFragmentCurrentFrames(fragmentNames: string[], fragmentInfo: Record<string, FragmentInfo>, liveFrames: Transform[], configFrames: Record<string, Transform>): Record<string, FragmentInfo>;
|
|
4
11
|
export interface FrameDelta {
|
|
5
12
|
componentName: string;
|
|
6
13
|
translation?: {
|
|
@@ -34,7 +41,7 @@ export interface UpdateError {
|
|
|
34
41
|
* applying them. Each PreparedUpdate carries old and new values so the caller
|
|
35
42
|
* can render a diff and confirm via useSceneBuilder's confirm().
|
|
36
43
|
*/
|
|
37
|
-
export declare function validateProposedFrameDeltas(deltas: FrameDelta[], config: PartConfig): {
|
|
44
|
+
export declare function validateProposedFrameDeltas(deltas: FrameDelta[], config: PartConfig, fragmentFrames?: Record<string, FragmentInfo>): {
|
|
38
45
|
errors: UpdateError[];
|
|
39
46
|
prepared: PreparedUpdate[];
|
|
40
47
|
};
|
|
@@ -1,4 +1,47 @@
|
|
|
1
|
-
import { applyEulerDeltaToPose, createPoseFromFrame } from '../../transform';
|
|
1
|
+
import { applyEulerDeltaToPose, createPose, createPoseFromFrame } from '../../transform';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves current frames for fragment-defined components from live framesystem
|
|
4
|
+
* data and any config $set overrides. Returns a FragmentInfo map suitable for
|
|
5
|
+
* validateProposedFrameDeltas and LLM inference.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveFragmentCurrentFrames(fragmentNames, fragmentInfo, liveFrames, configFrames) {
|
|
8
|
+
const liveByName = {};
|
|
9
|
+
for (const frame of liveFrames) {
|
|
10
|
+
liveByName[frame.referenceFrame] = frame;
|
|
11
|
+
}
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const name of fragmentNames) {
|
|
14
|
+
const meta = fragmentInfo[name];
|
|
15
|
+
if (!meta)
|
|
16
|
+
continue;
|
|
17
|
+
const observed = (configFrames[name] ?? liveByName[name])?.poseInObserverFrame;
|
|
18
|
+
if (!observed)
|
|
19
|
+
continue;
|
|
20
|
+
const pose = createPose(observed.pose);
|
|
21
|
+
result[name] = {
|
|
22
|
+
id: meta.id,
|
|
23
|
+
variables: meta.variables,
|
|
24
|
+
frame: {
|
|
25
|
+
parent: observed.referenceFrame,
|
|
26
|
+
translation: {
|
|
27
|
+
x: pose.x,
|
|
28
|
+
y: pose.y,
|
|
29
|
+
z: pose.z,
|
|
30
|
+
},
|
|
31
|
+
orientation: {
|
|
32
|
+
type: 'ov_degrees',
|
|
33
|
+
value: {
|
|
34
|
+
x: pose.oX,
|
|
35
|
+
y: pose.oY,
|
|
36
|
+
z: pose.oZ,
|
|
37
|
+
th: pose.theta,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
2
45
|
function mergeTranslation(a, b) {
|
|
3
46
|
return a || b ? { x: b?.x ?? a?.x, y: b?.y ?? a?.y, z: b?.z ?? a?.z } : undefined;
|
|
4
47
|
}
|
|
@@ -12,10 +55,13 @@ function mergeOrientation(a, b) {
|
|
|
12
55
|
* applying them. Each PreparedUpdate carries old and new values so the caller
|
|
13
56
|
* can render a diff and confirm via useSceneBuilder's confirm().
|
|
14
57
|
*/
|
|
15
|
-
export function validateProposedFrameDeltas(deltas, config) {
|
|
58
|
+
export function validateProposedFrameDeltas(deltas, config, fragmentFrames = {}) {
|
|
16
59
|
const errors = [];
|
|
17
60
|
const prepared = [];
|
|
18
|
-
const knownNames = new Set(
|
|
61
|
+
const knownNames = new Set([
|
|
62
|
+
...config.components.map((c) => c.name),
|
|
63
|
+
...Object.keys(fragmentFrames),
|
|
64
|
+
]);
|
|
19
65
|
// Merge multiple deltas for the same component — the LLM sometimes splits
|
|
20
66
|
// translation and orientation into separate entries despite the schema saying one per component.
|
|
21
67
|
const mergedDeltas = new Map();
|
|
@@ -35,13 +81,21 @@ export function validateProposedFrameDeltas(deltas, config) {
|
|
|
35
81
|
}
|
|
36
82
|
}
|
|
37
83
|
for (const delta of mergedDeltas.values()) {
|
|
38
|
-
const
|
|
39
|
-
|
|
84
|
+
const partComponent = config.components.find((c) => c.name === delta.componentName);
|
|
85
|
+
// Fragment-defined components aren't in config.components; their current
|
|
86
|
+
// frame comes from `fragmentFrames` (resolved from the live framesystem).
|
|
87
|
+
// Part config wins when the same name exists in both.
|
|
88
|
+
const fragmentEntry = partComponent ? undefined : fragmentFrames[delta.componentName];
|
|
89
|
+
const frame = partComponent?.frame ?? fragmentEntry?.frame;
|
|
90
|
+
if (!partComponent && !fragmentEntry) {
|
|
40
91
|
errors.push({ componentName: delta.componentName, reason: 'Component not found in config' });
|
|
41
92
|
continue;
|
|
42
93
|
}
|
|
43
|
-
if (!
|
|
44
|
-
errors.push({
|
|
94
|
+
if (!frame) {
|
|
95
|
+
errors.push({
|
|
96
|
+
componentName: delta.componentName,
|
|
97
|
+
reason: fragmentEntry ? 'Fragment has no frame' : 'Component has no frame',
|
|
98
|
+
});
|
|
45
99
|
continue;
|
|
46
100
|
}
|
|
47
101
|
if (delta.parent !== undefined &&
|
|
@@ -55,8 +109,9 @@ export function validateProposedFrameDeltas(deltas, config) {
|
|
|
55
109
|
});
|
|
56
110
|
continue;
|
|
57
111
|
}
|
|
58
|
-
const previousPose = createPoseFromFrame(
|
|
59
|
-
const previousParent =
|
|
112
|
+
const previousPose = createPoseFromFrame(frame);
|
|
113
|
+
const previousParent = frame.parent;
|
|
114
|
+
const geometry = frame.geometry;
|
|
60
115
|
const newParent = delta.parent ?? previousParent;
|
|
61
116
|
const newPose = {
|
|
62
117
|
x: delta.translation?.x ?? previousPose.x,
|
|
@@ -83,7 +138,7 @@ export function validateProposedFrameDeltas(deltas, config) {
|
|
|
83
138
|
previousParent,
|
|
84
139
|
pose: newPose,
|
|
85
140
|
previousPose,
|
|
86
|
-
geometry
|
|
141
|
+
geometry,
|
|
87
142
|
explanation: delta.explanation,
|
|
88
143
|
});
|
|
89
144
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { getContext, setContext } from 'svelte';
|
|
2
|
+
import { useConfigFrames } from '../../hooks/useConfigFrames.svelte';
|
|
3
|
+
import { useFragmentInfo } from '../../hooks/useFragmentInfo.svelte';
|
|
4
|
+
import { useFrames } from '../../hooks/useFrames.svelte';
|
|
2
5
|
import { usePartConfig } from '../../hooks/usePartConfig.svelte';
|
|
3
6
|
import { createPoseFromFrame, poseToEulerDegrees } from '../../transform';
|
|
4
|
-
import { validateProposedFrameDeltas } from './frameDeltaAdapter';
|
|
7
|
+
import { resolveFragmentCurrentFrames, validateProposedFrameDeltas, } from './frameDeltaAdapter';
|
|
5
8
|
const key = Symbol('scene-builder-context');
|
|
6
9
|
export const provideSceneBuilder = (onInfer) => {
|
|
7
10
|
const partConfig = usePartConfig();
|
|
11
|
+
const fragmentInfo = useFragmentInfo();
|
|
12
|
+
const frames = useFrames();
|
|
13
|
+
const configFrames = useConfigFrames();
|
|
14
|
+
const fragmentFrames = $derived(resolveFragmentCurrentFrames(Object.keys(fragmentInfo.current), fragmentInfo.current, frames.current ?? [], configFrames.current ?? {}));
|
|
8
15
|
let uiState = $state('idle');
|
|
9
16
|
let deltas = $state([]);
|
|
10
17
|
let explanation = $state('');
|
|
@@ -13,7 +20,7 @@ export const provideSceneBuilder = (onInfer) => {
|
|
|
13
20
|
// confirm() therefore always applies the LLM's intent against the latest config —
|
|
14
21
|
// drag changes to unspecified axes are preserved, not overwritten.
|
|
15
22
|
const validation = $derived.by(() => deltas.length > 0
|
|
16
|
-
? validateProposedFrameDeltas(deltas, partConfig.current)
|
|
23
|
+
? validateProposedFrameDeltas(deltas, partConfig.current, fragmentFrames)
|
|
17
24
|
: { prepared: [], errors: [] });
|
|
18
25
|
const updateErrors = $derived(validation.errors);
|
|
19
26
|
const diffGroups = $derived(validation.prepared.flatMap((u) => {
|
|
@@ -87,20 +94,37 @@ export const provideSceneBuilder = (onInfer) => {
|
|
|
87
94
|
},
|
|
88
95
|
async submit(prompt) {
|
|
89
96
|
uiState = 'loading';
|
|
90
|
-
const
|
|
97
|
+
const partComponents = partConfig.current.components
|
|
91
98
|
.filter((c) => c.frame !== undefined)
|
|
92
99
|
.map(({ name, frame }) => {
|
|
93
100
|
const pose = createPoseFromFrame(frame);
|
|
94
|
-
const
|
|
101
|
+
const orientation = poseToEulerDegrees(pose);
|
|
95
102
|
return {
|
|
96
103
|
name,
|
|
97
104
|
frame: {
|
|
98
105
|
parent: frame.parent,
|
|
99
106
|
translation: frame.translation,
|
|
100
|
-
orientation
|
|
107
|
+
orientation,
|
|
101
108
|
},
|
|
102
109
|
};
|
|
103
110
|
});
|
|
111
|
+
// Fragment components never appear in partConfig.components, so there's
|
|
112
|
+
// no name overlap with partComponents.
|
|
113
|
+
const fragmentComponents = Object.entries(fragmentFrames)
|
|
114
|
+
.filter(([, current]) => current.frame !== undefined)
|
|
115
|
+
.map(([name, current]) => {
|
|
116
|
+
const pose = createPoseFromFrame(current.frame);
|
|
117
|
+
const orientation = poseToEulerDegrees(pose);
|
|
118
|
+
return {
|
|
119
|
+
name,
|
|
120
|
+
frame: {
|
|
121
|
+
parent: current.frame.parent,
|
|
122
|
+
translation: current.frame.translation,
|
|
123
|
+
orientation,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
const components = [...partComponents, ...fragmentComponents];
|
|
104
128
|
try {
|
|
105
129
|
const data = await onInfer(prompt.trim(), components);
|
|
106
130
|
deltas = data.updates;
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
import { getTriangleBoxesFromIndices, getTriangleFromIndex, raycast } from './utils'
|
|
17
17
|
|
|
18
18
|
interface Props {
|
|
19
|
-
|
|
19
|
+
enabled?: boolean
|
|
20
|
+
selecting?: boolean
|
|
20
21
|
debug?: boolean
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
let {
|
|
24
|
+
let { enabled = false, selecting = false, debug = false }: Props = $props()
|
|
24
25
|
|
|
25
26
|
const world = useWorld()
|
|
26
27
|
const controls = useCameraControls()
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
let drawing = false
|
|
38
39
|
|
|
39
40
|
const onpointerdown = (event: PointerEvent) => {
|
|
40
|
-
if (!event.shiftKey
|
|
41
|
+
if (!selecting && !event.shiftKey) return
|
|
41
42
|
|
|
42
43
|
const { x, y } = raycast(event, camera.current)
|
|
43
44
|
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const onpointermove = (event: PointerEvent) => {
|
|
64
|
-
if (!drawing
|
|
65
|
+
if (!drawing) return
|
|
65
66
|
|
|
66
67
|
let ellipse = world.query(selectionTraits.Ellipse).at(-1)
|
|
67
68
|
|
|
@@ -129,13 +130,13 @@
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
const onpointerleave = () => {
|
|
132
|
-
if (!drawing
|
|
133
|
+
if (!drawing) return
|
|
133
134
|
|
|
134
135
|
onpointerup()
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
const onpointerup = () => {
|
|
138
|
-
if (!drawing
|
|
139
|
+
if (!drawing) return
|
|
139
140
|
|
|
140
141
|
drawing = false
|
|
141
142
|
|
|
@@ -244,6 +245,8 @@
|
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
$effect(() => {
|
|
248
|
+
if (!enabled) return
|
|
249
|
+
|
|
247
250
|
globalThis.addEventListener('keydown', onkeydown)
|
|
248
251
|
globalThis.addEventListener('keyup', onkeyup)
|
|
249
252
|
dom.addEventListener('pointerdown', onpointerdown)
|
|
@@ -263,26 +266,6 @@
|
|
|
263
266
|
|
|
264
267
|
const ellipses = useQuery(selectionTraits.Ellipse)
|
|
265
268
|
|
|
266
|
-
$effect(() => {
|
|
267
|
-
if (!controls.current) return
|
|
268
|
-
|
|
269
|
-
const currentControls = controls.current
|
|
270
|
-
|
|
271
|
-
if ('minPolarAngle' in currentControls) {
|
|
272
|
-
const { minPolarAngle, maxPolarAngle } = currentControls
|
|
273
|
-
|
|
274
|
-
// Locks the camera to top down while this component is mounted
|
|
275
|
-
currentControls.polarAngle = 0
|
|
276
|
-
currentControls.minPolarAngle = 0
|
|
277
|
-
currentControls.maxPolarAngle = 0
|
|
278
|
-
|
|
279
|
-
return () => {
|
|
280
|
-
currentControls.minPolarAngle = minPolarAngle
|
|
281
|
-
currentControls.maxPolarAngle = maxPolarAngle
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
})
|
|
285
|
-
|
|
286
269
|
// On unmount, destroy all lasso related entities
|
|
287
270
|
$effect(() => {
|
|
288
271
|
return () => {
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
import { getTriangleBoxesFromIndices, getTriangleFromIndex, raycast } from './utils'
|
|
17
17
|
|
|
18
18
|
interface Props {
|
|
19
|
-
|
|
19
|
+
enabled?: boolean
|
|
20
|
+
selecting?: boolean
|
|
20
21
|
debug?: boolean
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
let {
|
|
24
|
+
let { enabled = false, selecting = false, debug = false }: Props = $props()
|
|
24
25
|
|
|
25
26
|
const world = useWorld()
|
|
26
27
|
const controls = useCameraControls()
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
let drawing = false
|
|
38
39
|
|
|
39
40
|
const onpointerdown = (event: PointerEvent) => {
|
|
40
|
-
if (!event.shiftKey
|
|
41
|
+
if (!selecting && !event.shiftKey) return
|
|
41
42
|
|
|
42
43
|
const { x, y } = raycast(event, camera.current)
|
|
43
44
|
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
const onpointermove = (event: PointerEvent) => {
|
|
63
|
-
if (!drawing
|
|
64
|
+
if (!drawing) return
|
|
64
65
|
|
|
65
66
|
let lasso = world.query(selectionTraits.Lasso).at(-1)
|
|
66
67
|
|
|
@@ -100,13 +101,13 @@
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
const onpointerleave = () => {
|
|
103
|
-
if (!drawing
|
|
104
|
+
if (!drawing) return
|
|
104
105
|
|
|
105
106
|
onpointerup()
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
const onpointerup = () => {
|
|
109
|
-
if (!drawing
|
|
110
|
+
if (!drawing) return
|
|
110
111
|
|
|
111
112
|
drawing = false
|
|
112
113
|
|
|
@@ -225,6 +226,8 @@
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
$effect(() => {
|
|
229
|
+
if (!enabled) return
|
|
230
|
+
|
|
228
231
|
globalThis.addEventListener('keydown', onkeydown)
|
|
229
232
|
globalThis.addEventListener('keyup', onkeyup)
|
|
230
233
|
dom.addEventListener('pointerdown', onpointerdown)
|
|
@@ -244,26 +247,6 @@
|
|
|
244
247
|
|
|
245
248
|
const lassos = useQuery(selectionTraits.Lasso)
|
|
246
249
|
|
|
247
|
-
$effect(() => {
|
|
248
|
-
if (!controls.current) return
|
|
249
|
-
|
|
250
|
-
const currentControls = controls.current
|
|
251
|
-
|
|
252
|
-
if ('minPolarAngle' in currentControls) {
|
|
253
|
-
const { minPolarAngle, maxPolarAngle } = currentControls
|
|
254
|
-
|
|
255
|
-
// Locks the camera to top down while this component is mounted
|
|
256
|
-
currentControls.polarAngle = 0
|
|
257
|
-
currentControls.minPolarAngle = 0
|
|
258
|
-
currentControls.maxPolarAngle = 0
|
|
259
|
-
|
|
260
|
-
return () => {
|
|
261
|
-
currentControls.minPolarAngle = minPolarAngle
|
|
262
|
-
currentControls.maxPolarAngle = maxPolarAngle
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
|
|
267
250
|
// On unmount, destroy all lasso related entities
|
|
268
251
|
$effect(() => {
|
|
269
252
|
return () => {
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
interface Props {
|
|
20
20
|
/** Whether to auto-enable lasso mode when the component mounts */
|
|
21
21
|
enabled?: boolean
|
|
22
|
+
|
|
23
|
+
/** Allow manually going into selection state */
|
|
24
|
+
selecting?: boolean
|
|
25
|
+
|
|
22
26
|
// TODO: remove once a Selected trait exists
|
|
23
27
|
autoSelectNewEntities?: boolean
|
|
24
28
|
children?: Snippet
|
|
@@ -26,7 +30,12 @@
|
|
|
26
30
|
|
|
27
31
|
type SelectionType = 'lasso' | 'ellipse'
|
|
28
32
|
|
|
29
|
-
let {
|
|
33
|
+
let {
|
|
34
|
+
enabled = false,
|
|
35
|
+
selecting = false,
|
|
36
|
+
autoSelectNewEntities = false,
|
|
37
|
+
children,
|
|
38
|
+
}: Props = $props()
|
|
30
39
|
|
|
31
40
|
const { dom } = useThrelte()
|
|
32
41
|
const world = useWorld()
|
|
@@ -113,7 +122,13 @@
|
|
|
113
122
|
</Portal>
|
|
114
123
|
|
|
115
124
|
{#if isSelectionMode && rect.height > 0 && rect.width > 0}
|
|
116
|
-
<Ellipse
|
|
117
|
-
|
|
125
|
+
<Ellipse
|
|
126
|
+
enabled={selectionType === 'ellipse'}
|
|
127
|
+
{selecting}
|
|
128
|
+
/>
|
|
129
|
+
<Lasso
|
|
130
|
+
enabled={selectionType === 'lasso'}
|
|
131
|
+
{selecting}
|
|
132
|
+
/>
|
|
118
133
|
{@render children?.()}
|
|
119
134
|
{/if}
|
|
@@ -2,6 +2,8 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
/** Whether to auto-enable lasso mode when the component mounts */
|
|
4
4
|
enabled?: boolean;
|
|
5
|
+
/** Allow manually going into selection state */
|
|
6
|
+
selecting?: boolean;
|
|
5
7
|
autoSelectNewEntities?: boolean;
|
|
6
8
|
children?: Snippet;
|
|
7
9
|
}
|