@viamrobotics/motion-tools 0.14.2 → 0.14.5

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 (39) hide show
  1. package/dist/{Detail.svelte.d.ts → FrameConfigUpdater.svelte.d.ts} +4 -17
  2. package/dist/{Detail.svelte.js → FrameConfigUpdater.svelte.js} +10 -49
  3. package/dist/components/App.svelte +38 -40
  4. package/dist/components/AxesHelper.svelte +0 -1
  5. package/dist/components/Details.svelte +49 -5
  6. package/dist/components/Geometry.svelte +35 -8
  7. package/dist/components/LiveUpdatesBanner.svelte +1 -1
  8. package/dist/components/Tree/AddFrames.svelte +1 -1
  9. package/dist/components/Tree/Drawer.svelte +4 -2
  10. package/dist/components/Tree/Drawer.svelte.d.ts +1 -0
  11. package/dist/components/Tree/Logs.svelte +53 -4
  12. package/dist/components/Tree/TreeContainer.svelte +5 -2
  13. package/dist/components/WorldObjects.svelte +15 -0
  14. package/dist/components/weblab/WeblabActive.svelte +3 -3
  15. package/dist/frame.d.ts +76 -0
  16. package/dist/frame.js +12 -0
  17. package/dist/geometry.d.ts +2 -0
  18. package/dist/geometry.js +8 -0
  19. package/dist/hooks/useDrawAPI.svelte.d.ts +1 -0
  20. package/dist/hooks/useDrawAPI.svelte.js +56 -0
  21. package/dist/hooks/useFrames.svelte.js +3 -11
  22. package/dist/hooks/useGeometries.svelte.d.ts +1 -0
  23. package/dist/hooks/useGeometries.svelte.js +12 -2
  24. package/dist/hooks/useLogs.svelte.d.ts +3 -0
  25. package/dist/hooks/useLogs.svelte.js +40 -9
  26. package/dist/hooks/useObjects.svelte.js +1 -0
  27. package/dist/hooks/usePartConfig.svelte.d.ts +2 -32
  28. package/dist/hooks/usePartConfig.svelte.js +63 -69
  29. package/dist/hooks/usePointclouds.svelte.d.ts +1 -0
  30. package/dist/hooks/usePointclouds.svelte.js +5 -0
  31. package/dist/hooks/useStaticGeometries.svelte.js +1 -1
  32. package/dist/hooks/useWeblabs.svelte.d.ts +4 -7
  33. package/dist/hooks/useWeblabs.svelte.js +43 -18
  34. package/dist/three/OrientationVector.d.ts +1 -1
  35. package/dist/transform.d.ts +2 -4
  36. package/dist/transform.js +28 -41
  37. package/package.json +2 -2
  38. package/dist/components/weblab/WeblabProvider.svelte +0 -8
  39. package/dist/components/weblab/WeblabProvider.svelte.d.ts +0 -5
@@ -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(), {
@@ -4,7 +4,6 @@
4
4
  import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools'
5
5
  import { provideToast, ToastContainer } from '@viamrobotics/prime-core'
6
6
  import type { Struct } from '@viamrobotics/sdk'
7
-
8
7
  import Scene from './Scene.svelte'
9
8
  import TreeContainer from './Tree/TreeContainer.svelte'
10
9
  import Details from './Details.svelte'
@@ -16,7 +15,7 @@
16
15
  import { domPortal } from '../portal'
17
16
  import { provideSettings } from '../hooks/useSettings.svelte'
18
17
  import FileDrop from './FileDrop.svelte'
19
- import WeblabProvider from './weblab/WeblabProvider.svelte'
18
+ import { provideWeblabs } from '../hooks/useWeblabs.svelte'
20
19
  import { providePartConfig } from '../hooks/usePartConfig.svelte'
21
20
  import { useViamClient } from '@viamrobotics/svelte-sdk'
22
21
  import LiveUpdatesBanner from './LiveUpdatesBanner.svelte'
@@ -54,6 +53,7 @@
54
53
 
55
54
  createPartIDContext(() => partID)
56
55
 
56
+ provideWeblabs()
57
57
  provideToast()
58
58
 
59
59
  let root = $state.raw<HTMLElement>()
@@ -83,41 +83,39 @@
83
83
  <SvelteQueryDevtools initialIsOpen />
84
84
  {/if}
85
85
 
86
- <WeblabProvider>
87
- <div
88
- class="relative h-full w-full overflow-hidden"
89
- bind:this={root}
90
- >
91
- <Canvas renderMode="always">
92
- <World>
93
- <SceneProviders>
94
- {#snippet children({ focus })}
95
- <Scene>
96
- {@render appChildren?.()}
97
- </Scene>
98
-
99
- <XR {@attach domPortal(root)} />
100
-
101
- <Dashboard {@attach domPortal(root)} />
102
- <Details {@attach domPortal(root)} />
103
- {#if environment.current.isStandalone}
104
- <LiveUpdatesBanner {@attach domPortal(root)} />
105
- {/if}
106
-
107
- {#if !focus}
108
- <TreeContainer {@attach domPortal(root)} />
109
- {/if}
110
-
111
- {#if !focus && settings.current.enableArmPositionsWidget}
112
- <ArmPositions {@attach domPortal(root)} />
113
- {/if}
114
-
115
- <FileDrop {@attach domPortal(root)} />
116
- {/snippet}
117
- </SceneProviders>
118
- </World>
119
- </Canvas>
120
-
121
- <ToastContainer />
122
- </div>
123
- </WeblabProvider>
86
+ <div
87
+ class="relative h-full w-full overflow-hidden"
88
+ bind:this={root}
89
+ >
90
+ <Canvas renderMode="always">
91
+ <World>
92
+ <SceneProviders>
93
+ {#snippet children({ focus })}
94
+ <Scene>
95
+ {@render appChildren?.()}
96
+ </Scene>
97
+
98
+ <XR {@attach domPortal(root)} />
99
+
100
+ <Dashboard {@attach domPortal(root)} />
101
+ <Details {@attach domPortal(root)} />
102
+ {#if environment.current.isStandalone}
103
+ <LiveUpdatesBanner {@attach domPortal(root)} />
104
+ {/if}
105
+
106
+ {#if !focus}
107
+ <TreeContainer {@attach domPortal(root)} />
108
+ {/if}
109
+
110
+ {#if !focus && settings.current.enableArmPositionsWidget}
111
+ <ArmPositions {@attach domPortal(root)} />
112
+ {/if}
113
+
114
+ <FileDrop {@attach domPortal(root)} />
115
+ {/snippet}
116
+ </SceneProviders>
117
+ </World>
118
+ </Canvas>
119
+
120
+ <ToastContainer />
121
+ </div>
@@ -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,8 @@
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
+ import { useWeblabs } from '../hooks/useWeblabs.svelte'
29
30
 
30
31
  const { ...rest } = $props()
31
32
 
@@ -36,6 +37,7 @@
36
37
  const partConfig = usePartConfig()
37
38
  const selectedObject = useSelectedObject()
38
39
  const selectedObject3d = useSelectedObject3d()
40
+ const weblab = useWeblabs()
39
41
 
40
42
  const object = $derived(focusedObject.current ?? selectedObject.current)
41
43
  const object3d = $derived(focusedObject3d.current ?? selectedObject3d.current)
@@ -57,7 +59,7 @@
57
59
 
58
60
  const draggable = useDraggable('details')
59
61
 
60
- const detailConfigUpdater: DetailConfigUpdater = new DetailConfigUpdater(
62
+ const detailConfigUpdater = new FrameConfigUpdater(
61
63
  () => object,
62
64
  partConfig.updateFrame,
63
65
  partConfig.deleteFrame,
@@ -99,6 +101,48 @@
99
101
  stop()
100
102
  }
101
103
  })
104
+
105
+ const getCopyClipboardText = () => {
106
+ if (weblab.isActive('MOTION_TOOLS_EDIT_FRAME')) {
107
+ return JSON.stringify(
108
+ {
109
+ worldPosition: worldPosition,
110
+ worldOrientation: worldOrientation,
111
+ localPosition: {
112
+ x: localPose?.x,
113
+ y: localPose?.y,
114
+ z: localPose?.z,
115
+ },
116
+ localOrientation: {
117
+ x: localPose?.oX,
118
+ y: localPose?.oY,
119
+ z: localPose?.oZ,
120
+ th: localPose?.theta,
121
+ },
122
+ geometry: {
123
+ type: geometryType,
124
+ value: object?.geometry?.geometryType.value,
125
+ },
126
+ parentFrame: referenceFrame,
127
+ },
128
+ null,
129
+ 2
130
+ )
131
+ } else {
132
+ return JSON.stringify(
133
+ {
134
+ worldPosition: worldPosition,
135
+ worldOrientation: worldOrientation,
136
+ geometry: {
137
+ type: geometryType,
138
+ value: object?.geometry?.geometryType.value,
139
+ },
140
+ },
141
+ null,
142
+ 2
143
+ )
144
+ }
145
+ }
102
146
  </script>
103
147
 
104
148
  {#snippet ImmutableField({
@@ -199,7 +243,7 @@
199
243
 
200
244
  <button
201
245
  onclick={async () => {
202
- navigator.clipboard.writeText(JSON.stringify($state.snapshot(object)))
246
+ navigator.clipboard.writeText(getCopyClipboardText())
203
247
  copied = true
204
248
  setTimeout(() => (copied = false), 1000)
205
249
  }}
@@ -430,7 +474,7 @@
430
474
  <div class="flex items-center gap-2">
431
475
  {@render GeometryAttribute({
432
476
  label: 'r',
433
- ariaLabel: 'sphere dimensions radius value input',
477
+ ariaLabel: 'sphere dimensions radius value',
434
478
  value: radiusMm ? radiusMm.toFixed(2) : '-',
435
479
  onInput: (value) =>
436
480
  detailConfigUpdater.updateGeometry({ type: 'sphere', r: parseFloat(value) }),
@@ -523,7 +567,7 @@
523
567
  <Button
524
568
  variant="danger"
525
569
  class="mt-2 w-full"
526
- onclick={() => detailConfigUpdater.deleteFrame()}>Delete Frame</Button
570
+ onclick={() => detailConfigUpdater.deleteFrame()}>Delete frame</Button
527
571
  >
528
572
  {/if}
529
573
  </WeblabActive>
@@ -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 })}
@@ -15,7 +15,7 @@
15
15
  class="flex flex-col items-start rounded border-l-4 border-yellow-600 bg-yellow-50 px-4 py-2"
16
16
  >
17
17
  <p>
18
- <strong>Live Updates Paused</strong>
18
+ <strong>Live updates paused</strong>
19
19
  </p>
20
20
 
21
21
  <p class="text-sm">
@@ -9,7 +9,7 @@
9
9
  </script>
10
10
 
11
11
  <Drawer name="Add frames">
12
- <div class="flex h-64 w-60 flex-col gap-2 overflow-auto p-3">
12
+ <div class="flex max-h-64 flex-col gap-2 overflow-auto p-3">
13
13
  {#if framelessComponents.current.length > 0}
14
14
  <ul class="space-y-1">
15
15
  {#each framelessComponents.current as component (component)}
@@ -7,9 +7,10 @@
7
7
  name: string
8
8
  defaultOpen?: boolean
9
9
  children: Snippet
10
+ titleAlert?: Snippet
10
11
  }
11
12
 
12
- let { name, children, defaultOpen = false }: Props = $props()
13
+ let { name, children, titleAlert, defaultOpen = false }: Props = $props()
13
14
 
14
15
  const expanded = $derived(new PersistedState(`${name}-expanded`, defaultOpen))
15
16
  </script>
@@ -24,9 +25,10 @@
24
25
  label="unfold more icon"
25
26
  variant="ghost"
26
27
  cx="size-6"
27
- on:click={() => (expanded.current = !expanded.current)}
28
+ onclick={() => (expanded.current = !expanded.current)}
28
29
  />
29
30
  {name}
31
+ {@render titleAlert?.()}
30
32
  </h3>
31
33
  </button>
32
34
 
@@ -3,6 +3,7 @@ interface Props {
3
3
  name: string;
4
4
  defaultOpen?: boolean;
5
5
  children: Snippet;
6
+ titleAlert?: Snippet;
6
7
  }
7
8
  declare const Drawer: import("svelte").Component<Props, {}, "">;
8
9
  type Drawer = ReturnType<typeof Drawer>;
@@ -1,14 +1,56 @@
1
1
  <script lang="ts">
2
+ import { useFrames } from '../../hooks/useFrames.svelte'
3
+ import { useGeometries } from '../../hooks/useGeometries.svelte'
2
4
  import { useLogs } from '../../hooks/useLogs.svelte'
5
+ import { usePointClouds } from '../../hooks/usePointclouds.svelte'
3
6
  import Drawer from './Drawer.svelte'
4
7
 
8
+ const frames = useFrames()
9
+ const geometries = useGeometries()
10
+ const pointclouds = usePointClouds()
5
11
  const logs = useLogs()
6
- const truncated = $derived(logs.current.slice(0, 100).reverse())
12
+
13
+ $effect(() => {
14
+ if (frames.error) {
15
+ const message = `Frames: ${frames.error.message}`
16
+ logs.add(message, 'error')
17
+ }
18
+ })
19
+
20
+ $effect(() => {
21
+ if (geometries.errors.length > 0) {
22
+ for (const error of geometries.errors) {
23
+ logs.add(`Geometries: ${error.message}`, 'error')
24
+ }
25
+ }
26
+ })
27
+
28
+ $effect(() => {
29
+ if (pointclouds.errors.length > 0) {
30
+ for (const error of pointclouds.errors) {
31
+ logs.add(`Pointclouds: ${error.message}`, 'error')
32
+ }
33
+ }
34
+ })
7
35
  </script>
8
36
 
9
37
  <Drawer name="Logs">
10
- <div class="flex h-64 w-60 flex-col gap-2 overflow-auto p-3">
11
- {#each truncated as log (log.uuid)}
38
+ {#snippet titleAlert()}
39
+ {#if logs.warnings.length > 0}
40
+ <span class="mr-1 rounded bg-yellow-700 px-1 py-0.5 text-xs text-white">
41
+ {logs.warnings.length}
42
+ </span>
43
+ {/if}
44
+
45
+ {#if logs.errors.length > 0}
46
+ <span class="mr-1 rounded bg-red-700 px-1 py-0.5 text-xs text-white">
47
+ {logs.errors.length}
48
+ </span>
49
+ {/if}
50
+ {/snippet}
51
+
52
+ <div class="flex h-64 flex-col gap-2 overflow-auto p-3">
53
+ {#each logs.current as log (log.uuid)}
12
54
  <div>
13
55
  <div class="flex flex-wrap items-center gap-1.5">
14
56
  <div
@@ -23,7 +65,14 @@
23
65
  ></div>
24
66
  <div class="text-subtle-2">{log.timestamp}</div>
25
67
  </div>
26
- <div>{log.message}</div>
68
+ <div>
69
+ {#if log.count > 1}
70
+ <span class="mr-1 rounded bg-green-700 px-1 py-0.5 text-xs text-white">
71
+ {log.count}
72
+ </span>
73
+ {/if}
74
+ {log.message}
75
+ </div>
27
76
  </div>
28
77
  {:else}
29
78
  No logs
@@ -13,10 +13,12 @@
13
13
  import Widgets from './Widgets.svelte'
14
14
  import AddFrames from './AddFrames.svelte'
15
15
  import { useEnvironment } from '../../hooks/useEnvironment.svelte'
16
+ import { usePartID } from '../../hooks/usePartID.svelte'
16
17
  const { ...rest } = $props()
17
18
 
18
19
  provideTreeExpandedContext()
19
20
 
21
+ const partID = usePartID()
20
22
  const selected = useSelected()
21
23
  const objects = useObjects()
22
24
  const draggable = useDraggable('treeview')
@@ -40,7 +42,7 @@
40
42
  </script>
41
43
 
42
44
  <div
43
- class="bg-extralight border-medium absolute top-0 left-0 z-1000 m-2 overflow-y-auto border text-xs"
45
+ class="bg-extralight border-medium absolute top-0 left-0 z-1000 m-2 w-60 overflow-y-auto border text-xs"
44
46
  style:transform="translate({draggable.current.x}px, {draggable.current.y}px)"
45
47
  {...rest}
46
48
  >
@@ -56,9 +58,10 @@
56
58
  />
57
59
  {/key}
58
60
 
59
- {#if environment.current.isStandalone}
61
+ {#if environment.current.isStandalone && partID.current}
60
62
  <AddFrames />
61
63
  {/if}
64
+
62
65
  <Logs />
63
66
  <Settings />
64
67
  <Widgets />
@@ -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}>
@@ -9,13 +9,13 @@
9
9
  }
10
10
  let { experiment, children, renderIfActive = true }: Props = $props()
11
11
 
12
- const { weblab } = useWeblabs()
12
+ const weblabs = useWeblabs()
13
13
 
14
14
  $effect.pre(() => {
15
- weblab.load([experiment])
15
+ weblabs.load([experiment])
16
16
  })
17
17
  </script>
18
18
 
19
- {#if weblab.isActive(experiment) === renderIfActive}
19
+ {#if weblabs.isActive(experiment) === renderIfActive}
20
20
  {@render children()}
21
21
  {/if}