@viamrobotics/motion-tools 1.24.0 → 1.25.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.
@@ -0,0 +1,137 @@
1
+ <!--
2
+ @component
3
+
4
+ A compound capsule via a shared open-ended unit cylinder and two
5
+ hemispheres, each scaled per `r` and `l`, so dimension changes update
6
+ transforms only and cause no geometry rebuild.
7
+
8
+ Viam's capsule `l` is the *total* length, including the rounded caps, so the
9
+ midsection has length `l - 2r`.
10
+ -->
11
+ <script
12
+ module
13
+ lang="ts"
14
+ >
15
+ import { CylinderGeometry, EdgesGeometry, SphereGeometry } from 'three'
16
+
17
+ const unitCylinder = new CylinderGeometry(1, 1, 1, 16, 1, true)
18
+ unitCylinder.rotateX(Math.PI / 2)
19
+
20
+ /**
21
+ * Hemisphere with rounded part toward +Z and an open boundary on the XY plane.
22
+ * 6 height segments matches the existing sphere's density (`Mesh.svelte` uses `SphereGeometry(1, 16, 12)`).
23
+ */
24
+ const unitHemisphere = new SphereGeometry(1, 16, 6, 0, Math.PI * 2, 0, Math.PI / 2)
25
+ unitHemisphere.rotateX(Math.PI / 2)
26
+
27
+ const unitCylinderEdges = new EdgesGeometry(unitCylinder, 0)
28
+ const unitHemisphereEdges = new EdgesGeometry(unitHemisphere, 0)
29
+ </script>
30
+
31
+ <script lang="ts">
32
+ import type { ColorRepresentation } from 'three'
33
+
34
+ import { T, useThrelte } from '@threlte/core'
35
+ import { LineBasicMaterial, MeshToonMaterial } from 'three'
36
+
37
+ import { darkenColor } from '../../color'
38
+
39
+ interface Props {
40
+ r: number
41
+ l: number
42
+ color: ColorRepresentation
43
+ opacity?: number
44
+ depthTest?: boolean
45
+ }
46
+
47
+ let { r, l, color, opacity = 1, depthTest = true }: Props = $props()
48
+
49
+ const { invalidate } = useThrelte()
50
+ const material = new MeshToonMaterial()
51
+ const lineMaterial = new LineBasicMaterial()
52
+
53
+ $effect(() => {
54
+ material.color.set(color)
55
+ lineMaterial.color.set(darkenColor(color, 10))
56
+ invalidate()
57
+ })
58
+
59
+ $effect(() => {
60
+ const isTransparent = opacity < 1
61
+ material.opacity = opacity
62
+ material.depthWrite = opacity === 1
63
+ material.depthTest = depthTest
64
+ lineMaterial.depthTest = depthTest
65
+ if (material.transparent !== isTransparent) {
66
+ material.transparent = isTransparent
67
+ material.needsUpdate = true
68
+ }
69
+ invalidate()
70
+ })
71
+
72
+ const midsection = $derived(Math.max(0, l - 2 * r))
73
+ const halfMid = $derived(midsection / 2)
74
+ </script>
75
+
76
+ {#if midsection > 0}
77
+ <T.Mesh scale={[r, r, midsection]}>
78
+ <T
79
+ is={unitCylinder}
80
+ dispose={false}
81
+ />
82
+ <T is={material} />
83
+ <T.LineSegments
84
+ raycast={() => null}
85
+ bvh={{ enabled: false }}
86
+ >
87
+ <T
88
+ is={unitCylinderEdges}
89
+ dispose={false}
90
+ />
91
+ <T is={lineMaterial} />
92
+ </T.LineSegments>
93
+ </T.Mesh>
94
+ {/if}
95
+
96
+ <T.Mesh
97
+ position={[0, 0, halfMid]}
98
+ scale={r}
99
+ >
100
+ <T
101
+ is={unitHemisphere}
102
+ dispose={false}
103
+ />
104
+ <T is={material} />
105
+ <T.LineSegments
106
+ raycast={() => null}
107
+ bvh={{ enabled: false }}
108
+ >
109
+ <T
110
+ is={unitHemisphereEdges}
111
+ dispose={false}
112
+ />
113
+ <T is={lineMaterial} />
114
+ </T.LineSegments>
115
+ </T.Mesh>
116
+
117
+ <T.Mesh
118
+ position={[0, 0, -halfMid]}
119
+ rotation={[Math.PI, 0, 0]}
120
+ scale={[r, r, r]}
121
+ >
122
+ <T
123
+ is={unitHemisphere}
124
+ dispose={false}
125
+ />
126
+ <T is={material} />
127
+ <T.LineSegments
128
+ raycast={() => null}
129
+ bvh={{ enabled: false }}
130
+ >
131
+ <T
132
+ is={unitHemisphereEdges}
133
+ dispose={false}
134
+ />
135
+ <T is={lineMaterial} />
136
+ </T.LineSegments>
137
+ </T.Mesh>
@@ -0,0 +1,19 @@
1
+ import type { ColorRepresentation } from 'three';
2
+ interface Props {
3
+ r: number;
4
+ l: number;
5
+ color: ColorRepresentation;
6
+ opacity?: number;
7
+ depthTest?: boolean;
8
+ }
9
+ /**
10
+ * A compound capsule via a shared open-ended unit cylinder and two
11
+ * hemispheres, each scaled per `r` and `l`, so dimension changes update
12
+ * transforms only and cause no geometry rebuild.
13
+ *
14
+ * Viam's capsule `l` is the *total* length, including the rounded caps, so the
15
+ * midsection has length `l - 2r`.
16
+ */
17
+ declare const Capsule: import("svelte").Component<Props, {}, "">;
18
+ type Capsule = ReturnType<typeof Capsule>;
19
+ export default Capsule;
@@ -1,20 +1,36 @@
1
+ <script
2
+ module
3
+ lang="ts"
4
+ >
5
+ import { BoxGeometry, EdgesGeometry, SphereGeometry } from 'three'
6
+
7
+ /**
8
+ * Shared unit geometries — every mesh references these and sets
9
+ * dimensions through `mesh.scale`, so resizing never rebuilds GPU buffers.
10
+ */
11
+ const unitBox = new BoxGeometry(1, 1, 1)
12
+ const unitSphere = new SphereGeometry(1, 16, 12)
13
+ const unitBoxEdges = new EdgesGeometry(unitBox, 0)
14
+ const unitSphereEdges = new EdgesGeometry(unitSphere, 0)
15
+ </script>
16
+
1
17
  <script lang="ts">
2
18
  import type { Pose } from '@viamrobotics/sdk'
3
19
  import type { Entity } from 'koota'
4
20
 
5
21
  import { T, type Props as ThrelteProps, useThrelte } from '@threlte/core'
6
22
  import { type Snippet } from 'svelte'
7
- import { BufferGeometry, Color, DoubleSide, FrontSide, Material, Mesh } from 'three'
23
+ import { Color, DoubleSide, FrontSide, Group, Material, Mesh } from 'three'
8
24
 
9
25
  import { asColor } from '../../buffer'
10
26
  import { colors, darkenColor } from '../../color'
11
27
  import { traits, useTrait } from '../../ecs'
12
- import { CapsuleGeometry } from '../../three/CapsuleGeometry'
13
28
  import { poseToObject3d } from '../../transform'
14
29
 
15
30
  import AxesHelper from '../AxesHelper.svelte'
31
+ import Capsule from './Capsule.svelte'
16
32
 
17
- interface Props extends ThrelteProps<Mesh> {
33
+ interface Props extends Omit<ThrelteProps<Mesh>, 'ref'> {
18
34
  entity: Entity
19
35
  color?: string
20
36
  center?: Pose
@@ -56,6 +72,8 @@
56
72
 
57
73
  const currentOpacity = $derived(opacity.current ?? 0.7)
58
74
 
75
+ const isCapsule = $derived(capsule.current !== undefined)
76
+
59
77
  let material = $state.raw<Material>(new Material())
60
78
  $effect(() => {
61
79
  const isTransparent = currentOpacity < 1
@@ -69,83 +87,118 @@
69
87
  })
70
88
 
71
89
  const mesh = new Mesh()
72
- $effect.pre(() => {
90
+ const group = new Group()
91
+
92
+ $effect(() => {
93
+ const target = isCapsule ? group : mesh
73
94
  if (center) {
74
- poseToObject3d(center, mesh)
95
+ poseToObject3d(center, target)
75
96
  invalidate()
76
97
  }
77
98
  })
78
99
 
79
- let geo = $state.raw<BufferGeometry>()
80
- $effect.pre(() => {
81
- if (!box.current && !sphere.current && !capsule.current && !bufferGeometry.current) {
82
- geo = undefined
100
+ $effect(() => {
101
+ if (box.current) {
102
+ const { x, y, z } = box.current
103
+ mesh.scale.set(x * 0.001, y * 0.001, z * 0.001)
104
+ } else if (sphere.current) {
105
+ mesh.scale.setScalar((sphere.current.r ?? 0) * 0.001)
106
+ } else {
107
+ mesh.scale.set(1, 1, 1)
83
108
  }
109
+ invalidate()
84
110
  })
85
-
86
- const oncreate = (bufferGeometry: BufferGeometry) => {
87
- geo = bufferGeometry
88
- }
89
111
  </script>
90
112
 
91
- <T
92
- is={mesh}
93
- name={entity}
94
- userData.name={name}
95
- renderOrder={renderOrder.current}
96
- {...rest}
97
- >
98
- {#if box.current}
99
- {@const { x, y, z } = box.current ?? { x: 0, y: 0, z: 0 }}
100
- <T.BoxGeometry
101
- args={[x * 0.001, y * 0.001, z * 0.001]}
102
- {oncreate}
103
- />
104
- {:else if sphere.current}
105
- {@const { r } = sphere.current ?? { r: 0 }}
106
- <T.SphereGeometry
107
- args={[r * 0.001]}
108
- {oncreate}
109
- />
110
- {:else if capsule.current}
111
- {@const { r, l } = capsule.current ?? { r: 0, l: 0 }}
112
- <T
113
- is={CapsuleGeometry}
114
- args={[r * 0.001, l * 0.001]}
115
- {oncreate}
113
+ {#if isCapsule}
114
+ {@const { r, l } = capsule.current ?? { r: 0, l: 0 }}
115
+ <T
116
+ is={group}
117
+ name={entity}
118
+ userData.name={name}
119
+ renderOrder={renderOrder.current}
120
+ {...rest}
121
+ >
122
+ <Capsule
123
+ r={r * 0.001}
124
+ l={l * 0.001}
125
+ {color}
126
+ opacity={currentOpacity}
127
+ depthTest={materialProps.current?.depthTest ?? true}
116
128
  />
117
- {:else if bufferGeometry.current}
118
- <T
119
- is={bufferGeometry.current}
120
- {oncreate}
129
+
130
+ {@render children?.()}
131
+ </T>
132
+ {:else}
133
+ <T
134
+ is={mesh}
135
+ name={entity}
136
+ userData.name={name}
137
+ renderOrder={renderOrder.current}
138
+ {...rest}
139
+ >
140
+ {#if box.current}
141
+ <T
142
+ is={unitBox}
143
+ dispose={false}
144
+ />
145
+ <T.LineSegments
146
+ raycast={() => null}
147
+ bvh={{ enabled: false }}
148
+ >
149
+ <T
150
+ is={unitBoxEdges}
151
+ dispose={false}
152
+ />
153
+ <T.LineBasicMaterial color={darkenColor(color, 10)} />
154
+ </T.LineSegments>
155
+ {:else if sphere.current}
156
+ <T
157
+ is={unitSphere}
158
+ dispose={false}
159
+ />
160
+ <T.LineSegments
161
+ raycast={() => null}
162
+ bvh={{ enabled: false }}
163
+ >
164
+ <T
165
+ is={unitSphereEdges}
166
+ dispose={false}
167
+ />
168
+ <T.LineBasicMaterial color={darkenColor(color, 10)} />
169
+ </T.LineSegments>
170
+ {:else if bufferGeometry.current}
171
+ <T is={bufferGeometry.current}>
172
+ {#snippet children({ ref: geo })}
173
+ <!--
174
+ TODO(mp) currently some bufferGeometries are coming in empty,
175
+ this is a quick fix but this should be handled upstream
176
+ -->
177
+ {#if geo.getAttribute('position').array.length > 0}
178
+ <T.LineSegments
179
+ raycast={() => null}
180
+ bvh={{ enabled: false }}
181
+ >
182
+ <T.EdgesGeometry args={[geo, 0]} />
183
+ <T.LineBasicMaterial color={darkenColor(color, 10)} />
184
+ </T.LineSegments>
185
+ {/if}
186
+ {/snippet}
187
+ </T>
188
+ {/if}
189
+
190
+ <T.MeshToonMaterial
191
+ {color}
192
+ side={bufferGeometry.current ? DoubleSide : FrontSide}
193
+ depthTest={materialProps.current?.depthTest ?? true}
194
+ oncreate={(m) => {
195
+ material = m
196
+ }}
121
197
  />
122
- {/if}
123
-
124
- <T.MeshToonMaterial
125
- {color}
126
- side={bufferGeometry.current ? DoubleSide : FrontSide}
127
- depthTest={materialProps.current?.depthTest ?? true}
128
- oncreate={(m) => {
129
- material = m
130
- }}
131
- />
132
198
 
133
- <!--
134
- TODO(mp) currently some bufferGeometries are coming in empty,
135
- this is a quick fix but this should be handled upstream
136
- -->
137
- {#if geo && geo.getAttribute('position').array.length > 0}
138
- <T.LineSegments
139
- raycast={() => null}
140
- bvh={{ enabled: false }}
141
- >
142
- <T.EdgesGeometry args={[geo, 0]} />
143
- <T.LineBasicMaterial color={darkenColor(color, 10)} />
144
- </T.LineSegments>
145
- {/if}
146
-
147
- {@render children?.()}
148
- </T>
199
+ {@render children?.()}
200
+ </T>
201
+ {/if}
149
202
 
150
203
  {#if showAxesHelper.current}
151
204
  <AxesHelper
@@ -9,14 +9,18 @@ export const provideArmKinematics = (partID) => {
9
9
  const names = $derived(arms.current.map((arm) => arm.name));
10
10
  const clients = $derived(arms.current.map((arm) => createResourceClient(ArmClient, partID, () => arm.name)));
11
11
  const kinematicsQueries = $derived(clients.map((client) => [client.current?.name, createResourceQuery(client, 'getKinematics', () => options)]));
12
- const kinematics = $derived(Object.fromEntries(kinematicsQueries.map(([name, query]) => [
13
- name,
14
- query.data?.joints.map((j) => ({
15
- id: j.id,
16
- min: j.min,
17
- max: j.max,
18
- })),
19
- ])));
12
+ const kinematics = $derived(Object.fromEntries(kinematicsQueries.map(([name, query]) => {
13
+ const data = query.data;
14
+ const joints = data && ('joints' in data ? data.joints : data.kinematicsData.joints);
15
+ return [
16
+ name,
17
+ joints?.map((j) => ({
18
+ id: j.id,
19
+ min: j.min,
20
+ max: j.max,
21
+ })),
22
+ ];
23
+ })));
20
24
  setContext(key, {
21
25
  get names() {
22
26
  return names;
@@ -1,6 +1,5 @@
1
- import { observe } from '@threlte/core';
2
- import { commonApi, Pose } from '@viamrobotics/sdk';
3
- import { createRobotQuery, useRobotClient } from '@viamrobotics/svelte-sdk';
1
+ import { commonApi, MachineConnectionEvent, Pose } from '@viamrobotics/sdk';
2
+ import { createRobotQuery, useConnectionStatus, useRobotClient } from '@viamrobotics/svelte-sdk';
4
3
  import { untrack } from 'svelte';
5
4
  import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
6
5
  import { useEnvironment } from './useEnvironment.svelte';
@@ -12,10 +11,11 @@ import { useResourceByName } from './useResourceByName.svelte';
12
11
  import { RefreshRates, useSettings } from './useSettings.svelte';
13
12
  const originFrameComponentTypes = new Set(['arm', 'gantry', 'gripper', 'base']);
14
13
  export const usePose = (name, parent) => {
14
+ const partID = usePartID();
15
+ const connectionStatus = useConnectionStatus(() => partID.current);
15
16
  const environment = useEnvironment();
16
17
  const logs = useLogs();
17
18
  const settings = useSettings();
18
- const partID = usePartID();
19
19
  const robotClient = useRobotClient(() => partID.current);
20
20
  const currentName = $derived(name());
21
21
  const currentParent = $derived(parent());
@@ -49,8 +49,10 @@ export const usePose = (name, parent) => {
49
49
  logs.add(`Error fetching pose for ${currentName}: ${query.error.message}`, 'error');
50
50
  }
51
51
  });
52
- observe.pre(() => [environment.current.viewerMode, frames.current], () => {
53
- if (environment.current.viewerMode === 'monitor') {
52
+ $effect(() => {
53
+ if (environment.current.viewerMode === 'monitor' &&
54
+ frames.current &&
55
+ connectionStatus.current === MachineConnectionEvent.CONNECTED) {
54
56
  untrack(() => query.refetch());
55
57
  }
56
58
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -37,8 +37,8 @@
37
37
  "@typescript-eslint/eslint-plugin": "8.56.1",
38
38
  "@typescript-eslint/parser": "8.56.1",
39
39
  "@viamrobotics/prime-core": "0.1.5",
40
- "@viamrobotics/sdk": "0.58.0",
41
- "@viamrobotics/svelte-sdk": "1.0.1",
40
+ "@viamrobotics/sdk": "0.69.0",
41
+ "@viamrobotics/svelte-sdk": "1.2.2",
42
42
  "@vitest/browser": "3.2.4",
43
43
  "@vitest/coverage-v8": "^3.2.4",
44
44
  "@zag-js/collapsible": "1.22.1",