@viamrobotics/motion-tools 0.14.3 → 0.14.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.
@@ -1,20 +1,14 @@
1
+ import type { Frame } from './frame';
1
2
  import type { WorldObject } from './lib';
2
3
  import type { Geometries } from './WorldObject.svelte';
3
4
  import type { Pose } from '@viamrobotics/sdk';
4
5
  type UpdateFrameCallback = {
5
- (componentName: string, referenceFrame: string, pose: Pose, geometry?: {
6
- type: 'none' | 'box' | 'sphere' | 'capsule';
7
- r?: number;
8
- l?: number;
9
- x?: number;
10
- y?: number;
11
- z?: number;
12
- }): void;
6
+ (componentName: string, referenceFrame: string, pose: Pose, geometry?: Frame['geometry']): void;
13
7
  };
14
8
  type RemoveFrameCallback = {
15
9
  (componentName: string): void;
16
10
  };
17
- export declare class DetailConfigUpdater {
11
+ export declare class FrameConfigUpdater {
18
12
  private object;
19
13
  private referenceFrame;
20
14
  private updateFrame;
@@ -31,14 +25,7 @@ export declare class DetailConfigUpdater {
31
25
  oZ?: number;
32
26
  theta?: number;
33
27
  }) => void;
34
- updateGeometry: (geometry: {
35
- type: "none" | "box" | "sphere" | "capsule";
36
- r?: number;
37
- l?: number;
38
- x?: number;
39
- y?: number;
40
- z?: number;
41
- }) => void;
28
+ updateGeometry: (geometry: Partial<Frame["geometry"]>) => void;
42
29
  setFrameParent: (parentName: string) => void;
43
30
  deleteFrame: () => void;
44
31
  setGeometryType: (type: "none" | "box" | "sphere" | "capsule") => void;
@@ -1,4 +1,5 @@
1
- export class DetailConfigUpdater {
1
+ import { createPose } from './transform';
2
+ export class FrameConfigUpdater {
2
3
  object;
3
4
  referenceFrame;
4
5
  updateFrame;
@@ -49,7 +50,7 @@ export class DetailConfigUpdater {
49
50
  if (!object)
50
51
  return;
51
52
  let geometryObject;
52
- if (geometry.type === 'box') {
53
+ if (geometry?.type === 'box') {
53
54
  const currentGeometry = object.geometry?.geometryType.value;
54
55
  geometryObject = {
55
56
  type: 'box',
@@ -58,14 +59,14 @@ export class DetailConfigUpdater {
58
59
  z: geometry.z ?? currentGeometry?.dimsMm?.z,
59
60
  };
60
61
  }
61
- else if (geometry.type === 'sphere') {
62
+ else if (geometry?.type === 'sphere') {
62
63
  const currentGeometry = object.geometry?.geometryType.value;
63
64
  geometryObject = {
64
65
  type: 'sphere',
65
66
  r: geometry.r ?? currentGeometry?.radiusMm,
66
67
  };
67
68
  }
68
- else if (geometry.type === 'capsule') {
69
+ else if (geometry?.type === 'capsule') {
69
70
  const currentGeometry = object.geometry?.geometryType.value;
70
71
  geometryObject = {
71
72
  type: 'capsule',
@@ -73,29 +74,13 @@ export class DetailConfigUpdater {
73
74
  l: geometry.l ?? currentGeometry?.lengthMm,
74
75
  };
75
76
  }
76
- this.updateFrame(object.name ?? '', this.referenceFrame(), {
77
- x: object.localEditedPose.x,
78
- y: object.localEditedPose.y,
79
- z: object.localEditedPose.z,
80
- oX: object.localEditedPose.oX,
81
- oY: object.localEditedPose.oY,
82
- oZ: object.localEditedPose.oZ,
83
- theta: object.localEditedPose.theta,
84
- }, { ...geometryObject });
77
+ this.updateFrame(object.name ?? '', this.referenceFrame(), createPose(object.localEditedPose), geometryObject);
85
78
  };
86
79
  setFrameParent = (parentName) => {
87
80
  const object = this.object();
88
81
  if (!object)
89
82
  return;
90
- this.updateFrame(object.name ?? '', parentName, {
91
- x: object.localEditedPose.x,
92
- y: object.localEditedPose.y,
93
- z: object.localEditedPose.z,
94
- oX: object.localEditedPose.oX,
95
- oY: object.localEditedPose.oY,
96
- oZ: object.localEditedPose.oZ,
97
- theta: object.localEditedPose.theta,
98
- });
83
+ this.updateFrame(object.name ?? '', parentName, createPose(object.localEditedPose));
99
84
  };
100
85
  deleteFrame = () => {
101
86
  const object = this.object();
@@ -108,37 +93,13 @@ export class DetailConfigUpdater {
108
93
  if (!object)
109
94
  return;
110
95
  if (type === 'none') {
111
- this.updateFrame(object.name ?? '', this.referenceFrame(), {
112
- x: object.localEditedPose.x,
113
- y: object.localEditedPose.y,
114
- z: object.localEditedPose.z,
115
- oX: object.localEditedPose.oX,
116
- oY: object.localEditedPose.oY,
117
- oZ: object.localEditedPose.oZ,
118
- theta: object.localEditedPose.theta,
119
- }, { type: 'none' });
96
+ this.updateFrame(object.name ?? '', this.referenceFrame(), createPose(object.localEditedPose), { type: 'none' });
120
97
  }
121
98
  else if (type === 'box') {
122
- this.updateFrame(object.name ?? '', this.referenceFrame(), {
123
- x: object.localEditedPose.x,
124
- y: object.localEditedPose.y,
125
- z: object.localEditedPose.z,
126
- oX: object.localEditedPose.oX,
127
- oY: object.localEditedPose.oY,
128
- oZ: object.localEditedPose.oZ,
129
- theta: object.localEditedPose.theta,
130
- }, { type: 'box', x: 100, y: 100, z: 100 });
99
+ this.updateFrame(object.name ?? '', this.referenceFrame(), createPose(object.localEditedPose), { type: 'box', x: 100, y: 100, z: 100 });
131
100
  }
132
101
  else if (type === 'sphere') {
133
- this.updateFrame(object.name ?? '', this.referenceFrame(), {
134
- x: object.localEditedPose.x,
135
- y: object.localEditedPose.y,
136
- z: object.localEditedPose.z,
137
- oX: object.localEditedPose.oX,
138
- oY: object.localEditedPose.oY,
139
- oZ: object.localEditedPose.oZ,
140
- theta: object.localEditedPose.theta,
141
- }, { type: 'sphere', r: 100 });
102
+ this.updateFrame(object.name ?? '', this.referenceFrame(), createPose(object.localEditedPose), { type: 'sphere', r: 100 });
142
103
  }
143
104
  else if (type === 'capsule') {
144
105
  this.updateFrame(object.name ?? '', this.referenceFrame(), {
@@ -65,7 +65,6 @@
65
65
  <T
66
66
  is={line}
67
67
  {...rest}
68
- raycast={() => null}
69
68
  bvh={{ enabled: false }}
70
69
  >
71
70
  <T is={geometry} />
@@ -25,7 +25,7 @@
25
25
  import WeblabActive from './weblab/WeblabActive.svelte'
26
26
  import { useFrames } from '../hooks/useFrames.svelte'
27
27
  import { usePartConfig } from '../hooks/usePartConfig.svelte'
28
- import { DetailConfigUpdater } from '../Detail.svelte'
28
+ import { FrameConfigUpdater } from '../FrameConfigUpdater.svelte'
29
29
  import { useWeblabs } from '../hooks/useWeblabs.svelte'
30
30
 
31
31
  const { ...rest } = $props()
@@ -59,7 +59,7 @@
59
59
 
60
60
  const draggable = useDraggable('details')
61
61
 
62
- const detailConfigUpdater: DetailConfigUpdater = new DetailConfigUpdater(
62
+ const detailConfigUpdater = new FrameConfigUpdater(
63
63
  () => object,
64
64
  partConfig.updateFrame,
65
65
  partConfig.deleteFrame,
@@ -65,18 +65,38 @@
65
65
  const oncreate = (ref: BufferGeometry) => {
66
66
  geo = ref
67
67
  }
68
+
69
+ const parsePlyInput = (mesh: string | Uint8Array): BufferGeometry => {
70
+ // Case 1: already a base64 or ASCII string
71
+ if (typeof mesh === 'string') {
72
+ return plyLoader.parse(atob(mesh))
73
+ }
74
+
75
+ // Case 2: detect text vs binary PLY in Uint8Array
76
+ const header = new TextDecoder().decode(mesh.slice(0, 50))
77
+ const isAscii = header.includes('format ascii')
78
+
79
+ // Case 3: text-mode PLY → decode bytes to string
80
+ if (isAscii) {
81
+ const text = new TextDecoder().decode(mesh)
82
+ return plyLoader.parse(text)
83
+ }
84
+
85
+ // Case 4: binary PLY → pass ArrayBuffer directly
86
+ return plyLoader.parse(mesh.buffer as ArrayBuffer)
87
+ }
68
88
  </script>
69
89
 
70
90
  <T
71
91
  is={group}
72
92
  {...rest}
73
93
  >
74
- <AxesHelper
75
- width={3}
76
- length={0.1}
77
- />
78
-
79
94
  {#if geometry?.geometryType}
95
+ <AxesHelper
96
+ width={3}
97
+ length={0.1}
98
+ />
99
+
80
100
  <T
81
101
  is={mesh}
82
102
  {name}
@@ -84,8 +104,8 @@
84
104
  bvh={{ enabled: false }}
85
105
  >
86
106
  {#if geometry.geometryType.case === 'mesh'}
87
- {@const mesh = geometry.geometryType.value.mesh as Uint8Array<ArrayBuffer>}
88
- {@const meshGeometry = plyLoader.parse(typeof mesh === 'string' ? atob(mesh) : mesh.buffer)}
107
+ {@const mesh = geometry.geometryType.value.mesh}
108
+ {@const meshGeometry = parsePlyInput(mesh)}
89
109
  <T
90
110
  is={meshGeometry}
91
111
  {oncreate}
@@ -111,7 +131,7 @@
111
131
  args={[radiusMm * 0.001, lengthMm * 0.001]}
112
132
  {oncreate}
113
133
  />
114
- {:else}{/if}
134
+ {/if}
115
135
 
116
136
  {#if geometry.geometryType.case === 'line'}
117
137
  <MeshLineMaterial
@@ -137,6 +157,13 @@
137
157
  {/if}
138
158
  {/if}
139
159
  </T>
160
+ {:else}
161
+ <AxesHelper
162
+ {name}
163
+ {uuid}
164
+ width={3}
165
+ length={0.1}
166
+ />
140
167
  {/if}
141
168
 
142
169
  {@render children?.({ ref: group })}
@@ -74,6 +74,21 @@
74
74
  </Portal>
75
75
  {/each}
76
76
 
77
+ {#each drawAPI.frames as object (object.uuid)}
78
+ <Portal id={object.referenceFrame}>
79
+ <Frame
80
+ uuid={object.uuid}
81
+ name={object.name}
82
+ pose={object.pose}
83
+ geometry={object.geometry}
84
+ metadata={object.metadata}
85
+ >
86
+ <PortalTarget id={object.name} />
87
+ <Label text={object.name} />
88
+ </Frame>
89
+ </Portal>
90
+ {/each}
91
+
77
92
  {#each drawAPI.points as object (object.uuid)}
78
93
  <Portal id={object.referenceFrame}>
79
94
  <Pointcloud {object}>
@@ -0,0 +1,76 @@
1
+ import type { ValueOf } from 'type-fest';
2
+ type FrameGeometryMap = {
3
+ none: {
4
+ type: 'none';
5
+ };
6
+ box: {
7
+ type: 'box';
8
+ x: number;
9
+ y: number;
10
+ z: number;
11
+ };
12
+ sphere: {
13
+ type: 'sphere';
14
+ r: number;
15
+ };
16
+ capsule: {
17
+ type: 'capsule';
18
+ r: number;
19
+ l: number;
20
+ };
21
+ };
22
+ export type FrameGeometry = keyof FrameGeometryMap;
23
+ export type FrameGeometries = ValueOf<FrameGeometryMap>;
24
+ type FrameOrientationMap = {
25
+ quaternion: {
26
+ type: 'quaternion';
27
+ value: {
28
+ x: number;
29
+ y: number;
30
+ z: number;
31
+ w: number;
32
+ };
33
+ };
34
+ euler_angles: {
35
+ type: 'euler_angles';
36
+ value: {
37
+ roll: number;
38
+ pitch: number;
39
+ yaw: number;
40
+ };
41
+ };
42
+ ov_degrees: {
43
+ type: 'ov_degrees';
44
+ value: {
45
+ x: number;
46
+ y: number;
47
+ z: number;
48
+ th: number;
49
+ };
50
+ };
51
+ ov_radians: {
52
+ type: 'ov_radians';
53
+ value: {
54
+ x: number;
55
+ y: number;
56
+ z: number;
57
+ th: number;
58
+ };
59
+ };
60
+ };
61
+ export type FrameOrientation = keyof FrameOrientationMap;
62
+ export type FrameOrientations = ValueOf<FrameOrientationMap>;
63
+ export interface Frame<T extends FrameGeometry = FrameGeometry, K extends FrameOrientation = FrameOrientation> {
64
+ id?: string;
65
+ name?: string;
66
+ parent: string;
67
+ translation: {
68
+ x: number;
69
+ y: number;
70
+ z: number;
71
+ };
72
+ orientation: FrameOrientationMap[K];
73
+ geometry?: FrameGeometryMap[T];
74
+ }
75
+ export declare const createFrame: <T extends FrameGeometry = "box", K extends FrameOrientation = "ov_degrees">(geometry?: FrameGeometryMap[T]) => Frame<T>;
76
+ export {};
package/dist/frame.js ADDED
@@ -0,0 +1,12 @@
1
+ // TODO: replace with types exported from the sdk when created
2
+ export const createFrame = (geometry) => {
3
+ return {
4
+ parent: 'world',
5
+ translation: { x: 0, y: 0, z: 0 },
6
+ orientation: {
7
+ type: 'ov_degrees',
8
+ value: { x: 0, y: 0, z: 1, th: 0 },
9
+ },
10
+ geometry: (geometry ?? { type: 'box', x: 100, y: 100, z: 100 }),
11
+ };
12
+ };
@@ -0,0 +1,2 @@
1
+ import type { Geometry } from '@viamrobotics/sdk';
2
+ export declare const createGeometry: (geometryType?: Geometry["geometryType"], label?: string) => Geometry;
@@ -0,0 +1,8 @@
1
+ import { createPose } from './transform';
2
+ export const createGeometry = (geometryType, label = '') => {
3
+ return {
4
+ center: createPose(),
5
+ label,
6
+ geometryType: geometryType ?? { case: undefined, value: undefined },
7
+ };
8
+ };
@@ -4,6 +4,7 @@ type ConnectionStatus = 'connecting' | 'open' | 'closed';
4
4
  interface Context {
5
5
  addPoints(worldObject: WorldObject<PointsGeometry>): void;
6
6
  points: WorldObject<PointsGeometry>[];
7
+ frames: WorldObject[];
7
8
  lines: WorldObject[];
8
9
  meshes: WorldObject[];
9
10
  poses: WorldObject[];
@@ -5,6 +5,8 @@ import { parsePcdInWorker } from '../loaders/pcd';
5
5
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
6
6
  import { WorldObject } from '../WorldObject.svelte';
7
7
  import { useArrows } from './useArrows.svelte';
8
+ import { createGeometry } from '../geometry';
9
+ import { createPoseFromFrame } from '../transform';
8
10
  const key = Symbol('draw-api-context-key');
9
11
  const tryParse = (json) => {
10
12
  try {
@@ -15,6 +17,21 @@ const tryParse = (json) => {
15
17
  return;
16
18
  }
17
19
  };
20
+ /**
21
+ * @TODO get golang scripts to return protobufs so that we
22
+ * can use our types. Right now we're just marshalling JSON,
23
+ * leading to upper case var names and no type contract with the golang lib.
24
+ */
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ const lowercaseKeys = (obj) => {
27
+ if (Array.isArray(obj)) {
28
+ return obj.map(lowercaseKeys);
29
+ }
30
+ else if (obj && typeof obj === 'object' && obj.constructor === Object) {
31
+ return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k.toLowerCase(), lowercaseKeys(v)]));
32
+ }
33
+ return obj;
34
+ };
18
35
  class Float32Reader {
19
36
  littleEndian = true;
20
37
  offset = 0;
@@ -38,6 +55,7 @@ export const provideDrawAPI = () => {
38
55
  let reconnectDelay = 200;
39
56
  const maxReconnectDelay = 5_000;
40
57
  let ws;
58
+ const frames = $state([]);
41
59
  const points = $state([]);
42
60
  const lines = $state([]);
43
61
  const meshes = $state([]);
@@ -51,6 +69,32 @@ export const provideDrawAPI = () => {
51
69
  const origin = new Vector3();
52
70
  const loader = new GLTFLoader();
53
71
  const batchedArrow = useArrows();
72
+ const drawFrames = async (data) => {
73
+ for (const frame of data) {
74
+ const name = frame.name || frame.id || '';
75
+ const pose = createPoseFromFrame(lowercaseKeys(frame));
76
+ const geometry = createGeometry();
77
+ if (frame.geometry?.type === 'box') {
78
+ geometry.label = `${name} geometry (box)`;
79
+ geometry.geometryType.case = 'box';
80
+ geometry.geometryType.value = {
81
+ dimsMm: { x: frame.geometry.x, y: frame.geometry.y, z: frame.geometry.z },
82
+ };
83
+ }
84
+ else if (frame.geometry?.type === 'sphere') {
85
+ geometry.label = `${name} geometry (sphere)`;
86
+ geometry.geometryType.case = 'sphere';
87
+ geometry.geometryType.value = { radiusMm: frame.geometry.r };
88
+ }
89
+ else if (frame.geometry?.type === 'capsule') {
90
+ geometry.label = `${name} geometry (capsule)`;
91
+ geometry.geometryType.case = 'capsule';
92
+ geometry.geometryType.value = { lengthMm: frame.geometry.l, radiusMm: frame.geometry.r };
93
+ }
94
+ const worldObject = new WorldObject(name, pose, frame.parent ?? 'world', frame.geometry ? geometry : undefined);
95
+ frames.push(worldObject);
96
+ }
97
+ };
54
98
  const drawPCD = async (buffer) => {
55
99
  const { positions, colors } = await parsePcdInWorker(new Uint8Array(buffer));
56
100
  points.push(new WorldObject(`points ${++pointsIndex}`, undefined, undefined, {
@@ -256,6 +300,11 @@ export const provideDrawAPI = () => {
256
300
  const remove = (names) => {
257
301
  let index = -1;
258
302
  for (const name of names) {
303
+ index = frames.findIndex((frame) => frame.name === name);
304
+ if (index !== -1) {
305
+ frames.slice(index, 1);
306
+ continue;
307
+ }
259
308
  index = points.findIndex((p) => p.name === name);
260
309
  if (index !== -1) {
261
310
  points.splice(index, 1);
@@ -293,6 +342,7 @@ export const provideDrawAPI = () => {
293
342
  }
294
343
  };
295
344
  const removeAll = () => {
345
+ frames.splice(0, frames.length);
296
346
  points.splice(0, points.length);
297
347
  lines.splice(0, lines.length);
298
348
  meshes.splice(0, meshes.length);
@@ -362,6 +412,9 @@ export const provideDrawAPI = () => {
362
412
  if ('geometry' in data) {
363
413
  return drawGeometry(data.geometry, data.color);
364
414
  }
415
+ if ('frames' in data) {
416
+ return drawFrames(data.frames);
417
+ }
365
418
  if ('Knots' in data) {
366
419
  return drawNurbs(data, data.Color);
367
420
  }
@@ -384,6 +437,9 @@ export const provideDrawAPI = () => {
384
437
  };
385
438
  connect();
386
439
  setContext(key, {
440
+ get frames() {
441
+ return frames;
442
+ },
387
443
  get points() {
388
444
  return points;
389
445
  },
@@ -54,15 +54,7 @@ export const provideFrames = (partID) => {
54
54
  return;
55
55
  }
56
56
  worldObject.referenceFrame = component.frame.parent;
57
- worldObject.localEditedPose = {
58
- x: component.frame.translation.x,
59
- y: component.frame.translation.y,
60
- z: component.frame.translation.z,
61
- oX: component.frame.orientation.value.x,
62
- oY: component.frame.orientation.value.y,
63
- oZ: component.frame.orientation.value.z,
64
- theta: component.frame.orientation.value.th,
65
- };
57
+ worldObject.localEditedPose = createPoseFromFrame(component.frame);
66
58
  if (component.frame.geometry) {
67
59
  switch (component.frame.geometry.type) {
68
60
  case 'box':
@@ -16,6 +16,7 @@ export const provideObjects = () => {
16
16
  ...geometries.current,
17
17
  ...points.current,
18
18
  ...statics.current,
19
+ ...drawAPI.frames,
19
20
  ...drawAPI.meshes,
20
21
  ...drawAPI.models,
21
22
  ...drawAPI.nurbs,
@@ -1,29 +1,6 @@
1
+ import { type Frame } from '../frame';
1
2
  import { Struct, Pose } from '@viamrobotics/sdk';
2
3
  import type { ViamClient } from '@viamrobotics/sdk';
3
- export interface Frame {
4
- parent: string;
5
- translation: {
6
- x: number;
7
- y: number;
8
- z: number;
9
- };
10
- orientation: {
11
- value: {
12
- x: number;
13
- y: number;
14
- z: number;
15
- th: number;
16
- };
17
- };
18
- geometry?: {
19
- type: 'none' | 'box' | 'sphere' | 'capsule';
20
- x?: number;
21
- y?: number;
22
- z?: number;
23
- r?: number;
24
- l?: number;
25
- };
26
- }
27
4
  export interface PartConfig {
28
5
  components: {
29
6
  name: string;
@@ -39,14 +16,7 @@ interface PartConfigParams {
39
16
  standalonePartConfigProps?: StandalonePartConfigProps;
40
17
  }
41
18
  interface PartConfigContext {
42
- updateFrame: (componentName: string, referenceFrame: string, pose: Pose, geometry?: {
43
- type: 'none' | 'box' | 'sphere' | 'capsule';
44
- r?: number;
45
- l?: number;
46
- x?: number;
47
- y?: number;
48
- z?: number;
49
- }) => void;
19
+ updateFrame: (componentName: string, referenceFrame: string, pose: Pose, geometry?: Frame['geometry']) => void;
50
20
  saveLocalPartConfig: () => void;
51
21
  resetLocalPartConfig: () => void;
52
22
  deleteFrame: (componentName: string) => void;
@@ -1,4 +1,5 @@
1
- import { createNewFrame } from '../transform';
1
+ import { createFrame } from '../frame';
2
+ import { createPoseFromFrame } from '../transform';
2
3
  import { Struct, Pose } from '@viamrobotics/sdk';
3
4
  import { getContext, setContext } from 'svelte';
4
5
  const key = Symbol('part-config-context');
@@ -14,15 +15,6 @@ export const providePartConfig = (params) => {
14
15
  else {
15
16
  throw new Error('No part config provided');
16
17
  }
17
- const createFrame = (componentName) => {
18
- const fragmentId = _localPartConfig.componentNameToFragmentId()[componentName];
19
- if (fragmentId !== undefined) {
20
- createFragmentFrame(fragmentId, componentName);
21
- }
22
- else {
23
- createPartFrame(componentName);
24
- }
25
- };
26
18
  const createFragmentFrame = (fragmentId, componentName) => {
27
19
  const newConfig = _localPartConfig.getLocalPartConfig().toJson();
28
20
  if (newConfig.fragment_mods === undefined) {
@@ -39,7 +31,7 @@ export const providePartConfig = (params) => {
39
31
  const modSetPath = `components.${componentName}.frame`;
40
32
  const frame = {
41
33
  ['$set']: {
42
- [modSetPath]: createNewFrame(),
34
+ [modSetPath]: createFrame(),
43
35
  },
44
36
  };
45
37
  fragmentMod.mods.push(frame);
@@ -50,20 +42,11 @@ export const providePartConfig = (params) => {
50
42
  const newConfig = _localPartConfig.getLocalPartConfig().toJson();
51
43
  const component = newConfig?.components?.find((comp) => comp.name === componentName);
52
44
  if (component) {
53
- component.frame = createNewFrame();
45
+ component.frame = createFrame();
54
46
  }
55
47
  const configStruct = Struct.fromJson(newConfig);
56
48
  _localPartConfig.setLocalPartConfig(configStruct);
57
49
  };
58
- const updateFrame = (componentName, referenceFrame, framePosition, frameGeometry) => {
59
- const fragmentId = _localPartConfig.componentNameToFragmentId()[componentName];
60
- if (fragmentId !== undefined) {
61
- updateFragmentFrame(fragmentId, componentName, referenceFrame, framePosition, frameGeometry);
62
- }
63
- else {
64
- updatePartFrame(componentName, referenceFrame, framePosition, frameGeometry);
65
- }
66
- };
67
50
  const updateFragmentFrame = (fragmentId, componentName, referenceFrame, framePosition, frameGeometry) => {
68
51
  const newConfig = _localPartConfig.getLocalPartConfig().toJson();
69
52
  if (newConfig.fragment_mods === undefined) {
@@ -124,18 +107,19 @@ export const providePartConfig = (params) => {
124
107
  if (!component) {
125
108
  return;
126
109
  }
127
- if (component && component.frame) {
110
+ if (component.frame) {
111
+ const currentPose = createPoseFromFrame(component.frame);
128
112
  component.frame.parent = referenceFrame;
129
113
  component.frame.translation = {
130
- x: pose.x === undefined ? component.frame.translation.x : pose.x,
131
- y: pose.y === undefined ? component.frame.translation.y : pose.y,
132
- z: pose.z === undefined ? component.frame.translation.z : pose.z,
114
+ x: pose.x ?? currentPose.x,
115
+ y: pose.y ?? currentPose.y,
116
+ z: pose.z ?? currentPose.z,
133
117
  };
134
118
  component.frame.orientation.value = {
135
- x: pose.oX === undefined ? component.frame.orientation.value.x : pose.oX,
136
- y: pose.oY === undefined ? component.frame.orientation.value.y : pose.oY,
137
- z: pose.oZ === undefined ? component.frame.orientation.value.z : pose.oZ,
138
- th: pose.theta === undefined ? component.frame.orientation.value.th : pose.theta,
119
+ x: pose.oX ?? currentPose.oX,
120
+ y: pose.oY ?? currentPose.oY,
121
+ z: pose.oZ ?? currentPose.oZ,
122
+ th: pose.theta ?? currentPose.theta,
139
123
  };
140
124
  if (geometry) {
141
125
  if (geometry.type === 'none') {
@@ -149,15 +133,6 @@ export const providePartConfig = (params) => {
149
133
  const configStruct = Struct.fromJson(newConfig);
150
134
  _localPartConfig.setLocalPartConfig(configStruct);
151
135
  };
152
- const deleteFrame = (componentName) => {
153
- const fragmentId = _localPartConfig.componentNameToFragmentId()[componentName];
154
- if (fragmentId !== undefined) {
155
- deleteFragmentFrame(fragmentId, componentName);
156
- }
157
- else {
158
- deletePartFrame(componentName);
159
- }
160
- };
161
136
  const deletePartFrame = (componentName) => {
162
137
  const newConfig = _localPartConfig.getLocalPartConfig().toJson();
163
138
  const component = newConfig?.components?.find((comp) => comp.name === componentName);
@@ -191,18 +166,40 @@ export const providePartConfig = (params) => {
191
166
  const configStruct = Struct.fromJson(newConfig);
192
167
  _localPartConfig.setLocalPartConfig(configStruct);
193
168
  };
194
- const saveLocalPartConfig = () => {
195
- _localPartConfig.saveLocalPartConfig?.();
196
- };
197
- const resetLocalPartConfig = () => {
198
- _localPartConfig.resetLocalPartConfig?.();
199
- };
200
169
  setContext(key, {
201
- updateFrame,
202
- deleteFrame,
203
- createFrame,
204
- saveLocalPartConfig,
205
- resetLocalPartConfig,
170
+ updateFrame: (componentName, referenceFrame, framePosition, frameGeometry) => {
171
+ const fragmentId = _localPartConfig.componentNameToFragmentId()[componentName];
172
+ if (fragmentId !== undefined) {
173
+ updateFragmentFrame(fragmentId, componentName, referenceFrame, framePosition, frameGeometry);
174
+ }
175
+ else {
176
+ updatePartFrame(componentName, referenceFrame, framePosition, frameGeometry);
177
+ }
178
+ },
179
+ deleteFrame: (componentName) => {
180
+ const fragmentId = _localPartConfig.componentNameToFragmentId()[componentName];
181
+ if (fragmentId !== undefined) {
182
+ deleteFragmentFrame(fragmentId, componentName);
183
+ }
184
+ else {
185
+ deletePartFrame(componentName);
186
+ }
187
+ },
188
+ createFrame: (componentName) => {
189
+ const fragmentId = _localPartConfig.componentNameToFragmentId()[componentName];
190
+ if (fragmentId !== undefined) {
191
+ createFragmentFrame(fragmentId, componentName);
192
+ }
193
+ else {
194
+ createPartFrame(componentName);
195
+ }
196
+ },
197
+ saveLocalPartConfig: () => {
198
+ _localPartConfig.saveLocalPartConfig?.();
199
+ },
200
+ resetLocalPartConfig: () => {
201
+ _localPartConfig.resetLocalPartConfig?.();
202
+ },
206
203
  get localPartConfig() {
207
204
  return _localPartConfig.getLocalPartConfig();
208
205
  },
@@ -1,7 +1,7 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  import { get, set } from 'idb-keyval';
3
3
  import { Debounced } from 'runed';
4
- import { createGeometry } from '../transform';
4
+ import { createGeometry } from '../geometry';
5
5
  import { WorldObject } from '../WorldObject.svelte';
6
6
  const key = Symbol('static-geometries-context');
7
7
  export const provideStaticGeometries = () => {
@@ -19,7 +19,11 @@ const addCookie = (name, value, days, path = '/') => {
19
19
  document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}${expires}; path=${path}`;
20
20
  };
21
21
  const getCookieExperiments = () => {
22
- return getCookie('weblab_experiments')?.split(',') ?? [];
22
+ const cookie = getCookie('weblab_experiments');
23
+ if (!cookie) {
24
+ return [];
25
+ }
26
+ return decodeURIComponent(cookie).split(',');
23
27
  };
24
28
  export const createWeblabs = () => {
25
29
  const activeExperiments = new SvelteSet();
@@ -41,7 +45,8 @@ export const createWeblabs = () => {
41
45
  export const provideWeblabs = () => {
42
46
  const urlExperiment = new URLSearchParams(window.location.search).get('experiment');
43
47
  if (urlExperiment) {
44
- addCookie('weblab_experiments', [...getCookieExperiments(), urlExperiment].join(','));
48
+ const experimentSet = new Set([...getCookieExperiments(), urlExperiment]);
49
+ addCookie('weblab_experiments', Array.from(experimentSet).join(','));
45
50
  }
46
51
  setContext(WEBLABS_CONTEXT_KEY, createWeblabs());
47
52
  };
@@ -67,7 +67,7 @@ export declare class OrientationVector {
67
67
  /**
68
68
  * Copies value of ov to this orientation vector.
69
69
  */
70
- copy(ov: OrientationVector): this;
70
+ copy(ov: OrientationVectorLike): this;
71
71
  equals(orientationVector: OrientationVectorLike): boolean;
72
72
  fromArray(array: number[], offset?: number): this;
73
73
  toArray(array?: number[], offset?: number): number[];
@@ -1,8 +1,8 @@
1
1
  import type { Geometry, Pose } from '@viamrobotics/sdk';
2
2
  import { type Object3D, Matrix4, Quaternion, Vector3 } from 'three';
3
- import type { Frame } from './hooks/usePartConfig.svelte';
3
+ import type { Frame } from './frame';
4
4
  export declare const createPose: (pose?: Pose) => Pose;
5
- export declare const createGeometry: (geometryType?: Geometry["geometryType"], label?: string) => Geometry;
5
+ export declare const createPoseFromFrame: (frame: Frame) => Pose;
6
6
  export declare const quaternionToPose: (quaternion: Quaternion, pose: Partial<Pose>) => void;
7
7
  export declare const vector3ToPose: (vec3: Vector3, pose: Partial<Pose>) => void;
8
8
  export declare const object3dToPose: (object3d: Object3D, pose: Partial<Pose>) => Partial<import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Pose>>;
@@ -11,7 +11,5 @@ export declare const poseToVector3: (pose: Partial<Pose>, vec3: Vector3) => void
11
11
  export declare const poseToObject3d: (pose: Partial<Pose>, object3d: Object3D) => void;
12
12
  export declare const poseToDirection: (pose: Pose) => Vector3;
13
13
  export declare const scaleToDimensions: (scale: Vector3, geometry: Geometry["geometryType"]) => void;
14
- export declare const createPoseFromFrame: (frame: Frame) => Pose;
15
- export declare const createNewFrame: () => Frame;
16
14
  export declare const poseToMatrix: (pose: Pose) => Matrix4;
17
15
  export declare const matrixToPose: (matrix: Matrix4) => import("@viamrobotics/sdk").PlainMessage<import("@viamrobotics/sdk/dist/gen/common/v1/common_pb").Pose>;
package/dist/transform.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { OrientationVector } from './three/OrientationVector';
2
- import { MathUtils, Matrix4, Quaternion, Vector3 } from 'three';
2
+ import { Euler, MathUtils, Matrix4, Quaternion, Vector3 } from 'three';
3
+ const quaternion = new Quaternion();
4
+ const euler = new Euler();
3
5
  const ov = new OrientationVector();
4
6
  export const createPose = (pose) => {
5
7
  return {
@@ -12,11 +14,31 @@ export const createPose = (pose) => {
12
14
  theta: pose?.theta ?? 0,
13
15
  };
14
16
  };
15
- export const createGeometry = (geometryType, label = '') => {
17
+ export const createPoseFromFrame = (frame) => {
18
+ if (frame.orientation.type === 'quaternion') {
19
+ quaternion.copy(frame.orientation.value);
20
+ ov.setFromQuaternion(quaternion);
21
+ }
22
+ else if (frame.orientation.type === 'euler_angles') {
23
+ euler.set(frame.orientation.value.roll, frame.orientation.value.pitch, frame.orientation.value.yaw, 'ZYX');
24
+ quaternion.setFromEuler(euler);
25
+ ov.setFromQuaternion(quaternion);
26
+ }
27
+ else if (frame.orientation.type === 'ov_radians') {
28
+ ov.copy(frame.orientation.value);
29
+ }
30
+ else if (frame.orientation.type === 'ov_degrees') {
31
+ const th = MathUtils.degToRad(frame.orientation.value.th);
32
+ ov.set(frame.orientation.value.x, frame.orientation.value.y, frame.orientation.value.z, th);
33
+ }
16
34
  return {
17
- center: createPose(),
18
- label,
19
- geometryType: geometryType ?? { case: undefined, value: undefined },
35
+ x: frame.translation.x ?? 0,
36
+ y: frame.translation.y ?? 0,
37
+ z: frame.translation.z ?? 0,
38
+ oX: ov.x,
39
+ oY: ov.y,
40
+ oZ: ov.z,
41
+ theta: MathUtils.radToDeg(ov.th),
20
42
  };
21
43
  };
22
44
  export const quaternionToPose = (quaternion, pose) => {
@@ -49,7 +71,7 @@ export const poseToObject3d = (pose, object3d) => {
49
71
  poseToQuaternion(pose, object3d.quaternion);
50
72
  };
51
73
  export const poseToDirection = (pose) => {
52
- const ov = new OrientationVector(pose.oX, pose.oY, pose.oZ, pose.theta);
74
+ ov.set(pose.oX, pose.oY, pose.oZ, MathUtils.degToRad(pose.theta));
53
75
  return new Vector3(ov.x, ov.y, ov.z);
54
76
  };
55
77
  export const scaleToDimensions = (scale, geometry) => {
@@ -67,41 +89,6 @@ export const scaleToDimensions = (scale, geometry) => {
67
89
  geometry.value.radiusMm *= scale.x;
68
90
  }
69
91
  };
70
- export const createPoseFromFrame = (frame) => {
71
- return {
72
- x: frame.translation.x,
73
- y: frame.translation.y,
74
- z: frame.translation.z,
75
- oX: frame.orientation.value.x,
76
- oY: frame.orientation.value.y,
77
- oZ: frame.orientation.value.z,
78
- theta: frame.orientation.value.th,
79
- };
80
- };
81
- export const createNewFrame = () => {
82
- return {
83
- parent: 'world',
84
- translation: {
85
- x: 0,
86
- y: 0,
87
- z: 0,
88
- },
89
- orientation: {
90
- value: {
91
- x: 0,
92
- y: 0,
93
- z: 1,
94
- th: 0,
95
- },
96
- },
97
- geometry: {
98
- type: 'box',
99
- x: 100,
100
- y: 100,
101
- z: 100,
102
- },
103
- };
104
- };
105
92
  export const poseToMatrix = (pose) => {
106
93
  const matrix = new Matrix4();
107
94
  const poseQuaternion = new Quaternion().setFromAxisAngle(new Vector3(pose.oX, pose.oY, pose.oZ), pose.theta * (Math.PI / 180));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "0.14.3",
3
+ "version": "0.14.6",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -90,6 +90,9 @@
90
90
  "svelte": ">=5",
91
91
  "svelte-virtuallists": ">=1"
92
92
  },
93
+ "engines": {
94
+ "node": ">=22.0.0"
95
+ },
93
96
  "svelte": "./dist/index.js",
94
97
  "types": "./dist/index.d.ts",
95
98
  "exports": {