@viamrobotics/motion-tools 1.11.1 → 1.12.1

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 +240 -92
  7. package/dist/components/Lasso/Tool.svelte +100 -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 +18 -10
  19. package/dist/components/overlay/FloatingPanel.svelte.d.ts +6 -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,301 @@
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
+ const element = event.target as HTMLElement
43
+ const rect = element.getBoundingClientRect()
44
+ mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
45
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
39
46
 
40
- lassos.push({
41
- positions: [x, y, 0],
42
- indices: new Uint16Array(),
43
- boxes: [],
44
- min: { x, y },
45
- max: { x, y },
46
- })
47
+ raycaster.setFromCamera(mouse, camera.current)
48
+ raycaster.ray.intersectPlane(plane, point)
49
+ return point
50
+ }
51
+
52
+ const onpointerdown = (event: PointerEvent) => {
53
+ if (!event.shiftKey) return
54
+
55
+ const { x, y } = raycast(event)
56
+
57
+ drawing = true
58
+
59
+ world.spawn(
60
+ traits.LinePositions(new Float32Array([x, y, 0])),
61
+ traits.LineWidth(1.5),
62
+ traits.RenderOrder(999),
63
+ traits.Material({ depthTest: false }),
64
+ traits.Color({ r: 1, g: 0, b: 0 }),
65
+ lassoTraits.Box({ minX: x, minY: y, maxX: x, maxY: y }),
66
+ lassoTraits.Lasso
67
+ )
47
68
 
48
69
  if (controls.current) {
49
70
  controls.current.enabled = false
50
71
  }
51
72
  }
52
73
 
53
- const onpointermove = (event: IntersectionEvent<PointerEvent>) => {
54
- event.point.toArray(position)
55
-
74
+ const onpointermove = (event: PointerEvent) => {
56
75
  if (!drawing) return
57
76
 
58
- let line = lassos.at(-1)
77
+ let lasso = world.query(lassoTraits.Lasso).at(-1)
78
+
79
+ if (!lasso) return
80
+
81
+ if (frameScheduled) return
82
+
83
+ frameScheduled = true
84
+
85
+ /**
86
+ * pointermove can execute at a rate much higher than screen
87
+ * refresh, creating huge polygon vertex counts, so we cap it.
88
+ */
89
+ requestAnimationFrame(() => {
90
+ frameScheduled = false
91
+
92
+ const { x, y } = raycast(event)
93
+ const positions = lasso.get(traits.LinePositions)
94
+ const box = lasso.get(lassoTraits.Box)
59
95
 
60
- if (!line) return
96
+ if (!positions || !box) return
61
97
 
62
- const { x, y } = event.point
63
- line.positions.push(x, y, 0)
98
+ const nextPositions = new Float32Array(positions.length + 3)
99
+ nextPositions.set(positions)
100
+ nextPositions[positions.length] = x
101
+ nextPositions[positions.length + 1] = y
102
+ lasso.set(traits.LinePositions, nextPositions)
64
103
 
65
- if (x < line.min.x) line.min.x = x
66
- else if (x > line.max.x) line.max.x = x
104
+ if (x < box.minX) box.minX = x
105
+ else if (x > box.maxX) box.maxX = x
67
106
 
68
- if (y < line.min.y) line.min.y = y
69
- else if (y > line.max.y) line.max.y = y
107
+ if (y < box.minY) box.minY = y
108
+ else if (y > box.maxY) box.maxY = y
109
+
110
+ lasso.set(lassoTraits.Box, box)
111
+ })
112
+ }
113
+
114
+ const onpointerleave = () => {
115
+ if (!drawing) return
116
+
117
+ onpointerup()
70
118
  }
71
119
 
72
120
  const onpointerup = () => {
121
+ if (!drawing) return
122
+
73
123
  drawing = false
74
124
 
75
- let lasso = lassos.at(-1)
125
+ let lasso = world.query(lassoTraits.Lasso).at(-1)
76
126
 
77
127
  if (!lasso) return
78
128
 
79
- const [x, y] = lasso.positions
129
+ let positions = lasso.get(traits.LinePositions)
130
+
131
+ if (!positions) return
132
+
133
+ const [startX, startY] = positions
80
134
 
81
135
  if (controls.current) {
82
136
  controls.current.enabled = true
83
137
  }
84
138
 
85
139
  // Close the loop
86
- lasso.positions.push(x, y, 0)
87
-
88
- const { positions } = lasso
140
+ const nextPositions = new Float32Array(positions.length + 3)
141
+ nextPositions.set(positions)
142
+ nextPositions[positions.length] = startX
143
+ nextPositions[positions.length + 1] = startY
144
+ lasso.set(traits.LinePositions, nextPositions)
145
+ positions = nextPositions
89
146
 
90
147
  const indices = earcut(positions, undefined, 3)
91
- lasso.indices = new Uint16Array(indices)
148
+ if (debug) {
149
+ lasso.add(lassoTraits.Indices(new Uint16Array(indices)))
150
+ }
92
151
 
93
- for (let i = 0; i < indices.length; i += 6) {
152
+ const getTriangleFromIndex = (i: number, triangle: Triangle) => {
94
153
  const stride = 3
95
154
  const ia = indices[i + 0] * stride
96
155
  const ib = indices[i + 1] * stride
97
156
  const ic = indices[i + 2] * stride
98
-
99
157
  a.set(positions[ia + 0], positions[ia + 1], positions[ia + 2])
100
158
  b.set(positions[ib + 0], positions[ib + 1], positions[ib + 2])
101
159
  c.set(positions[ic + 0], positions[ic + 1], positions[ic + 2])
102
- box3.setFromPoints([a, b, c])
160
+ triangle.set(a, b, c)
161
+ }
162
+
163
+ const boxes: lassoTraits.AABB[] = []
164
+ for (let i = 0, l = indices.length; i < l; i += 3) {
165
+ getTriangleFromIndex(i, triangle)
166
+ box3.setFromPoints([triangle.a, triangle.b, triangle.c])
167
+ boxes.push({ minX: box3.min.x, minY: box3.min.y, maxX: box3.max.x, maxY: box3.max.y })
168
+ }
169
+ if (debug) {
170
+ lasso.add(lassoTraits.Boxes(boxes))
171
+ }
172
+
173
+ const lassoBox = lasso.get(lassoTraits.Box)
174
+
175
+ if (!lassoBox) return
176
+
177
+ min.set(lassoBox.minX, lassoBox.minY, Number.NEGATIVE_INFINITY)
178
+ max.set(lassoBox.maxX, lassoBox.maxY, Number.POSITIVE_INFINITY)
179
+ box3.set(min, max)
180
+
181
+ const enclosedPoints: number[] = []
182
+
183
+ for (const pointsEntity of world.query(traits.Points, Not(lassoTraits.LassoEnclosedPoints))) {
184
+ const geometry = pointsEntity.get(traits.BufferGeometry)
185
+
186
+ if (!geometry) return
103
187
 
104
- lasso.boxes.push(box3.clone())
188
+ const points = scene.getObjectByName(pointsEntity as unknown as string)
189
+
190
+ if (!points) {
191
+ return
192
+ }
193
+
194
+ geometry.boundsTree?.shapecast({
195
+ intersectsBounds: (box) => {
196
+ return box.intersectsBox(box3)
197
+ },
198
+
199
+ intersectsPoint: (point: Vector3) => {
200
+ for (let i = 0, j = 0, l = indices.length; i < l; i += 3, j += 1) {
201
+ const { minX, minY, maxX, maxY } = boxes[j]
202
+
203
+ min.set(minX, minY, Number.NEGATIVE_INFINITY)
204
+ max.set(maxX, maxY, Number.POSITIVE_INFINITY)
205
+ triangleBox.set(min, max)
206
+
207
+ if (triangleBox.containsPoint(point)) {
208
+ getTriangleFromIndex(i, triangle)
209
+
210
+ if (triangle.containsPoint(point)) {
211
+ enclosedPoints.push(point.x, point.y, point.z)
212
+ }
213
+ }
214
+ }
215
+ },
216
+ // intersectsPoint is not yet in typedef, this can be removed when it is added
217
+ } as ShapecastCallbacks)
105
218
  }
219
+
220
+ const lassoResultGeometry = createBufferGeometry(new Float32Array(enclosedPoints))
221
+
222
+ world.spawn(
223
+ traits.Name('Lasso result'),
224
+ traits.BufferGeometry(lassoResultGeometry),
225
+ traits.Color({ r: 1, g: 0, b: 0 }),
226
+ traits.RenderOrder(999),
227
+ traits.Material({ depthTest: false }),
228
+ traits.Points,
229
+ traits.Removable,
230
+ lassoTraits.LassoEnclosedPoints,
231
+ lassoTraits.PointsCapturedBy(lasso)
232
+ )
106
233
  }
234
+
235
+ const onkeydown = (event: KeyboardEvent) => {
236
+ if (event.key === 'Shift') {
237
+ dom.style.cursor = 'crosshair'
238
+ }
239
+ }
240
+
241
+ const onkeyup = (event: KeyboardEvent) => {
242
+ if (event.key === 'Shift') {
243
+ dom.style.removeProperty('cursor')
244
+ }
245
+ }
246
+
247
+ $effect(() => {
248
+ window.addEventListener('keydown', onkeydown)
249
+ window.addEventListener('keyup', onkeyup)
250
+ dom.addEventListener('pointerdown', onpointerdown)
251
+ dom.addEventListener('pointermove', onpointermove)
252
+ dom.addEventListener('pointerup', onpointerup)
253
+ dom.addEventListener('pointerleave', onpointerleave)
254
+
255
+ return () => {
256
+ window.removeEventListener('keydown', onkeydown)
257
+ window.removeEventListener('keyup', onkeyup)
258
+ dom.removeEventListener('pointerdown', onpointerdown)
259
+ dom.removeEventListener('pointermove', onpointermove)
260
+ dom.removeEventListener('pointerup', onpointerup)
261
+ dom.removeEventListener('pointerleave', onpointerleave)
262
+ }
263
+ })
264
+
265
+ const lassos = useQuery(lassoTraits.Lasso)
266
+
267
+ $effect(() => {
268
+ if (!controls.current) return
269
+
270
+ const currentControls = controls.current
271
+
272
+ const { minPolarAngle, maxPolarAngle } = currentControls
273
+
274
+ // Locks the camera to top down while this component is mounted
275
+ currentControls.polarAngle = 0
276
+ currentControls.minPolarAngle = 0
277
+ currentControls.maxPolarAngle = 0
278
+
279
+ return () => {
280
+ currentControls.minPolarAngle = minPolarAngle
281
+ currentControls.maxPolarAngle = maxPolarAngle
282
+ }
283
+ })
284
+
285
+ // On unmount, destroy all lasso related entities
286
+ $effect(() => {
287
+ return () => {
288
+ for (const entity of world.query(lassoTraits.LassoEnclosedPoints)) {
289
+ if (world.has(entity)) {
290
+ entity.destroy()
291
+ }
292
+ }
293
+ }
294
+ })
107
295
  </script>
108
296
 
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}
297
+ {#if debug}
298
+ {#each lassos.current as lasso (lasso)}
299
+ <Debug {lasso} />
300
+ {/each}
301
+ {/if}
@@ -0,0 +1,100 @@
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
+ import { useThrelte } from '@threlte/core'
14
+ import { ElementRect } from 'runed'
15
+
16
+ interface Props {
17
+ /** Whether to auto-enable lasso mode when the component mounts */
18
+ enabled?: boolean
19
+
20
+ /** Fires when the user has committed to a lasso selection */
21
+ onSelection: (pcd: Blob) => void
22
+ }
23
+
24
+ let { enabled = false, onSelection }: Props = $props()
25
+
26
+ const { dom } = useThrelte()
27
+ const world = useWorld()
28
+ const settings = useSettings()
29
+ const isLassoMode = $derived(settings.current.interactionMode === 'lasso')
30
+
31
+ const onCommitClick = () => {
32
+ const entities = world.query(lassoTraits.LassoEnclosedPoints)
33
+
34
+ const geometries: BufferGeometry[] = []
35
+ for (const entity of entities) {
36
+ const geometry = entity.get(traits.BufferGeometry)
37
+
38
+ if (geometry) {
39
+ geometries.push(geometry)
40
+ }
41
+ }
42
+
43
+ const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries)
44
+ const positions = mergedGeometry.getAttribute('position').array as Float32Array
45
+
46
+ const pcd = createBinaryPCD(positions)
47
+
48
+ onSelection(pcd)
49
+
50
+ for (const entity of entities) {
51
+ if (world.has(entity)) {
52
+ entity.destroy()
53
+ }
54
+ }
55
+ }
56
+
57
+ $effect(() => {
58
+ if (enabled) {
59
+ settings.current.interactionMode = 'lasso'
60
+ }
61
+ })
62
+
63
+ const rect = new ElementRect(() => dom)
64
+ </script>
65
+
66
+ <Portal id="dashboard">
67
+ <fieldset>
68
+ <DashboardButton
69
+ active={isLassoMode}
70
+ icon="selection-drag"
71
+ description="{isLassoMode ? 'Disable' : 'Enable'} lasso selection"
72
+ onclick={() => {
73
+ settings.current.interactionMode = isLassoMode ? 'navigate' : 'lasso'
74
+ }}
75
+ />
76
+ </fieldset>
77
+ </Portal>
78
+
79
+ {#if isLassoMode && rect.height > 0 && rect.width > 0}
80
+ <Lasso />
81
+
82
+ <Portal id="dom">
83
+ <FloatingPanel
84
+ isOpen
85
+ exitable={false}
86
+ title="Lasso"
87
+ strategy="absolute"
88
+ defaultSize={{ width: 445, height: 100 }}
89
+ defaultPosition={{ x: rect.width / 2 - 200, y: rect.height - 10 - 100 }}
90
+ >
91
+ <div class="flex items-center gap-4 p-4 text-xs">
92
+ Shift + click and drag to make a lasso selection.
93
+ <Button
94
+ onclick={onCommitClick}
95
+ variant="success">Commit selection</Button
96
+ >
97
+ </div>
98
+ </FloatingPanel>
99
+ </Portal>
100
+ {/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,9 @@
8
8
  interface Props {
9
9
  title?: string
10
10
  defaultSize?: { width: number; height: number }
11
+ defaultPosition?: { x: number; y: number }
12
+ exitable?: boolean
13
+ strategy?: 'absolute' | 'fixed'
11
14
  isOpen?: boolean
12
15
  children: Snippet
13
16
  }
@@ -15,8 +18,10 @@
15
18
  let {
16
19
  title = '',
17
20
  defaultSize = { width: 700, height: 500 },
21
+ exitable = true,
18
22
  isOpen = $bindable(false),
19
23
  children,
24
+ ...props
20
25
  }: Props = $props()
21
26
 
22
27
  const id = $props.id()
@@ -26,6 +31,7 @@
26
31
  resizable: false,
27
32
  allowOverflow: false,
28
33
  open: isOpen,
34
+ ...props,
29
35
  }))
30
36
 
31
37
  const api = $derived(floatingPanel.connect(floatingPanelService, normalizeProps))
@@ -54,17 +60,19 @@
54
60
  {title}
55
61
  </p>
56
62
 
57
- <div
58
- {...api.getControlProps()}
59
- class="flex gap-3"
60
- >
61
- <button
62
- aria-label="Close connection configs panel"
63
- onclick={() => (isOpen = false)}
63
+ {#if exitable}
64
+ <div
65
+ {...api.getControlProps()}
66
+ class="flex gap-3"
64
67
  >
65
- <Icon name="close" />
66
- </button>
67
- </div>
68
+ <button
69
+ aria-label="Close connection configs panel"
70
+ onclick={() => (isOpen = false)}
71
+ >
72
+ <Icon name="close" />
73
+ </button>
74
+ </div>
75
+ {/if}
68
76
  </div>
69
77
  </div>
70
78
 
@@ -5,6 +5,12 @@ interface Props {
5
5
  width: number;
6
6
  height: number;
7
7
  };
8
+ defaultPosition?: {
9
+ x: number;
10
+ y: number;
11
+ };
12
+ exitable?: boolean;
13
+ strategy?: 'absolute' | 'fixed';
8
14
  isOpen?: boolean;
9
15
  children: Snippet;
10
16
  }
@@ -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.1",
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>