@viamrobotics/motion-tools 0.13.0 → 0.14.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.
@@ -11,11 +11,15 @@ type UpdateFrameCallback = {
11
11
  z?: number;
12
12
  }): void;
13
13
  };
14
+ type RemoveFrameCallback = {
15
+ (componentName: string): void;
16
+ };
14
17
  export declare class DetailConfigUpdater {
15
18
  private object;
16
19
  private referenceFrame;
17
20
  private updateFrame;
18
- constructor(object: () => WorldObject<Geometries> | undefined, updateFrame: UpdateFrameCallback, referenceFrame: () => string);
21
+ private removeFrame;
22
+ constructor(object: () => WorldObject<Geometries> | undefined, updateFrame: UpdateFrameCallback, removeFrame: RemoveFrameCallback, referenceFrame: () => string);
19
23
  updateLocalPosition: ({ x, y, z }: {
20
24
  x?: number;
21
25
  y?: number;
@@ -36,6 +40,7 @@ export declare class DetailConfigUpdater {
36
40
  z?: number;
37
41
  }) => void;
38
42
  setFrameParent: (parentName: string) => void;
43
+ deleteFrame: () => void;
39
44
  setGeometryType: (type: "none" | "box" | "sphere" | "capsule") => void;
40
45
  }
41
46
  export {};
@@ -2,10 +2,12 @@ export class DetailConfigUpdater {
2
2
  object;
3
3
  referenceFrame;
4
4
  updateFrame;
5
- constructor(object, updateFrame, referenceFrame) {
5
+ removeFrame;
6
+ constructor(object, updateFrame, removeFrame, referenceFrame) {
6
7
  this.referenceFrame = referenceFrame;
7
8
  this.object = object;
8
9
  this.updateFrame = updateFrame;
10
+ this.removeFrame = removeFrame;
9
11
  }
10
12
  updateLocalPosition = ({ x, y, z }) => {
11
13
  const object = this.object();
@@ -95,6 +97,12 @@ export class DetailConfigUpdater {
95
97
  theta: object.localEditedPose.theta,
96
98
  });
97
99
  };
100
+ deleteFrame = () => {
101
+ const object = this.object();
102
+ if (!object)
103
+ return;
104
+ this.removeFrame(object.name ?? '');
105
+ };
98
106
  setGeometryType = (type) => {
99
107
  const object = this.object();
100
108
  if (!object)
@@ -1,5 +1,6 @@
1
- import type { Geometry, Pose, TransformWithUUID } from '@viamrobotics/sdk';
2
- import { BatchedMesh, Object3D, Vector3, type ColorRepresentation } from 'three';
1
+ import type { Geometry, PlainMessage, Pose, Struct, TransformWithUUID } from '@viamrobotics/sdk';
2
+ import { BatchedMesh, Color, Object3D, Vector3 } from 'three';
3
+ import type { ValueOf } from 'type-fest';
3
4
  import type { OBB } from 'three/addons/math/OBB.js';
4
5
  export type PointsGeometry = {
5
6
  center: undefined;
@@ -16,9 +17,14 @@ export type LinesGeometry = {
16
17
  };
17
18
  };
18
19
  export type Geometries = Geometry | PointsGeometry | LinesGeometry;
20
+ export declare const SupportedShapes: {
21
+ readonly points: "points";
22
+ readonly line: "line";
23
+ readonly arrow: "arrow";
24
+ };
19
25
  export type Metadata = {
20
26
  colors?: Float32Array;
21
- color?: ColorRepresentation;
27
+ color?: Color;
22
28
  opacity?: number;
23
29
  gltf?: {
24
30
  scene: Object3D;
@@ -26,22 +32,25 @@ export type Metadata = {
26
32
  points?: Vector3[];
27
33
  pointSize?: number;
28
34
  lineWidth?: number;
29
- lineDotColor?: ColorRepresentation;
35
+ lineDotColor?: Color;
30
36
  batched?: {
31
37
  id: number;
32
38
  object: BatchedMesh;
33
39
  };
40
+ shape?: ValueOf<typeof SupportedShapes>;
34
41
  getBoundingBoxAt?: (box: OBB) => void;
35
42
  };
36
- export declare const determinePose: (object: WorldObject, pose: WorldObject["pose"] | undefined) => WorldObject["pose"];
43
+ export declare const isMetadataKey: (key: string) => key is keyof Metadata;
37
44
  export declare class WorldObject<T extends Geometries = Geometries> {
38
45
  uuid: string;
39
46
  name: string;
40
47
  referenceFrame: string | undefined;
41
- pose: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Pose>;
48
+ pose: PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Pose>;
42
49
  geometry?: T;
43
50
  metadata: Metadata;
44
- localEditedPose: import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Pose>;
51
+ localEditedPose: PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Pose>;
45
52
  constructor(name: string, pose?: Pose, parent?: string, geometry?: T, metadata?: Metadata);
46
53
  }
47
- export declare const fromTransform: (transform: TransformWithUUID) => WorldObject<import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Geometry>>;
54
+ export declare const parseMetadata: (fields?: PlainMessage<Struct>["fields"]) => Metadata;
55
+ export declare const fromTransform: (transform: TransformWithUUID) => WorldObject<PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Geometry>>;
56
+ export declare const determinePose: (object: WorldObject, pose: Pose | undefined) => Pose;
@@ -1,17 +1,25 @@
1
- import { BatchedMesh, Color, MathUtils, Matrix4, Object3D, Quaternion, Vector3, } from 'three';
2
- import { createPose } from './transform';
3
- export const determinePose = (object, pose) => {
4
- if (pose === undefined) {
5
- return object.localEditedPose;
6
- }
7
- else {
8
- const poseNetwork = poseToMatrix(object.pose);
9
- const poseUsePose = poseToMatrix(pose);
10
- const poseLocalEditedPose = poseToMatrix(object.localEditedPose);
11
- const poseNetworkInverse = poseNetwork.invert();
12
- const resultMatrix = poseUsePose.multiply(poseNetworkInverse).multiply(poseLocalEditedPose);
13
- return matrixToPose(resultMatrix);
14
- }
1
+ import { BatchedMesh, Color, MathUtils, Object3D, Vector3 } from 'three';
2
+ import { createPose, matrixToPose, poseToMatrix } from './transform';
3
+ import { isColorRepresentation, isRGB, parseColor, parseOpacity, parseRGB } from './color';
4
+ export const SupportedShapes = {
5
+ points: 'points',
6
+ line: 'line',
7
+ arrow: 'arrow',
8
+ };
9
+ const METADATA_KEYS = [
10
+ 'colors',
11
+ 'color',
12
+ 'opacity',
13
+ 'gltf',
14
+ 'points',
15
+ 'pointSize',
16
+ 'lineWidth',
17
+ 'lineDotColor',
18
+ 'batched',
19
+ 'shape',
20
+ ];
21
+ export const isMetadataKey = (key) => {
22
+ return METADATA_KEYS.includes(key);
15
23
  };
16
24
  export class WorldObject {
17
25
  uuid;
@@ -19,20 +27,23 @@ export class WorldObject {
19
27
  referenceFrame = $state.raw();
20
28
  pose = $state.raw(createPose());
21
29
  geometry;
22
- metadata;
30
+ metadata = $state({});
23
31
  localEditedPose = $state.raw(createPose());
24
32
  constructor(name, pose, parent = 'world', geometry, metadata) {
25
33
  this.uuid = MathUtils.generateUUID();
26
34
  this.name = name;
27
35
  this.referenceFrame = parent;
28
36
  this.geometry = geometry;
29
- this.metadata = metadata ?? {};
37
+ if (metadata) {
38
+ this.metadata = metadata;
39
+ }
30
40
  if (pose) {
31
41
  this.pose = pose;
32
42
  this.localEditedPose = { ...pose };
33
43
  }
34
44
  }
35
45
  }
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
47
  const unwrapValue = (value) => {
37
48
  if (!value?.kind)
38
49
  return value;
@@ -42,9 +53,9 @@ const unwrapValue = (value) => {
42
53
  case 'boolValue':
43
54
  return value.kind.value;
44
55
  case 'structValue': {
45
- // Recursively unwrap nested struct
46
56
  const result = {};
47
57
  for (const [key, val] of Object.entries(value.kind.value.fields || {})) {
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
59
  result[key] = unwrapValue(val);
49
60
  }
50
61
  return result;
@@ -57,56 +68,65 @@ const unwrapValue = (value) => {
57
68
  return value.kind.value;
58
69
  }
59
70
  };
60
- const parseMetadata = (metadata) => {
61
- let json = {};
62
- for (const [k, v] of Object.entries(metadata)) {
71
+ export const parseMetadata = (fields = {}) => {
72
+ const json = {};
73
+ for (const [k, v] of Object.entries(fields)) {
74
+ if (!isMetadataKey(k))
75
+ continue;
63
76
  const unwrappedValue = unwrapValue(v);
64
- // TODO: Remove special case and add better handling for metadata
65
- if (k === 'color' && unwrappedValue && typeof unwrappedValue === 'object') {
66
- const { r, g, b } = unwrappedValue;
67
- json[k] = new Color().setRGB(r / 255, g / 255, b / 255);
68
- }
69
- else {
70
- json = { ...json, [k]: unwrappedValue };
77
+ switch (k) {
78
+ case 'color':
79
+ case 'lineDotColor':
80
+ json[k] = readColor(unwrappedValue);
81
+ break;
82
+ case 'opacity':
83
+ json[k] = parseOpacity(unwrappedValue);
84
+ break;
85
+ case 'gltf':
86
+ json[k] = unwrappedValue;
87
+ break;
88
+ case 'points':
89
+ json[k] = unwrappedValue;
90
+ break;
91
+ case 'pointSize':
92
+ json[k] = unwrappedValue;
93
+ break;
94
+ case 'lineWidth':
95
+ json[k] = unwrappedValue;
96
+ break;
97
+ case 'batched':
98
+ json[k] = unwrappedValue;
99
+ break;
100
+ case 'shape':
101
+ json[k] = unwrappedValue;
102
+ break;
71
103
  }
72
104
  }
73
105
  return json;
74
106
  };
107
+ const readColor = (color) => {
108
+ if (isColorRepresentation(color))
109
+ return parseColor(color);
110
+ if (isRGB(color))
111
+ return parseRGB(color);
112
+ return new Color('black');
113
+ };
75
114
  export const fromTransform = (transform) => {
76
- const metadata = transform.metadata
77
- ? parseMetadata(transform.metadata.fields)
78
- : {};
115
+ const metadata = transform.metadata ? parseMetadata(transform.metadata.fields) : {};
79
116
  const worldObject = new WorldObject(transform.referenceFrame, transform.poseInObserverFrame?.pose, transform.poseInObserverFrame?.referenceFrame, transform.physicalObject, metadata);
80
117
  worldObject.uuid = transform.uuidString;
81
118
  return worldObject;
82
119
  };
83
- const poseToMatrix = (pose) => {
84
- const matrix = new Matrix4();
85
- const poseQuaternion = new Quaternion().setFromAxisAngle(new Vector3(pose.oX, pose.oY, pose.oZ), pose.theta * (Math.PI / 180));
86
- matrix.makeRotationFromQuaternion(poseQuaternion);
87
- matrix.setPosition(new Vector3(pose.x, pose.y, pose.z));
88
- return matrix;
89
- };
90
- const matrixToPose = (matrix) => {
91
- const pose = createPose();
92
- const translation = new Vector3();
93
- const quaternion = new Quaternion();
94
- matrix.decompose(translation, quaternion, new Vector3());
95
- pose.x = translation.x;
96
- pose.y = translation.y;
97
- pose.z = translation.z;
98
- const s = Math.sqrt(1 - quaternion.w * quaternion.w);
99
- if (s < 0.000001) {
100
- pose.oX = 0;
101
- pose.oY = 0;
102
- pose.oZ = 1;
103
- pose.theta = 0;
120
+ export const determinePose = (object, pose) => {
121
+ if (pose === undefined) {
122
+ return object.localEditedPose;
104
123
  }
105
124
  else {
106
- pose.oX = quaternion.x / s;
107
- pose.oY = quaternion.y / s;
108
- pose.oZ = quaternion.z / s;
109
- pose.theta = Math.acos(quaternion.w) * 2 * (180 / Math.PI);
125
+ const poseNetwork = poseToMatrix(object.pose);
126
+ const poseUsePose = poseToMatrix(pose);
127
+ const poseLocalEditedPose = poseToMatrix(object.localEditedPose);
128
+ const poseNetworkInverse = poseNetwork.invert();
129
+ const resultMatrix = poseUsePose.multiply(poseNetworkInverse).multiply(poseLocalEditedPose);
130
+ return matrixToPose(resultMatrix);
110
131
  }
111
- return pose;
112
132
  };
package/dist/color.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Color, type ColorRepresentation } from 'three';
1
+ import { Color, type ColorRepresentation, type RGB } from 'three';
2
2
  /**
3
3
  * Darkens a THREE.Color by a given percentage while preserving hue.
4
4
  * @param color The original THREE.Color instance.
@@ -27,3 +27,8 @@ export declare const resourceColors: {
27
27
  readonly switch: string;
28
28
  readonly webcam: string;
29
29
  };
30
+ export declare const isColorRepresentation: (color: unknown) => color is ColorRepresentation;
31
+ export declare const parseColor: (color: unknown, defaultColor?: ColorRepresentation) => Color;
32
+ export declare const isRGB: (color: unknown) => color is RGB;
33
+ export declare const parseRGB: (color: unknown, defaultColor?: RGB) => Color;
34
+ export declare const parseOpacity: (opacity: unknown, defaultOpacity?: number) => number;
package/dist/color.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Color } from 'three';
2
2
  import twColors from 'tailwindcss/colors';
3
+ import { isNumber } from 'lodash-es';
3
4
  // Step 3: linear sRGB → sRGB
4
5
  const linearToSrgb = (x) => {
5
6
  return x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
@@ -73,3 +74,62 @@ export const resourceColors = {
73
74
  switch: oklchToHex(twColors.stone[darkness]),
74
75
  webcam: oklchToHex(twColors.sky[darkness]),
75
76
  };
77
+ export const isColorRepresentation = (color) => {
78
+ if (!color)
79
+ return false;
80
+ if (isColorString(color))
81
+ return true;
82
+ if (isColorHex(color))
83
+ return true;
84
+ if (isColor(color))
85
+ return true;
86
+ return false;
87
+ };
88
+ export const parseColor = (color, defaultColor = 'black') => {
89
+ if (!isColorRepresentation(color))
90
+ return new Color(defaultColor);
91
+ return new Color(color);
92
+ };
93
+ export const isRGB = (color) => {
94
+ if (!color ||
95
+ typeof color !== 'object' ||
96
+ !('r' in color) ||
97
+ !('g' in color) ||
98
+ !('b' in color)) {
99
+ return false;
100
+ }
101
+ return isNumber(color.r) && isNumber(color.g) && isNumber(color.b);
102
+ };
103
+ export const parseRGB = (color, defaultColor = { r: 0, g: 0, b: 0 }) => {
104
+ if (!isRGB(color))
105
+ return new Color().setRGB(defaultColor.r, defaultColor.g, defaultColor.b);
106
+ return new Color().setRGB(color.r, color.g, color.b);
107
+ };
108
+ export const parseOpacity = (opacity, defaultOpacity = 1) => {
109
+ if (!isNumber(opacity))
110
+ return defaultOpacity;
111
+ return opacity > 1 ? opacity / 100 : opacity;
112
+ };
113
+ const isColor = (color) => {
114
+ if (!color)
115
+ return false;
116
+ return color instanceof Color;
117
+ };
118
+ const isColorString = (color) => {
119
+ if (!color)
120
+ return false;
121
+ if (typeof color === 'string') {
122
+ const parsed = new Color(color);
123
+ return parsed.isColor;
124
+ }
125
+ return false;
126
+ };
127
+ const isColorHex = (color) => {
128
+ if (!color)
129
+ return false;
130
+ if (typeof color === 'number') {
131
+ const parsed = new Color(color);
132
+ return parsed.isColor;
133
+ }
134
+ return false;
135
+ };
@@ -21,6 +21,7 @@
21
21
  import { useViamClient } from '@viamrobotics/svelte-sdk'
22
22
  import LiveUpdatesBanner from './LiveUpdatesBanner.svelte'
23
23
  import ArmPositions from './widgets/ArmPositions.svelte'
24
+ import { provideEnvironment } from '../hooks/useEnvironment.svelte'
24
25
 
25
26
  interface LocalConfigProps {
26
27
  getLocalPartConfig: () => Struct
@@ -45,6 +46,7 @@
45
46
 
46
47
  const appClient = useViamClient()
47
48
  const settings = provideSettings()
49
+ const environment = provideEnvironment()
48
50
 
49
51
  $effect(() => {
50
52
  settings.current.enableKeybindings = enableKeybindings
@@ -55,10 +57,9 @@
55
57
  provideToast()
56
58
 
57
59
  let root = $state.raw<HTMLElement>()
58
- let isStandalone = $state(false)
59
60
 
60
61
  if (localConfigProps) {
61
- isStandalone = false
62
+ environment.current.isStandalone = false
62
63
  providePartConfig({
63
64
  appEmbeddedPartConfigProps: {
64
65
  isDirty: () => localConfigProps.isDirty(),
@@ -68,7 +69,7 @@
68
69
  },
69
70
  })
70
71
  } else {
71
- isStandalone = true
72
+ environment.current.isStandalone = true
72
73
  providePartConfig({
73
74
  standalonePartConfigProps: {
74
75
  viamClient: () => appClient?.current,
@@ -99,7 +100,7 @@
99
100
 
100
101
  <Dashboard {@attach domPortal(root)} />
101
102
  <Details {@attach domPortal(root)} />
102
- {#if isStandalone}
103
+ {#if environment.current.isStandalone}
103
104
  <LiveUpdatesBanner {@attach domPortal(root)} />
104
105
  {/if}
105
106
 
@@ -60,6 +60,7 @@
60
60
  const detailConfigUpdater: DetailConfigUpdater = new DetailConfigUpdater(
61
61
  () => object,
62
62
  partConfig.updateFrame,
63
+ partConfig.deleteFrame,
63
64
  () => referenceFrame
64
65
  )
65
66
 
@@ -493,6 +494,16 @@
493
494
 
494
495
  <h3 class="text-subtle-2 pt-3 pb-2">Actions</h3>
495
496
 
497
+ <WeblabActive experiment="MOTION_TOOLS_EDIT_FRAME">
498
+ {#if isFrameNode}
499
+ <Button
500
+ variant="danger"
501
+ class="mb-2 w-full"
502
+ onclick={() => detailConfigUpdater.deleteFrame()}>Delete Frame</Button
503
+ >
504
+ {/if}
505
+ </WeblabActive>
506
+
496
507
  {#if focused.current}
497
508
  <Button
498
509
  class="w-full"
@@ -19,18 +19,18 @@
19
19
  </p>
20
20
 
21
21
  <p class="text-sm">
22
- <a
22
+ <button
23
23
  class="cursor-pointer text-blue-600"
24
24
  onclick={() => {
25
25
  partConfig.saveLocalPartConfig()
26
- }}>Save</a
26
+ }}>Save</button
27
27
  >
28
28
  or
29
- <a
29
+ <button
30
30
  class="cursor-pointer text-blue-600"
31
31
  onclick={() => {
32
32
  partConfig.resetLocalPartConfig()
33
- }}>Discard</a
33
+ }}>Discard</button
34
34
  >
35
35
  to resume.
36
36
  </p>
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import Drawer from './Drawer.svelte'
3
+ import { usePartConfig } from '../../hooks/usePartConfig.svelte'
4
+ import { useFramelessComponents } from '../../hooks/useFramelessComponents.svelte'
5
+
6
+ const framelessComponents = useFramelessComponents()
7
+ const partConfig = usePartConfig()
8
+ </script>
9
+
10
+ <Drawer name="Add frames">
11
+ <div class="flex h-64 w-60 flex-col gap-2 overflow-auto p-3">
12
+ {#if framelessComponents.current.length > 0}
13
+ <ul class="space-y-1">
14
+ {#each framelessComponents.current as component (component)}
15
+ <li class="text-sm text-gray-700">
16
+ {component}
17
+ <button
18
+ class="focus:ring-opacity-50 ml-2 rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
19
+ onclick={() => partConfig.createFrame(component)}>Add Frame</button
20
+ >
21
+ </li>
22
+ {/each}
23
+ </ul>
24
+ {:else}
25
+ No components without frames
26
+ {/if}
27
+ </div>
28
+ </Drawer>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const AddFrames: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type AddFrames = InstanceType<typeof AddFrames>;
18
+ export default AddFrames;
@@ -11,7 +11,8 @@
11
11
  import { useDraggable } from '../../hooks/useDraggable.svelte'
12
12
  import { useWorldStates } from '../../hooks/useWorldState.svelte'
13
13
  import Widgets from './Widgets.svelte'
14
-
14
+ import AddFrames from './AddFrames.svelte'
15
+ import { useEnvironment } from '../../hooks/useEnvironment.svelte'
15
16
  const { ...rest } = $props()
16
17
 
17
18
  provideTreeExpandedContext()
@@ -20,6 +21,7 @@
20
21
  const objects = useObjects()
21
22
  const draggable = useDraggable('treeview')
22
23
  const worldStates = useWorldStates()
24
+ const environment = useEnvironment()
23
25
 
24
26
  let rootNode = $state<TreeNode>({
25
27
  id: 'world',
@@ -54,6 +56,9 @@
54
56
  />
55
57
  {/key}
56
58
 
59
+ {#if environment.current.isStandalone}
60
+ <AddFrames />
61
+ {/if}
57
62
  <Logs />
58
63
  <Settings />
59
64
  <Widgets />
@@ -1,18 +1,70 @@
1
1
  <script lang="ts">
2
+ import { Color, Vector3 } from 'three'
3
+
2
4
  import Frame from './Frame.svelte'
3
5
  import Label from './Label.svelte'
4
6
  import Portal from './portal/Portal.svelte'
5
7
  import PortalTarget from './portal/PortalTarget.svelte'
6
8
  import { WorldObject } from '../WorldObject.svelte'
9
+ import { useArrows } from '../hooks/useArrows.svelte'
10
+ import { poseToDirection } from '../transform'
7
11
 
8
12
  interface Props {
9
13
  worldObjects: WorldObject[]
10
14
  }
11
15
 
12
16
  let { worldObjects }: Props = $props()
17
+
18
+ const batchedArrow = useArrows()
19
+ const currentArrows: Record<string, { id: number; arrow: WorldObject }> = {}
20
+
21
+ const arrows = $derived(worldObjects.filter((object) => object.metadata?.shape === 'arrow'))
22
+ const objects = $derived(worldObjects.filter((object) => object.metadata?.shape !== 'arrow'))
23
+
24
+ const getArrows = () => ({ ...currentArrows })
25
+ const getArrow = (uuid: string) => currentArrows[uuid]
26
+ const removeArrow = (uuid: string) => delete currentArrows[uuid]
27
+ const setArrow = (arrow: WorldObject) => {
28
+ const currentArrow = getArrow(arrow.uuid)
29
+ const color = arrow.metadata?.color ?? new Color('yellow')
30
+ if (currentArrow) {
31
+ batchedArrow.updateArrow(
32
+ currentArrow.id,
33
+ poseToDirection(arrow.pose),
34
+ new Vector3(arrow.pose.x, arrow.pose.y, arrow.pose.z),
35
+ 0.1,
36
+ color,
37
+ true
38
+ )
39
+
40
+ currentArrows[arrow.uuid] = { id: currentArrow.id, arrow }
41
+ } else {
42
+ const id = batchedArrow.addArrow(
43
+ poseToDirection(arrow.pose),
44
+ new Vector3(arrow.pose.x, arrow.pose.y, arrow.pose.z),
45
+ 0.1,
46
+ color,
47
+ true
48
+ )
49
+ currentArrows[arrow.uuid] = { id, arrow }
50
+ }
51
+ }
52
+
53
+ $effect(() => {
54
+ const toRemove = getArrows()
55
+ arrows.forEach((arrow) => {
56
+ setArrow(arrow)
57
+ delete toRemove[arrow.uuid]
58
+ })
59
+
60
+ Object.values(toRemove).forEach(({ id, arrow }) => {
61
+ batchedArrow.removeArrow(id)
62
+ removeArrow(arrow.uuid)
63
+ })
64
+ })
13
65
  </script>
14
66
 
15
- {#each worldObjects as object (object.uuid)}
67
+ {#each objects as object (object.uuid)}
16
68
  <Portal id={object.referenceFrame}>
17
69
  <Frame
18
70
  uuid={object.uuid}
@@ -92,7 +92,7 @@ export const provideDrawAPI = () => {
92
92
  geometry.geometryType.value = data.capsule;
93
93
  }
94
94
  const object = new WorldObject(data.label ?? ++geometryIndex, data.center, parent, geometry, {
95
- color,
95
+ color: new Color(color),
96
96
  });
97
97
  meshes.push(object);
98
98
  };
@@ -104,7 +104,7 @@ export const provideDrawAPI = () => {
104
104
  }
105
105
  const controlPoints = data.ControlPts.map((point) => new Vector4(point.x / 1000, point.y / 1000, point.z / 1000));
106
106
  const curve = new NURBSCurve(data.Degree, data.Knots, controlPoints);
107
- const object = new WorldObject(data.name, data.pose, data.parent, { center: undefined, geometryType: { case: 'line', value: new Float32Array() } }, { color, points: curve.getPoints(200) });
107
+ const object = new WorldObject(data.name, data.pose, data.parent, { center: undefined, geometryType: { case: 'line', value: new Float32Array() } }, { color: new Color(color), points: curve.getPoints(200) });
108
108
  nurbs.push(object);
109
109
  };
110
110
  const drawPoses = async (reader) => {
@@ -0,0 +1,10 @@
1
+ interface Environemnt {
2
+ viewerMode: 'edit' | 'monitor';
3
+ isStandalone: boolean;
4
+ }
5
+ interface Context {
6
+ current: Environemnt;
7
+ }
8
+ export declare const provideEnvironment: () => Context;
9
+ export declare const useEnvironment: () => Context;
10
+ export {};
@@ -0,0 +1,19 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const key = Symbol('environment');
3
+ const defaults = () => ({
4
+ viewerMode: 'monitor',
5
+ isStandalone: true,
6
+ });
7
+ export const provideEnvironment = () => {
8
+ const environment = $state(defaults());
9
+ const context = {
10
+ get current() {
11
+ return environment;
12
+ },
13
+ };
14
+ setContext(key, context);
15
+ return context;
16
+ };
17
+ export const useEnvironment = () => {
18
+ return getContext(key);
19
+ };