@viamrobotics/motion-tools 1.11.0 → 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 (45) hide show
  1. package/dist/components/App.svelte +6 -1
  2. package/dist/components/CameraControls.svelte +1 -1
  3. package/dist/components/Focus.svelte +1 -8
  4. package/dist/components/Geometry.svelte +13 -13
  5. package/dist/components/Lasso/Debug.svelte +72 -0
  6. package/dist/components/Lasso/Debug.svelte.d.ts +8 -0
  7. package/dist/components/Lasso/Lasso.svelte +299 -0
  8. package/dist/components/Lasso/Lasso.svelte.d.ts +6 -0
  9. package/dist/components/Lasso/Tool.svelte +94 -0
  10. package/dist/components/Lasso/Tool.svelte.d.ts +9 -0
  11. package/dist/components/Lasso/traits.d.ts +21 -0
  12. package/dist/components/Lasso/traits.js +16 -0
  13. package/dist/components/LineGeometry.svelte +20 -0
  14. package/dist/components/LineGeometry.svelte.d.ts +12 -0
  15. package/dist/components/MeasureTool/MeasureTool.svelte +2 -2
  16. package/dist/components/PCD.svelte +34 -0
  17. package/dist/components/PCD.svelte.d.ts +6 -0
  18. package/dist/components/PointerMissBox.svelte +1 -2
  19. package/dist/components/Points.svelte +1 -1
  20. package/dist/components/Scene.svelte +10 -8
  21. package/dist/components/hover/HoveredEntityTooltip.svelte +2 -1
  22. package/dist/components/overlay/FloatingPanel.svelte +17 -10
  23. package/dist/components/overlay/FloatingPanel.svelte.d.ts +5 -0
  24. package/dist/components/xr/CameraFeed.svelte +3 -0
  25. package/dist/components/xr/CameraFeed.svelte.d.ts +1 -0
  26. package/dist/components/xr/JointLimitsWidget.svelte +25 -22
  27. package/dist/components/xr/XR.svelte +28 -20
  28. package/dist/ecs/traits.d.ts +8 -0
  29. package/dist/ecs/traits.js +8 -0
  30. package/dist/hooks/useControls.svelte.d.ts +2 -1
  31. package/dist/hooks/useControls.svelte.js +7 -2
  32. package/dist/hooks/usePartConfig.svelte.js +1 -1
  33. package/dist/hooks/useSettings.svelte.d.ts +1 -1
  34. package/dist/hooks/useSettings.svelte.js +1 -1
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +3 -0
  37. package/dist/pcd.d.ts +1 -0
  38. package/dist/pcd.js +44 -0
  39. package/dist/plugins/bvh.svelte.d.ts +8 -0
  40. package/dist/plugins/bvh.svelte.js +74 -0
  41. package/dist/ply.d.ts +1 -1
  42. package/dist/ply.js +5 -0
  43. package/dist/test/createRandomPcdBinary.d.ts +1 -1
  44. package/dist/test/createRandomPcdBinary.js +14 -27
  45. package/package.json +4 -1
@@ -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'
@@ -29,6 +30,7 @@
29
30
  import Camera from './overlay/widgets/Camera.svelte'
30
31
  import HoveredEntities from './hover/HoveredEntities.svelte'
31
32
  import Settings from './overlay/settings/Settings.svelte'
33
+ import { useXR } from '@threlte/xr'
32
34
 
33
35
  interface LocalConfigProps {
34
36
  getLocalPartConfig: () => Struct
@@ -67,6 +69,7 @@
67
69
  const settings = provideSettings()
68
70
  const environment = provideEnvironment()
69
71
  const currentRobotCameraWidgets = $derived(settings.current.openCameraWidgets[partID] || [])
72
+ const { isPresenting } = useXR()
70
73
 
71
74
  $effect(() => {
72
75
  settings.current.enableKeybindings = enableKeybindings
@@ -148,11 +151,13 @@
148
151
  <ArmPositions />
149
152
  {/if}
150
153
 
151
- {#if !focus}
154
+ {#if !focus && !$isPresenting}
152
155
  {#each currentRobotCameraWidgets as cameraName (cameraName)}
153
156
  <Camera name={cameraName} />
154
157
  {/each}
155
158
  {/if}
159
+
160
+ <PortalTarget id="dom" />
156
161
  </div>
157
162
  {/snippet}
158
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>
@@ -49,14 +49,7 @@
49
49
  </TrackballControls>
50
50
  </Camera>
51
51
 
52
- <T
53
- is={object3d}
54
- bvh={{
55
- enabled: object3d.type === 'Points',
56
- maxDepth: 40,
57
- maxLeafTris: 20,
58
- }}
59
- />
52
+ <T is={object3d} />
60
53
 
61
54
  <T.BoxHelper
62
55
  args={[object3d, 'red']}
@@ -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,7 +139,7 @@
136
139
  is={mesh}
137
140
  name={entity}
138
141
  userData.name={name}
139
- bvh={{ enabled: geometryType === 'buffer' }}
142
+ renderOrder={renderOrder.current}
140
143
  >
141
144
  {#if model && renderMode.includes('model')}
142
145
  <T is={model} />
@@ -144,14 +147,7 @@
144
147
 
145
148
  {#if !model || renderMode.includes('colliders')}
146
149
  {#if linePositions.current}
147
- <T
148
- is={LineGeometry}
149
- oncreate={(ref) => {
150
- if (linePositions.current) {
151
- ref.setPositions(linePositions.current)
152
- }
153
- }}
154
- />
150
+ <LineGeometry positions={linePositions.current} />
155
151
  {:else if box.current}
156
152
  {@const { x, y, z } = box.current ?? { x: 0, y: 0, z: 0 }}
157
153
  <T.BoxGeometry
@@ -179,16 +175,20 @@
179
175
  is={LineMaterial}
180
176
  {color}
181
177
  width={lineWidth.current ? lineWidth.current * 0.001 : 0.5}
178
+ depthTest={materialProps.current?.depthTest}
182
179
  />
183
180
  {:else}
181
+ {@const currentOpacity = opacity.current ?? 0.7}
184
182
  <T.MeshToonMaterial
185
183
  {color}
186
184
  side={geometryType === 'buffer' ? DoubleSide : FrontSide}
187
- transparent={(opacity.current ?? 0.7) < 1}
188
- opacity={opacity.current ?? 0.7}
185
+ transparent={currentOpacity < 1}
186
+ depthWrite={currentOpacity === 1}
187
+ opacity={currentOpacity}
188
+ depthTest={materialProps.current?.depthTest}
189
189
  />
190
190
 
191
- {#if geo && renderMode.includes('colliders')}
191
+ {#if geo && (renderMode.includes('colliders') || !model)}
192
192
  <T.LineSegments
193
193
  raycast={() => null}
194
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;
@@ -0,0 +1,299 @@
1
+ <script lang="ts">
2
+ import { Raycaster, Box3, Vector3, Vector2, Plane, Triangle } from 'three'
3
+ import { useThrelte } from '@threlte/core'
4
+ import { Not } from 'koota'
5
+ import { useCameraControls } from '../../hooks/useControls.svelte'
6
+ import earcut from 'earcut'
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'
12
+
13
+ interface Props {
14
+ debug?: boolean
15
+ }
16
+
17
+ let { debug = false }: Props = $props()
18
+
19
+ const world = useWorld()
20
+ const controls = useCameraControls()
21
+ const { scene, dom, camera } = useThrelte()
22
+
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()
29
+ const a = new Vector3()
30
+ const b = new Vector3()
31
+ const c = new Vector3()
32
+
33
+ let frameScheduled = false
34
+ let drawing = false
35
+
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()
40
+
41
+ const raycast = (event: PointerEvent) => {
42
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1
43
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
44
+
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
+ )
66
+
67
+ if (controls.current) {
68
+ controls.current.enabled = false
69
+ }
70
+ }
71
+
72
+ const onpointermove = (event: PointerEvent) => {
73
+ if (!drawing) return
74
+
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)
93
+
94
+ if (!positions || !box) return
95
+
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)
101
+
102
+ if (x < box.minX) box.minX = x
103
+ else if (x > box.maxX) box.maxX = x
104
+
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()
116
+ }
117
+
118
+ const onpointerup = () => {
119
+ if (!drawing) return
120
+
121
+ drawing = false
122
+
123
+ let lasso = world.query(lassoTraits.Lasso).at(-1)
124
+
125
+ if (!lasso) return
126
+
127
+ let positions = lasso.get(traits.LinePositions)
128
+
129
+ if (!positions) return
130
+
131
+ const [startX, startY] = positions
132
+
133
+ if (controls.current) {
134
+ controls.current.enabled = true
135
+ }
136
+
137
+ // Close the loop
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
144
+
145
+ const indices = earcut(positions, undefined, 3)
146
+ if (debug) {
147
+ lasso.add(lassoTraits.Indices(new Uint16Array(indices)))
148
+ }
149
+
150
+ const getTriangleFromIndex = (i: number, triangle: Triangle) => {
151
+ const stride = 3
152
+ const ia = indices[i + 0] * stride
153
+ const ib = indices[i + 1] * stride
154
+ const ic = indices[i + 2] * stride
155
+ a.set(positions[ia + 0], positions[ia + 1], positions[ia + 2])
156
+ b.set(positions[ib + 0], positions[ib + 1], positions[ib + 2])
157
+ c.set(positions[ic + 0], positions[ic + 1], positions[ic + 2])
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
185
+
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)
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
+ )
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
+ })
293
+ </script>
294
+
295
+ {#if debug}
296
+ {#each lassos.current as lasso (lasso)}
297
+ <Debug {lasso} />
298
+ {/each}
299
+ {/if}
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ debug?: boolean;
3
+ }
4
+ declare const Lasso: import("svelte").Component<Props, {}, "">;
5
+ type Lasso = ReturnType<typeof Lasso>;
6
+ export default Lasso;
@@ -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} />