@viamrobotics/motion-tools 1.11.1 → 1.12.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.
Files changed (35) hide show
  1. package/dist/components/App.svelte +3 -0
  2. package/dist/components/CameraControls.svelte +1 -1
  3. package/dist/components/Geometry.svelte +13 -12
  4. package/dist/components/Lasso/Debug.svelte +72 -0
  5. package/dist/components/Lasso/Debug.svelte.d.ts +8 -0
  6. package/dist/components/Lasso/Lasso.svelte +238 -92
  7. package/dist/components/Lasso/Tool.svelte +94 -0
  8. package/dist/components/Lasso/Tool.svelte.d.ts +9 -0
  9. package/dist/components/Lasso/traits.d.ts +21 -0
  10. package/dist/components/Lasso/traits.js +16 -0
  11. package/dist/components/LineGeometry.svelte +20 -0
  12. package/dist/components/{Lasso/Line.svelte.d.ts → LineGeometry.svelte.d.ts} +4 -3
  13. package/dist/components/MeasureTool/MeasureTool.svelte +2 -2
  14. package/dist/components/PCD.svelte +34 -0
  15. package/dist/components/PCD.svelte.d.ts +6 -0
  16. package/dist/components/PointerMissBox.svelte +1 -1
  17. package/dist/components/Scene.svelte +7 -2
  18. package/dist/components/overlay/FloatingPanel.svelte +17 -10
  19. package/dist/components/overlay/FloatingPanel.svelte.d.ts +5 -0
  20. package/dist/ecs/traits.d.ts +8 -0
  21. package/dist/ecs/traits.js +8 -0
  22. package/dist/hooks/useControls.svelte.d.ts +2 -1
  23. package/dist/hooks/useControls.svelte.js +7 -2
  24. package/dist/hooks/usePartConfig.svelte.js +1 -1
  25. package/dist/hooks/useSettings.svelte.d.ts +1 -1
  26. package/dist/hooks/useSettings.svelte.js +1 -1
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +3 -0
  29. package/dist/pcd.d.ts +1 -0
  30. package/dist/pcd.js +44 -0
  31. package/dist/plugins/bvh.svelte.js +6 -1
  32. package/dist/test/createRandomPcdBinary.d.ts +1 -1
  33. package/dist/test/createRandomPcdBinary.js +14 -27
  34. package/package.json +1 -1
  35. package/dist/components/Lasso/Line.svelte +0 -44
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
3
  import { Canvas } from '@threlte/core'
4
+ import { PortalTarget } from '@threlte/extras'
4
5
  import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools'
5
6
  import { provideToast, ToastContainer } from '@viamrobotics/prime-core'
6
7
  import type { Struct } from '@viamrobotics/sdk'
@@ -155,6 +156,8 @@
155
156
  <Camera name={cameraName} />
156
157
  {/each}
157
158
  {/if}
159
+
160
+ <PortalTarget id="dom" />
158
161
  </div>
159
162
  {/snippet}
160
163
  </SceneProviders>
@@ -20,7 +20,7 @@
20
20
  icon="camera-outline"
21
21
  description="Reset camera"
22
22
  onclick={() => {
23
- cameraControls.current?.reset(true)
23
+ cameraControls.setInitialPose()
24
24
  }}
25
25
  />
26
26
  </fieldset>
@@ -3,7 +3,7 @@
3
3
  import { type Snippet } from 'svelte'
4
4
  import { meshBounds } from '@threlte/extras'
5
5
  import { BufferGeometry, Color, DoubleSide, FrontSide, Group, Mesh } from 'three'
6
- import { Line2, LineGeometry, LineMaterial } from 'three/examples/jsm/Addons.js'
6
+ import { Line2, LineMaterial } from 'three/examples/jsm/Addons.js'
7
7
  import { CapsuleGeometry } from '../three/CapsuleGeometry'
8
8
  import { colors, darkenColor } from '../color'
9
9
  import AxesHelper from './AxesHelper.svelte'
@@ -11,6 +11,7 @@
11
11
  import { traits, useTrait } from '../ecs'
12
12
  import { poseToObject3d } from '../transform'
13
13
  import type { Pose } from '@viamrobotics/sdk'
14
+ import LineGeometry from './LineGeometry.svelte'
14
15
 
15
16
  interface Props extends ThrelteProps<Group> {
16
17
  entity: Entity
@@ -47,6 +48,8 @@
47
48
  const lineWidth = useTrait(() => entity, traits.LineWidth)
48
49
  const center = useTrait(() => entity, traits.Center)
49
50
  const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
51
+ const materialProps = useTrait(() => entity, traits.Material)
52
+ const renderOrder = useTrait(() => entity, traits.RenderOrder)
50
53
 
51
54
  const geometryType = $derived.by(() => {
52
55
  if (box.current) return 'box'
@@ -136,6 +139,7 @@
136
139
  is={mesh}
137
140
  name={entity}
138
141
  userData.name={name}
142
+ renderOrder={renderOrder.current}
139
143
  >
140
144
  {#if model && renderMode.includes('model')}
141
145
  <T is={model} />
@@ -143,14 +147,7 @@
143
147
 
144
148
  {#if !model || renderMode.includes('colliders')}
145
149
  {#if linePositions.current}
146
- <T
147
- is={LineGeometry}
148
- oncreate={(ref) => {
149
- if (linePositions.current) {
150
- ref.setPositions(linePositions.current)
151
- }
152
- }}
153
- />
150
+ <LineGeometry positions={linePositions.current} />
154
151
  {:else if box.current}
155
152
  {@const { x, y, z } = box.current ?? { x: 0, y: 0, z: 0 }}
156
153
  <T.BoxGeometry
@@ -178,16 +175,20 @@
178
175
  is={LineMaterial}
179
176
  {color}
180
177
  width={lineWidth.current ? lineWidth.current * 0.001 : 0.5}
178
+ depthTest={materialProps.current?.depthTest}
181
179
  />
182
180
  {:else}
181
+ {@const currentOpacity = opacity.current ?? 0.7}
183
182
  <T.MeshToonMaterial
184
183
  {color}
185
184
  side={geometryType === 'buffer' ? DoubleSide : FrontSide}
186
- transparent={(opacity.current ?? 0.7) < 1}
187
- opacity={opacity.current ?? 0.7}
185
+ transparent={currentOpacity < 1}
186
+ depthWrite={currentOpacity === 1}
187
+ opacity={currentOpacity}
188
+ depthTest={materialProps.current?.depthTest}
188
189
  />
189
190
 
190
- {#if geo && renderMode.includes('colliders')}
191
+ {#if geo && (renderMode.includes('colliders') || !model)}
191
192
  <T.LineSegments
192
193
  raycast={() => null}
193
194
  bvh={{ enabled: false }}
@@ -0,0 +1,72 @@
1
+ <!--
2
+ @component
3
+
4
+ Shows all steps for querying points within a lasso selection
5
+ -->
6
+ <script lang="ts">
7
+ import { T } from '@threlte/core'
8
+ import { Box3, BufferAttribute, BufferGeometry, Vector3 } from 'three'
9
+ import { useTrait, traits } from '../../ecs'
10
+ import type { Entity } from 'koota'
11
+ import * as lassoTraits from './traits'
12
+
13
+ const box3 = new Box3()
14
+ const min = new Vector3()
15
+ const max = new Vector3()
16
+
17
+ interface Props {
18
+ lasso: Entity
19
+ }
20
+
21
+ let { lasso }: Props = $props()
22
+
23
+ const indices = useTrait(() => lasso, lassoTraits.Indices)
24
+ const positions = useTrait(() => lasso, traits.LinePositions)
25
+ const box = useTrait(() => lasso, lassoTraits.Box)
26
+ const boxes = useTrait(() => lasso, lassoTraits.Boxes)
27
+
28
+ const geometry = new BufferGeometry()
29
+
30
+ $effect(() => {
31
+ if (indices.current) {
32
+ geometry.setIndex(new BufferAttribute(indices.current, 1))
33
+ }
34
+
35
+ if (positions.current) {
36
+ geometry.setAttribute('position', new BufferAttribute(positions.current, 3))
37
+ }
38
+ })
39
+ </script>
40
+
41
+ {#if positions.current && indices.current}
42
+ <T.Mesh>
43
+ <T is={geometry} />
44
+ <T.MeshBasicMaterial
45
+ wireframe
46
+ color="green"
47
+ />
48
+ </T.Mesh>
49
+ {/if}
50
+
51
+ {#if boxes.current}
52
+ {#each boxes.current as box (box)}
53
+ <T.Box3Helper
54
+ args={[
55
+ new Box3().set(min.set(box.minX, box.minY, 0), max.set(box.maxX, box.maxY, 0)),
56
+ 'lightgreen',
57
+ ]}
58
+ />
59
+ {/each}
60
+ {/if}
61
+
62
+ {#if box.current}
63
+ <T.Box3Helper
64
+ args={[
65
+ box3.set(
66
+ min.set(box.current.minX, box.current.minY, 0),
67
+ max.set(box.current.maxX, box.current.maxY, 0)
68
+ ),
69
+ 'red',
70
+ ]}
71
+ />
72
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { Entity } from 'koota';
2
+ interface Props {
3
+ lasso: Entity;
4
+ }
5
+ /** Shows all steps for querying points within a lasso selection */
6
+ declare const Debug: import("svelte").Component<Props, {}, "">;
7
+ type Debug = ReturnType<typeof Debug>;
8
+ export default Debug;
@@ -1,153 +1,299 @@
1
1
  <script lang="ts">
2
- import { T } from '@threlte/core'
3
- import type { IntersectionEvent } from '@threlte/extras'
4
- import Line from './Line.svelte'
2
+ import { Raycaster, Box3, Vector3, Vector2, Plane, Triangle } from 'three'
3
+ import { useThrelte } from '@threlte/core'
4
+ import { Not } from 'koota'
5
5
  import { useCameraControls } from '../../hooks/useControls.svelte'
6
6
  import earcut from 'earcut'
7
- import { Box3, BufferAttribute, Vector3 } from 'three'
7
+ import { traits, useQuery, useWorld } from '../../ecs'
8
+ import type { ShapecastCallbacks } from 'three-mesh-bvh'
9
+ import { createBufferGeometry } from '../../attribute'
10
+ import * as lassoTraits from './traits'
11
+ import Debug from './Debug.svelte'
8
12
 
9
13
  interface Props {
10
14
  debug?: boolean
11
15
  }
12
16
 
13
- let { debug = true }: Props = $props()
17
+ let { debug = false }: Props = $props()
14
18
 
19
+ const world = useWorld()
15
20
  const controls = useCameraControls()
21
+ const { scene, dom, camera } = useThrelte()
16
22
 
17
23
  const box3 = new Box3()
24
+ const min = new Vector3()
25
+ const max = new Vector3()
26
+
27
+ const triangle = new Triangle()
28
+ const triangleBox = new Box3()
18
29
  const a = new Vector3()
19
30
  const b = new Vector3()
20
31
  const c = new Vector3()
21
32
 
33
+ let frameScheduled = false
22
34
  let drawing = false
23
35
 
24
- let position = $state<[number, number, number]>([0, 0, 0])
25
- let lassos = $state<
26
- {
27
- positions: number[]
28
- indices: Uint16Array
29
- boxes: Box3[]
30
- min: { x: number; y: number }
31
- max: { x: number; y: number }
32
- }[]
33
- >([])
34
-
35
- const onpointerdown = (event: IntersectionEvent<PointerEvent>) => {
36
- drawing = true
36
+ const raycaster = new Raycaster()
37
+ const mouse = new Vector2()
38
+ const plane = new Plane(new Vector3(0, 0, 1), 0)
39
+ const point = new Vector3()
37
40
 
38
- const { x, y } = event.point
41
+ const raycast = (event: PointerEvent) => {
42
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1
43
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
39
44
 
40
- lassos.push({
41
- positions: [x, y, 0],
42
- indices: new Uint16Array(),
43
- boxes: [],
44
- min: { x, y },
45
- max: { x, y },
46
- })
45
+ raycaster.setFromCamera(mouse, camera.current)
46
+ raycaster.ray.intersectPlane(plane, point)
47
+ return point
48
+ }
49
+
50
+ const onpointerdown = (event: PointerEvent) => {
51
+ if (!event.shiftKey) return
52
+
53
+ const { x, y } = raycast(event)
54
+
55
+ drawing = true
56
+
57
+ world.spawn(
58
+ traits.LinePositions(new Float32Array([x, y, 0])),
59
+ traits.LineWidth(1.5),
60
+ traits.RenderOrder(999),
61
+ traits.Material({ depthTest: false }),
62
+ traits.Color({ r: 1, g: 0, b: 0 }),
63
+ lassoTraits.Box({ minX: x, minY: y, maxX: x, maxY: y }),
64
+ lassoTraits.Lasso
65
+ )
47
66
 
48
67
  if (controls.current) {
49
68
  controls.current.enabled = false
50
69
  }
51
70
  }
52
71
 
53
- const onpointermove = (event: IntersectionEvent<PointerEvent>) => {
54
- event.point.toArray(position)
55
-
72
+ const onpointermove = (event: PointerEvent) => {
56
73
  if (!drawing) return
57
74
 
58
- let line = lassos.at(-1)
75
+ let lasso = world.query(lassoTraits.Lasso).at(-1)
76
+
77
+ if (!lasso) return
78
+
79
+ if (frameScheduled) return
80
+
81
+ frameScheduled = true
82
+
83
+ /**
84
+ * pointermove can execute at a rate much higher than screen
85
+ * refresh, creating huge polygon vertex counts, so we cap it.
86
+ */
87
+ requestAnimationFrame(() => {
88
+ frameScheduled = false
89
+
90
+ const { x, y } = raycast(event)
91
+ const positions = lasso.get(traits.LinePositions)
92
+ const box = lasso.get(lassoTraits.Box)
59
93
 
60
- if (!line) return
94
+ if (!positions || !box) return
61
95
 
62
- const { x, y } = event.point
63
- line.positions.push(x, y, 0)
96
+ const nextPositions = new Float32Array(positions.length + 3)
97
+ nextPositions.set(positions)
98
+ nextPositions[positions.length] = x
99
+ nextPositions[positions.length + 1] = y
100
+ lasso.set(traits.LinePositions, nextPositions)
64
101
 
65
- if (x < line.min.x) line.min.x = x
66
- else if (x > line.max.x) line.max.x = x
102
+ if (x < box.minX) box.minX = x
103
+ else if (x > box.maxX) box.maxX = x
67
104
 
68
- if (y < line.min.y) line.min.y = y
69
- else if (y > line.max.y) line.max.y = y
105
+ if (y < box.minY) box.minY = y
106
+ else if (y > box.maxY) box.maxY = y
107
+
108
+ lasso.set(lassoTraits.Box, box)
109
+ })
110
+ }
111
+
112
+ const onpointerleave = () => {
113
+ if (!drawing) return
114
+
115
+ onpointerup()
70
116
  }
71
117
 
72
118
  const onpointerup = () => {
119
+ if (!drawing) return
120
+
73
121
  drawing = false
74
122
 
75
- let lasso = lassos.at(-1)
123
+ let lasso = world.query(lassoTraits.Lasso).at(-1)
76
124
 
77
125
  if (!lasso) return
78
126
 
79
- const [x, y] = lasso.positions
127
+ let positions = lasso.get(traits.LinePositions)
128
+
129
+ if (!positions) return
130
+
131
+ const [startX, startY] = positions
80
132
 
81
133
  if (controls.current) {
82
134
  controls.current.enabled = true
83
135
  }
84
136
 
85
137
  // Close the loop
86
- lasso.positions.push(x, y, 0)
87
-
88
- const { positions } = lasso
138
+ const nextPositions = new Float32Array(positions.length + 3)
139
+ nextPositions.set(positions)
140
+ nextPositions[positions.length] = startX
141
+ nextPositions[positions.length + 1] = startY
142
+ lasso.set(traits.LinePositions, nextPositions)
143
+ positions = nextPositions
89
144
 
90
145
  const indices = earcut(positions, undefined, 3)
91
- lasso.indices = new Uint16Array(indices)
146
+ if (debug) {
147
+ lasso.add(lassoTraits.Indices(new Uint16Array(indices)))
148
+ }
92
149
 
93
- for (let i = 0; i < indices.length; i += 6) {
150
+ const getTriangleFromIndex = (i: number, triangle: Triangle) => {
94
151
  const stride = 3
95
152
  const ia = indices[i + 0] * stride
96
153
  const ib = indices[i + 1] * stride
97
154
  const ic = indices[i + 2] * stride
98
-
99
155
  a.set(positions[ia + 0], positions[ia + 1], positions[ia + 2])
100
156
  b.set(positions[ib + 0], positions[ib + 1], positions[ib + 2])
101
157
  c.set(positions[ic + 0], positions[ic + 1], positions[ic + 2])
102
- box3.setFromPoints([a, b, c])
158
+ triangle.set(a, b, c)
159
+ }
160
+
161
+ const boxes: lassoTraits.AABB[] = []
162
+ for (let i = 0, l = indices.length; i < l; i += 3) {
163
+ getTriangleFromIndex(i, triangle)
164
+ box3.setFromPoints([triangle.a, triangle.b, triangle.c])
165
+ boxes.push({ minX: box3.min.x, minY: box3.min.y, maxX: box3.max.x, maxY: box3.max.y })
166
+ }
167
+ if (debug) {
168
+ lasso.add(lassoTraits.Boxes(boxes))
169
+ }
170
+
171
+ const lassoBox = lasso.get(lassoTraits.Box)
172
+
173
+ if (!lassoBox) return
174
+
175
+ min.set(lassoBox.minX, lassoBox.minY, Number.NEGATIVE_INFINITY)
176
+ max.set(lassoBox.maxX, lassoBox.maxY, Number.POSITIVE_INFINITY)
177
+ box3.set(min, max)
178
+
179
+ const enclosedPoints: number[] = []
180
+
181
+ for (const pointsEntity of world.query(traits.Points, Not(lassoTraits.LassoEnclosedPoints))) {
182
+ const geometry = pointsEntity.get(traits.BufferGeometry)
183
+
184
+ if (!geometry) return
103
185
 
104
- lasso.boxes.push(box3.clone())
186
+ const points = scene.getObjectByName(pointsEntity as unknown as string)
187
+
188
+ if (!points) {
189
+ return
190
+ }
191
+
192
+ geometry.boundsTree?.shapecast({
193
+ intersectsBounds: (box) => {
194
+ return box.intersectsBox(box3)
195
+ },
196
+
197
+ intersectsPoint: (point: Vector3) => {
198
+ for (let i = 0, j = 0, l = indices.length; i < l; i += 3, j += 1) {
199
+ const { minX, minY, maxX, maxY } = boxes[j]
200
+
201
+ min.set(minX, minY, Number.NEGATIVE_INFINITY)
202
+ max.set(maxX, maxY, Number.POSITIVE_INFINITY)
203
+ triangleBox.set(min, max)
204
+
205
+ if (triangleBox.containsPoint(point)) {
206
+ getTriangleFromIndex(i, triangle)
207
+
208
+ if (triangle.containsPoint(point)) {
209
+ enclosedPoints.push(point.x, point.y, point.z)
210
+ }
211
+ }
212
+ }
213
+ },
214
+ // intersectsPoint is not yet in typedef, this can be removed when it is added
215
+ } as ShapecastCallbacks)
105
216
  }
217
+
218
+ const lassoResultGeometry = createBufferGeometry(new Float32Array(enclosedPoints))
219
+
220
+ world.spawn(
221
+ traits.Name('Lasso result'),
222
+ traits.BufferGeometry(lassoResultGeometry),
223
+ traits.Color({ r: 1, g: 0, b: 0 }),
224
+ traits.RenderOrder(999),
225
+ traits.Material({ depthTest: false }),
226
+ traits.Points,
227
+ traits.Removable,
228
+ lassoTraits.LassoEnclosedPoints,
229
+ lassoTraits.PointsCapturedBy(lasso)
230
+ )
106
231
  }
232
+
233
+ const onkeydown = (event: KeyboardEvent) => {
234
+ if (event.key === 'Shift') {
235
+ dom.style.cursor = 'crosshair'
236
+ }
237
+ }
238
+
239
+ const onkeyup = (event: KeyboardEvent) => {
240
+ if (event.key === 'Shift') {
241
+ dom.style.removeProperty('cursor')
242
+ }
243
+ }
244
+
245
+ $effect(() => {
246
+ window.addEventListener('keydown', onkeydown)
247
+ window.addEventListener('keyup', onkeyup)
248
+ dom.addEventListener('pointerdown', onpointerdown)
249
+ dom.addEventListener('pointermove', onpointermove)
250
+ dom.addEventListener('pointerup', onpointerup)
251
+ dom.addEventListener('pointerleave', onpointerleave)
252
+
253
+ return () => {
254
+ window.removeEventListener('keydown', onkeydown)
255
+ window.removeEventListener('keyup', onkeyup)
256
+ dom.removeEventListener('pointerdown', onpointerdown)
257
+ dom.removeEventListener('pointermove', onpointermove)
258
+ dom.removeEventListener('pointerup', onpointerup)
259
+ dom.removeEventListener('pointerleave', onpointerleave)
260
+ }
261
+ })
262
+
263
+ const lassos = useQuery(lassoTraits.Lasso)
264
+
265
+ $effect(() => {
266
+ if (!controls.current) return
267
+
268
+ const currentControls = controls.current
269
+
270
+ const { minPolarAngle, maxPolarAngle } = currentControls
271
+
272
+ // Locks the camera to top down while this component is mounted
273
+ currentControls.polarAngle = 0
274
+ currentControls.minPolarAngle = 0
275
+ currentControls.maxPolarAngle = 0
276
+
277
+ return () => {
278
+ currentControls.minPolarAngle = minPolarAngle
279
+ currentControls.maxPolarAngle = maxPolarAngle
280
+ }
281
+ })
282
+
283
+ // On unmount, destroy all lasso related entities
284
+ $effect(() => {
285
+ return () => {
286
+ for (const entity of world.query(lassoTraits.LassoEnclosedPoints)) {
287
+ if (world.has(entity)) {
288
+ entity.destroy()
289
+ }
290
+ }
291
+ }
292
+ })
107
293
  </script>
108
294
 
109
- <T.Mesh
110
- {onpointerdown}
111
- {onpointerup}
112
- {onpointermove}
113
- >
114
- <T.PlaneGeometry args={[7, 7, 10, 10]} />
115
- <T.MeshBasicMaterial
116
- wireframe
117
- color="blue"
118
- transparent
119
- opacity={debug ? 1 : 0}
120
- />
121
- </T.Mesh>
122
-
123
- {#each lassos as lasso (lasso)}
124
- <Line positions={lasso.positions} />
125
-
126
- {#if debug}
127
- {#if lasso.indices.length > 0}
128
- <T.Mesh>
129
- <T.BufferGeometry
130
- oncreate={(ref) => {
131
- ref.setIndex(new BufferAttribute(lasso.indices, 1))
132
- ref.setAttribute('position', new BufferAttribute(new Float32Array(lasso.positions), 3))
133
- }}
134
- />
135
- <T.MeshBasicMaterial
136
- wireframe
137
- color="green"
138
- />
139
- </T.Mesh>
140
- {/if}
141
-
142
- {#each lasso.boxes as box (box)}
143
- <T.Box3Helper args={[box, 'lightgreen']} />
144
- {/each}
145
-
146
- <T.Box3Helper
147
- args={[
148
- new Box3(a.set(lasso.min.x, lasso.min.y, 0), b.set(lasso.max.x, lasso.max.y, 0)),
149
- 'red',
150
- ]}
151
- />
152
- {/if}
153
- {/each}
295
+ {#if debug}
296
+ {#each lassos.current as lasso (lasso)}
297
+ <Debug {lasso} />
298
+ {/each}
299
+ {/if}
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ import { Portal } from '@threlte/extras'
3
+ import { Button } from '@viamrobotics/prime-core'
4
+ import Lasso from './Lasso.svelte'
5
+ import DashboardButton from '../overlay/dashboard/Button.svelte'
6
+ import { useSettings } from '../../hooks/useSettings.svelte'
7
+ import FloatingPanel from '../overlay/FloatingPanel.svelte'
8
+ import { traits, useWorld } from '../../ecs'
9
+ import * as lassoTraits from './traits'
10
+ import { BufferGeometryUtils } from 'three/examples/jsm/Addons.js'
11
+ import { createBinaryPCD } from '../../pcd'
12
+ import type { BufferGeometry } from 'three'
13
+
14
+ interface Props {
15
+ /** Whether to auto-enable lasso mode when the component mounts */
16
+ enabled?: boolean
17
+
18
+ /** Fires when the user has committed to a lasso selection */
19
+ onSelection: (pcd: Blob) => void
20
+ }
21
+
22
+ let { enabled = false, onSelection }: Props = $props()
23
+
24
+ const world = useWorld()
25
+ const settings = useSettings()
26
+ const isLassoMode = $derived(settings.current.interactionMode === 'lasso')
27
+
28
+ const onCommitClick = () => {
29
+ const entities = world.query(lassoTraits.LassoEnclosedPoints)
30
+
31
+ const geometries: BufferGeometry[] = []
32
+ for (const entity of entities) {
33
+ const geometry = entity.get(traits.BufferGeometry)
34
+
35
+ if (geometry) {
36
+ geometries.push(geometry)
37
+ }
38
+ }
39
+
40
+ const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries)
41
+ const positions = mergedGeometry.getAttribute('position').array as Float32Array
42
+
43
+ const pcd = createBinaryPCD(positions)
44
+
45
+ onSelection(pcd)
46
+
47
+ for (const entity of entities) {
48
+ if (world.has(entity)) {
49
+ entity.destroy()
50
+ }
51
+ }
52
+ }
53
+
54
+ $effect(() => {
55
+ if (enabled) {
56
+ settings.current.interactionMode = 'lasso'
57
+ }
58
+ })
59
+ </script>
60
+
61
+ <Portal id="dashboard">
62
+ <fieldset>
63
+ <DashboardButton
64
+ active={isLassoMode}
65
+ icon="selection-drag"
66
+ description="{isLassoMode ? 'Disable' : 'Enable'} lasso selection"
67
+ onclick={() => {
68
+ settings.current.interactionMode = isLassoMode ? 'navigate' : 'lasso'
69
+ }}
70
+ />
71
+ </fieldset>
72
+ </Portal>
73
+
74
+ {#if isLassoMode}
75
+ <Lasso />
76
+
77
+ <Portal id="dom">
78
+ <FloatingPanel
79
+ isOpen
80
+ exitable={false}
81
+ title="Lasso"
82
+ defaultSize={{ width: 445, height: 100 }}
83
+ defaultPosition={{ x: window.innerWidth / 2 - 200, y: window.innerHeight - 10 - 100 }}
84
+ >
85
+ <div class="flex items-center gap-4 p-4 text-xs">
86
+ Shift + click and drag to make a lasso selection.
87
+ <Button
88
+ onclick={onCommitClick}
89
+ variant="success">Commit selection</Button
90
+ >
91
+ </div>
92
+ </FloatingPanel>
93
+ </Portal>
94
+ {/if}
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ /** Whether to auto-enable lasso mode when the component mounts */
3
+ enabled?: boolean;
4
+ /** Fires when the user has committed to a lasso selection */
5
+ onSelection: (pcd: Blob) => void;
6
+ }
7
+ declare const Tool: import("svelte").Component<Props, {}, "">;
8
+ type Tool = ReturnType<typeof Tool>;
9
+ export default Tool;
@@ -0,0 +1,21 @@
1
+ export declare const Lasso: import("koota").Trait<() => boolean>;
2
+ export declare const LassoEnclosedPoints: import("koota").Trait<() => boolean>;
3
+ /**
4
+ * Captured points are removable, so we want to also destroy
5
+ * the source lasso every time a user deletes one.
6
+ */
7
+ export declare const PointsCapturedBy: import("koota").Relation<import("koota").Trait<Record<string, never>>>;
8
+ export interface AABB {
9
+ minX: number;
10
+ minY: number;
11
+ maxX: number;
12
+ maxY: number;
13
+ }
14
+ export declare const Box: import("koota").Trait<{
15
+ minX: number;
16
+ minY: number;
17
+ maxX: number;
18
+ maxY: number;
19
+ }>;
20
+ export declare const Indices: import("koota").Trait<() => Uint16Array<ArrayBuffer>>;
21
+ export declare const Boxes: import("koota").Trait<() => AABB[]>;
@@ -0,0 +1,16 @@
1
+ import { relation, trait } from 'koota';
2
+ export const Lasso = trait(() => true);
3
+ export const LassoEnclosedPoints = trait(() => true);
4
+ /**
5
+ * Captured points are removable, so we want to also destroy
6
+ * the source lasso every time a user deletes one.
7
+ */
8
+ export const PointsCapturedBy = relation({ autoDestroy: 'target' });
9
+ export const Box = trait({
10
+ minX: 0,
11
+ minY: 0,
12
+ maxX: 0,
13
+ maxY: 0,
14
+ });
15
+ export const Indices = trait(() => new Uint16Array());
16
+ export const Boxes = trait(() => []);
@@ -0,0 +1,20 @@
1
+ <script>
2
+ import { LineGeometry } from 'three/examples/jsm/Addons.js'
3
+ import { T } from '@threlte/core'
4
+ import { untrack } from 'svelte'
5
+
6
+ let { positions } = $props()
7
+
8
+ let geometry = $state.raw(new LineGeometry())
9
+
10
+ $effect(() => {
11
+ if (positions) {
12
+ untrack(() => {
13
+ geometry = new LineGeometry()
14
+ geometry.setPositions(positions)
15
+ })
16
+ }
17
+ })
18
+ </script>
19
+
20
+ <T is={geometry} />
@@ -1,11 +1,12 @@
1
- export default Line;
2
- type Line = {
1
+ export default LineGeometry;
2
+ type LineGeometry = {
3
3
  $on?(type: string, callback: (e: any) => void): () => void;
4
4
  $set?(props: Partial<$$ComponentProps>): void;
5
5
  };
6
- declare const Line: import("svelte").Component<{
6
+ declare const LineGeometry: import("svelte").Component<{
7
7
  positions: any;
8
8
  }, {}, "">;
9
+ import { LineGeometry } from 'three/examples/jsm/Addons.js';
9
10
  type $$ComponentProps = {
10
11
  positions: any;
11
12
  };
@@ -21,7 +21,7 @@
21
21
  let p1 = $state.raw<Vector3>()
22
22
  let p2 = $state.raw<Vector3>()
23
23
 
24
- const enabled = $derived(settings.current.enableMeasure)
24
+ const enabled = $derived(settings.current.interactionMode === 'measure')
25
25
 
26
26
  const { onclick, onmove, raycaster } = useMouseRaycaster(() => ({
27
27
  enabled,
@@ -85,7 +85,7 @@
85
85
  icon="ruler"
86
86
  description="{enabled ? 'Disable' : 'Enable'} measurement"
87
87
  onclick={() => {
88
- settings.current.enableMeasure = !settings.current.enableMeasure
88
+ settings.current.interactionMode = enabled ? 'navigate' : 'measure'
89
89
  }}
90
90
  />
91
91
  <Popover>
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { parsePcdInWorker } from '../lib'
3
+ import { traits, useWorld } from '../ecs'
4
+ import { createBufferGeometry } from '../attribute'
5
+ import type { Entity } from 'koota'
6
+
7
+ interface Props {
8
+ data: Uint8Array
9
+ }
10
+
11
+ let { data }: Props = $props()
12
+
13
+ const world = useWorld()
14
+
15
+ let entity: Entity
16
+
17
+ $effect(() => {
18
+ parsePcdInWorker(data).then(({ positions, colors }) => {
19
+ const geometry = createBufferGeometry(positions, colors)
20
+
21
+ entity = world.spawn(
22
+ traits.Name('Random points'),
23
+ traits.Points,
24
+ traits.BufferGeometry(geometry)
25
+ )
26
+ })
27
+
28
+ return () => {
29
+ if (entity && world.has(entity)) {
30
+ entity.destroy()
31
+ }
32
+ }
33
+ })
34
+ </script>
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ data: Uint8Array;
3
+ }
4
+ declare const PCD: import("svelte").Component<Props, {}, "">;
5
+ type PCD = ReturnType<typeof PCD>;
6
+ export default PCD;
@@ -12,7 +12,7 @@
12
12
  const transformControls = useTransformControls()
13
13
  const cameraDown = new Vector3()
14
14
 
15
- const enabled = $derived(!settings.current.enableMeasure)
15
+ const enabled = $derived(settings.current.interactionMode === 'navigate')
16
16
 
17
17
  const size = 1_000
18
18
  </script>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { Vector3 } from 'three'
2
+ import { ShaderMaterial, Vector3 } from 'three'
3
3
  import { T } from '@threlte/core'
4
4
  import { Grid, interactivity, PerfMonitor, PortalTarget } from '@threlte/extras'
5
5
  import Entities from './Entities.svelte'
@@ -40,7 +40,7 @@
40
40
  })
41
41
 
42
42
  $effect(() => {
43
- enabled.set(!settings.current.enableMeasure)
43
+ enabled.set(settings.current.interactionMode === 'navigate')
44
44
  })
45
45
 
46
46
  bvh(raycaster, () => ({ helper: false }))
@@ -76,11 +76,16 @@
76
76
 
77
77
  {#if !$isPresenting && settings.current.grid}
78
78
  <Grid
79
+ oncreate={(ref) => {
80
+ const material = ref.material as ShaderMaterial
81
+ material.depthWrite = false
82
+ }}
79
83
  raycast={() => null}
80
84
  bvh={{ enabled: false }}
81
85
  plane="xy"
82
86
  sectionColor="#333"
83
87
  infiniteGrid
88
+ renderOrder={999}
84
89
  cellSize={settings.current.gridCellSize}
85
90
  sectionSize={settings.current.gridSectionSize}
86
91
  fadeOrigin={new Vector3()}
@@ -8,6 +8,8 @@
8
8
  interface Props {
9
9
  title?: string
10
10
  defaultSize?: { width: number; height: number }
11
+ defaultPosition?: { x: number; y: number }
12
+ exitable?: boolean
11
13
  isOpen?: boolean
12
14
  children: Snippet
13
15
  }
@@ -15,6 +17,8 @@
15
17
  let {
16
18
  title = '',
17
19
  defaultSize = { width: 700, height: 500 },
20
+ defaultPosition,
21
+ exitable = true,
18
22
  isOpen = $bindable(false),
19
23
  children,
20
24
  }: Props = $props()
@@ -23,6 +27,7 @@
23
27
  const floatingPanelService = useMachine(floatingPanel.machine, () => ({
24
28
  id,
25
29
  defaultSize,
30
+ defaultPosition,
26
31
  resizable: false,
27
32
  allowOverflow: false,
28
33
  open: isOpen,
@@ -54,17 +59,19 @@
54
59
  {title}
55
60
  </p>
56
61
 
57
- <div
58
- {...api.getControlProps()}
59
- class="flex gap-3"
60
- >
61
- <button
62
- aria-label="Close connection configs panel"
63
- onclick={() => (isOpen = false)}
62
+ {#if exitable}
63
+ <div
64
+ {...api.getControlProps()}
65
+ class="flex gap-3"
64
66
  >
65
- <Icon name="close" />
66
- </button>
67
- </div>
67
+ <button
68
+ aria-label="Close connection configs panel"
69
+ onclick={() => (isOpen = false)}
70
+ >
71
+ <Icon name="close" />
72
+ </button>
73
+ </div>
74
+ {/if}
68
75
  </div>
69
76
  </div>
70
77
 
@@ -5,6 +5,11 @@ interface Props {
5
5
  width: number;
6
6
  height: number;
7
7
  };
8
+ defaultPosition?: {
9
+ x: number;
10
+ y: number;
11
+ };
12
+ exitable?: boolean;
8
13
  isOpen?: boolean;
9
14
  children: Snippet;
10
15
  }
@@ -59,6 +59,7 @@ export declare const Instance: import("koota").Trait<{
59
59
  meshID: number;
60
60
  instanceID: number;
61
61
  }>;
62
+ export declare const RenderOrder: import("koota").Trait<() => number>;
62
63
  export declare const Opacity: import("koota").Trait<() => number>;
63
64
  /**
64
65
  * The color of an object
@@ -69,6 +70,13 @@ export declare const Color: import("koota").Trait<{
69
70
  g: number;
70
71
  b: number;
71
72
  }>;
73
+ /**
74
+ * Material properties
75
+ */
76
+ export declare const Material: import("koota").Trait<{
77
+ depthTest: boolean;
78
+ }>;
79
+ export declare const DepthTest: import("koota").Trait<() => boolean>;
72
80
  export declare const Arrow: import("koota").Trait<() => boolean>;
73
81
  export declare const Positions: import("koota").Trait<() => Float32Array<ArrayBuffer>>;
74
82
  export declare const Colors: import("koota").Trait<() => Uint8Array<ArrayBuffer>>;
@@ -37,12 +37,20 @@ export const Instance = trait({
37
37
  meshID: -1,
38
38
  instanceID: -1,
39
39
  });
40
+ export const RenderOrder = trait(() => 0);
40
41
  export const Opacity = trait(() => 1);
41
42
  /**
42
43
  * The color of an object
43
44
  * @default { r: 1, g: 0, b: 0 }
44
45
  */
45
46
  export const Color = trait({ r: 0, g: 0, b: 0 });
47
+ /**
48
+ * Material properties
49
+ */
50
+ export const Material = trait({
51
+ depthTest: false,
52
+ });
53
+ export const DepthTest = trait(() => true);
46
54
  export const Arrow = trait(() => true);
47
55
  export const Positions = trait(() => new Float32Array());
48
56
  export const Colors = trait(() => new Uint8Array());
@@ -8,8 +8,9 @@ interface CameraControlsContext {
8
8
  current: CameraControlsRef | undefined;
9
9
  set(current: CameraControlsRef): void;
10
10
  setPose(pose: CameraPose, animate?: boolean): void;
11
+ setInitialPose(): void;
11
12
  }
12
- export declare const provideCameraControls: (cameraPose: () => CameraPose | undefined) => void;
13
+ export declare const provideCameraControls: (initialCameraPose: () => CameraPose | undefined) => void;
13
14
  export declare const useCameraControls: () => CameraControlsContext;
14
15
  interface TransformControlsContext {
15
16
  active: boolean;
@@ -1,7 +1,7 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  const TRANSFORM_CONTROLS_KEY = Symbol('tranform-controls-context');
3
3
  const CAMERA_CONTROLS_KEY = Symbol('camera-controls-context');
4
- export const provideCameraControls = (cameraPose) => {
4
+ export const provideCameraControls = (initialCameraPose) => {
5
5
  let controls = $state.raw();
6
6
  const setPose = (pose, animate = false) => {
7
7
  const [x, y, z] = pose.position;
@@ -9,8 +9,12 @@ export const provideCameraControls = (cameraPose) => {
9
9
  controls?.setPosition(x, y, z, animate);
10
10
  controls?.setLookAt(x, y, z, lookAtX, lookAtY, lookAtZ, animate);
11
11
  };
12
+ const setInitialPose = () => {
13
+ const pose = initialCameraPose();
14
+ setPose(pose ?? { position: [3, 3, 3], lookAt: [0, 0, 0] }, true);
15
+ };
12
16
  $effect(() => {
13
- const pose = cameraPose();
17
+ const pose = initialCameraPose();
14
18
  if (pose) {
15
19
  setPose(pose);
16
20
  }
@@ -23,6 +27,7 @@ export const provideCameraControls = (cameraPose) => {
23
27
  controls = current;
24
28
  },
25
29
  setPose,
30
+ setInitialPose,
26
31
  });
27
32
  };
28
33
  export const useCameraControls = () => {
@@ -277,7 +277,7 @@ class StandalonePartConfig {
277
277
  if (!fragmentId) {
278
278
  continue;
279
279
  }
280
- const components = fragmentResponse?.fragment?.fields['components'].kind;
280
+ const components = fragmentResponse?.fragment?.fields['components']?.kind;
281
281
  if (components?.case === 'listValue') {
282
282
  for (const component of components.value.values) {
283
283
  if (component.kind.case === 'structValue') {
@@ -12,7 +12,7 @@ export interface Settings {
12
12
  pointColor: string;
13
13
  lineWidth: number;
14
14
  lineDotSize: number;
15
- enableMeasure: boolean;
15
+ interactionMode: 'navigate' | 'measure' | 'lasso';
16
16
  enableMeasureAxisX: boolean;
17
17
  enableMeasureAxisY: boolean;
18
18
  enableMeasureAxisZ: boolean;
@@ -15,7 +15,7 @@ const defaults = () => ({
15
15
  pointColor: '#333333',
16
16
  lineWidth: 0.005,
17
17
  lineDotSize: 0.01,
18
- enableMeasure: false,
18
+ interactionMode: 'navigate',
19
19
  enableMeasureAxisX: true,
20
20
  enableMeasureAxisY: true,
21
21
  enableMeasureAxisZ: true,
package/dist/index.d.ts CHANGED
@@ -1 +1,3 @@
1
1
  export { default as MotionTools } from './components/App.svelte';
2
+ export { default as LassoTool } from './components/Lasso/Tool.svelte';
3
+ export { default as PCD } from './components/PCD.svelte';
package/dist/index.js CHANGED
@@ -1 +1,4 @@
1
1
  export { default as MotionTools } from './components/App.svelte';
2
+ // Plugins
3
+ export { default as LassoTool } from './components/Lasso/Tool.svelte';
4
+ export { default as PCD } from './components/PCD.svelte';
package/dist/pcd.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const createBinaryPCD: (positions: Float32Array, colors?: Uint8Array) => Blob;
package/dist/pcd.js ADDED
@@ -0,0 +1,44 @@
1
+ export const createBinaryPCD = (positions, colors) => {
2
+ const numPoints = positions.length / 3;
3
+ const hasColor = !!colors;
4
+ if (hasColor && colors.length !== numPoints * 3) {
5
+ throw new Error('Color array length must be numPoints * 3');
6
+ }
7
+ // 12 bytes xyz + optional 4 byte packed rgb
8
+ const stride = hasColor ? 16 : 12;
9
+ const header = `# .PCD v0.7 - Point Cloud Data file format
10
+ VERSION 0.7
11
+ FIELDS x y z${hasColor ? ' rgb' : ''}
12
+ SIZE 4 4 4${hasColor ? ' 4' : ''}
13
+ TYPE F F F${hasColor ? ' F' : ''}
14
+ COUNT 1 1 1${hasColor ? ' 1' : ''}
15
+ WIDTH ${numPoints}
16
+ HEIGHT 1
17
+ VIEWPOINT 0 0 0 1 0 0 0
18
+ POINTS ${numPoints}
19
+ DATA binary
20
+ `;
21
+ const headerBytes = new TextEncoder().encode(header);
22
+ const bodyBuffer = new ArrayBuffer(numPoints * stride);
23
+ const view = new DataView(bodyBuffer);
24
+ for (let i = 0; i < numPoints; i++) {
25
+ const offset = i * stride;
26
+ const pi = i * 3;
27
+ // XYZ
28
+ view.setFloat32(offset + 0, positions[pi + 0], true);
29
+ view.setFloat32(offset + 4, positions[pi + 1], true);
30
+ view.setFloat32(offset + 8, positions[pi + 2], true);
31
+ if (hasColor) {
32
+ const r = colors[pi + 0];
33
+ const g = colors[pi + 1];
34
+ const b = colors[pi + 2];
35
+ // pack into uint32
36
+ const packed = (r << 16) | (g << 8) | b;
37
+ // write as float32
38
+ view.setUint32(offset + 12, packed, true);
39
+ }
40
+ }
41
+ return new Blob([headerBytes, bodyBuffer], {
42
+ type: 'application/octet-stream',
43
+ });
44
+ };
@@ -50,7 +50,12 @@ export const bvh = (raycaster, options) => {
50
50
  ref.remove(helper);
51
51
  };
52
52
  }
53
- else if (isInstanceOf(ref, 'Mesh')) {
53
+ else if (isInstanceOf(ref, 'Mesh') &&
54
+ /**
55
+ * (mp) Line2s sort of suck. Their buffer attribute design internally is much different
56
+ * but they give no indication other than this that they are different.
57
+ */
58
+ ref.geometry.attributes.position) {
54
59
  ref.geometry.computeBoundsTree = computeBoundsTree;
55
60
  ref.geometry.disposeBoundsTree = disposeBoundsTree;
56
61
  ref.raycast = acceleratedRaycast;
@@ -1 +1 @@
1
- export declare const createRandomPcdBinary: (numPoints?: number, scale?: number, axes?: string) => Uint8Array;
1
+ export declare const createRandomPcdBinary: (numPoints?: number, scale?: number, axes?: string) => Promise<Uint8Array>;
@@ -1,31 +1,18 @@
1
- export const createRandomPcdBinary = (numPoints = 200, scale = 1, axes = 'xyz') => {
2
- const header = `
3
- # .PCD v0.7 - Point Cloud Data file format
4
- VERSION 0.7
5
- FIELDS x y z rgb
6
- SIZE 4 4 4 4
7
- TYPE F F F F
8
- COUNT 1 1 1
9
- WIDTH ${numPoints}
10
- HEIGHT 1
11
- VIEWPOINT 0 0 0 1 0 0 0
12
- POINTS ${numPoints}
13
- DATA ascii
14
- `.trim();
1
+ import { createBinaryPCD } from '../pcd';
2
+ export const createRandomPcdBinary = async (numPoints = 200, scale = 1, axes = 'xyz') => {
15
3
  const doX = axes.includes('x');
16
4
  const doY = axes.includes('y');
17
5
  const doZ = axes.includes('z');
18
- const points = Array.from({ length: numPoints }, () => {
19
- const x = doX ? ((Math.random() - 0.5) * scale).toFixed(6) : '0.000000';
20
- const y = doY ? ((Math.random() - 0.5) * scale).toFixed(6) : '0.000000';
21
- const z = doZ ? ((Math.random() - 0.5) * scale).toFixed(6) : '0.000000';
22
- const red = Math.floor(Math.random() * 256);
23
- const green = Math.floor(Math.random() * 256);
24
- const blue = Math.floor(Math.random() * 256);
25
- const rgbInt = (red << 16) | (green << 8) | blue;
26
- const rgbFloat = String(new Float32Array(new Uint32Array([rgbInt]).buffer)[0]);
27
- return `${x} ${y} ${z} ${rgbFloat}`;
28
- });
29
- const encoder = new TextEncoder();
30
- return encoder.encode(`${header}\n${points.join('\n')}`);
6
+ const positions = new Float32Array(numPoints * 3);
7
+ const colors = new Uint8Array(numPoints * 3);
8
+ for (let i = 0, l = positions.length; i < l; i += 1) {
9
+ positions[i] = doX ? (Math.random() - 0.5) * scale : 0;
10
+ positions[i + 1] = doY ? (Math.random() - 0.5) * scale : 0;
11
+ positions[i + 2] = doZ ? (Math.random() - 0.5) * scale : 0;
12
+ colors[i] = Math.floor(Math.random() * 256);
13
+ colors[i + 1] = Math.floor(Math.random() * 256);
14
+ colors[i + 2] = Math.floor(Math.random() * 256);
15
+ }
16
+ const buffer = await createBinaryPCD(positions, colors).arrayBuffer();
17
+ return new Uint8Array(buffer);
31
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,44 +0,0 @@
1
- <script>
2
- import { T, useThrelte } from '@threlte/core'
3
- import { Line2 } from 'three/examples/jsm/lines/Line2.js'
4
- import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'
5
- import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js'
6
- import { untrack } from 'svelte'
7
-
8
- let { positions } = $props()
9
-
10
- let geometry = $state.raw(new LineGeometry())
11
-
12
- const material = new LineMaterial()
13
- const line = new Line2()
14
- material.linewidth = 1
15
- material.color.set('red')
16
- material.depthTest = false
17
- material.depthWrite = false
18
-
19
- const { invalidate } = useThrelte()
20
-
21
- $effect(() => {
22
- untrack(() => {
23
- geometry = new LineGeometry()
24
- invalidate()
25
- })
26
-
27
- if (!positions || positions.length === 0) {
28
- return
29
- }
30
-
31
- untrack(() => {
32
- geometry.setPositions(positions)
33
- })
34
- })
35
- </script>
36
-
37
- <T
38
- is={line}
39
- frustumCulled={false}
40
- renderOrder={Number.POSITIVE_INFINITY}
41
- >
42
- <T is={geometry} />
43
- <T is={material} />
44
- </T>