@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.
Files changed (51) hide show
  1. package/dist/components/App.svelte +14 -2
  2. package/dist/components/App.svelte.d.ts +6 -2
  3. package/dist/components/AxesHelper.svelte +3 -1
  4. package/dist/components/AxesHelper.svelte.d.ts +1 -1
  5. package/dist/components/Entities/Arrows/Arrows.svelte +0 -9
  6. package/dist/components/Entities/AxesHelper.svelte +38 -0
  7. package/dist/components/Entities/AxesHelper.svelte.d.ts +8 -0
  8. package/dist/components/Entities/AxesHelpers.svelte +13 -0
  9. package/dist/{plugins/LLMSceneBuilder/AISettings.svelte.d.ts → components/Entities/AxesHelpers.svelte.d.ts} +6 -14
  10. package/dist/components/Entities/Boxes.svelte +290 -0
  11. package/dist/components/Entities/Boxes.svelte.d.ts +14 -0
  12. package/dist/components/Entities/Entities.svelte +10 -5
  13. package/dist/components/Entities/GLTF.svelte +0 -9
  14. package/dist/components/Entities/Line.svelte +0 -9
  15. package/dist/components/Entities/Mesh.svelte +5 -23
  16. package/dist/components/Entities/Points.svelte +1 -9
  17. package/dist/components/Entities/composeBoxMatrix.d.ts +12 -0
  18. package/dist/components/Entities/composeBoxMatrix.js +29 -0
  19. package/dist/components/Entities/hooks/useEntityEvents.svelte.d.ts +27 -0
  20. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +87 -39
  21. package/dist/components/Scene.svelte +0 -1
  22. package/dist/components/Selected.svelte +14 -3
  23. package/dist/components/SelectedTransformControls.svelte +3 -5
  24. package/dist/components/overlay/Details.svelte +9 -4
  25. package/dist/hooks/plugins/bvh.svelte.js +9 -0
  26. package/dist/hooks/useConfigFrames.svelte.js +5 -3
  27. package/dist/hooks/useFragmentInfo.svelte.d.ts +24 -0
  28. package/dist/hooks/useFragmentInfo.svelte.js +86 -0
  29. package/dist/hooks/useFramelessComponents.svelte.js +3 -1
  30. package/dist/hooks/usePartConfig.svelte.d.ts +0 -6
  31. package/dist/hooks/usePartConfig.svelte.js +5 -60
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/plugins/Focus/FocusBox.svelte +12 -1
  35. package/dist/plugins/LLMSceneBuilder/frameDeltaAdapter.d.ts +9 -2
  36. package/dist/plugins/LLMSceneBuilder/frameDeltaAdapter.js +65 -10
  37. package/dist/plugins/LLMSceneBuilder/useSceneBuilder.svelte.js +29 -5
  38. package/dist/plugins/Selection/Ellipse.svelte +9 -26
  39. package/dist/plugins/Selection/Ellipse.svelte.d.ts +2 -1
  40. package/dist/plugins/Selection/Lasso.svelte +9 -26
  41. package/dist/plugins/Selection/Lasso.svelte.d.ts +2 -1
  42. package/dist/plugins/Selection/SelectionTool.svelte +18 -3
  43. package/dist/plugins/Selection/SelectionTool.svelte.d.ts +2 -0
  44. package/dist/plugins/TopDownLock/TopDownLock.svelte +25 -0
  45. package/dist/plugins/TopDownLock/TopDownLock.svelte.d.ts +3 -0
  46. package/dist/plugins/index.d.ts +1 -0
  47. package/dist/plugins/index.js +1 -0
  48. package/dist/three/OBBHelper.d.ts +8 -1
  49. package/dist/three/OBBHelper.js +11 -1
  50. package/package.json +3 -2
  51. 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(partConfig.componentNameToFragmentInfo)) {
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 = config.componentNameToFragmentInfo[componentName]?.id;
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 = config.componentNameToFragmentInfo[componentName]?.id;
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 = config.componentNameToFragmentInfo[componentName]?.id;
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(config.components.map((c) => c.name));
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 component = config.components.find((c) => c.name === delta.componentName);
39
- if (!component) {
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 (!component.frame) {
44
- errors.push({ componentName: delta.componentName, reason: 'Component has no frame' });
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(component.frame);
59
- const previousParent = component.frame.parent;
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: component.frame.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 components = partConfig.current.components
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 { roll, pitch, yaw } = poseToEulerDegrees(pose);
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: { roll, pitch, yaw },
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
- active?: boolean
19
+ enabled?: boolean
20
+ selecting?: boolean
20
21
  debug?: boolean
21
22
  }
22
23
 
23
- let { active = false, debug = false }: Props = $props()
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 || !active) return
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 || !active) return
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 || !active) return
133
+ if (!drawing) return
133
134
 
134
135
  onpointerup()
135
136
  }
136
137
 
137
138
  const onpointerup = () => {
138
- if (!drawing || !active) return
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 () => {
@@ -1,5 +1,6 @@
1
1
  interface Props {
2
- active?: boolean;
2
+ enabled?: boolean;
3
+ selecting?: boolean;
3
4
  debug?: boolean;
4
5
  }
5
6
  declare const Ellipse: import("svelte").Component<Props, {}, "">;
@@ -16,11 +16,12 @@
16
16
  import { getTriangleBoxesFromIndices, getTriangleFromIndex, raycast } from './utils'
17
17
 
18
18
  interface Props {
19
- active?: boolean
19
+ enabled?: boolean
20
+ selecting?: boolean
20
21
  debug?: boolean
21
22
  }
22
23
 
23
- let { active = false, debug = false }: Props = $props()
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 || !active) return
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 || !active) return
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 || !active) return
104
+ if (!drawing) return
104
105
 
105
106
  onpointerup()
106
107
  }
107
108
 
108
109
  const onpointerup = () => {
109
- if (!drawing || !active) return
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 () => {
@@ -1,5 +1,6 @@
1
1
  interface Props {
2
- active?: boolean;
2
+ enabled?: boolean;
3
+ selecting?: boolean;
3
4
  debug?: boolean;
4
5
  }
5
6
  declare const Lasso: import("svelte").Component<Props, {}, "">;
@@ -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 { enabled = false, autoSelectNewEntities = false, children }: Props = $props()
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 active={selectionType === 'ellipse'} />
117
- <Lasso active={selectionType === 'lasso'} />
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
  }