@viamrobotics/motion-tools 1.15.8 → 1.18.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 (87) hide show
  1. package/dist/attribute.d.ts +3 -2
  2. package/dist/attribute.js +24 -16
  3. package/dist/buf/draw/v1/drawing_pb.d.ts +33 -16
  4. package/dist/buf/draw/v1/drawing_pb.js +35 -17
  5. package/dist/buf/draw/v1/metadata_pb.d.ts +44 -3
  6. package/dist/buf/draw/v1/metadata_pb.js +54 -3
  7. package/dist/buf/draw/v1/scene_pb.d.ts +6 -6
  8. package/dist/buf/draw/v1/scene_pb.js +7 -7
  9. package/dist/buffer.d.ts +54 -45
  10. package/dist/buffer.js +91 -57
  11. package/dist/color.d.ts +1 -2
  12. package/dist/color.js +5 -12
  13. package/dist/components/App.svelte +18 -3
  14. package/dist/components/App.svelte.d.ts +15 -2
  15. package/dist/components/Entities/Arrows/ArrowGroups.svelte +5 -6
  16. package/dist/components/Entities/Arrows/Arrows.svelte +9 -0
  17. package/dist/components/Entities/Entities.svelte +18 -1
  18. package/dist/components/Entities/Frame.svelte +7 -1
  19. package/dist/components/Entities/GLTF.svelte +13 -2
  20. package/dist/components/Entities/Line.svelte +46 -18
  21. package/dist/components/Entities/LineDots.svelte +38 -8
  22. package/dist/components/Entities/LineDots.svelte.d.ts +2 -2
  23. package/dist/components/Entities/LineGeometry.svelte +2 -1
  24. package/dist/components/Entities/LineGeometry.svelte.d.ts +2 -0
  25. package/dist/components/Entities/Mesh.svelte +8 -1
  26. package/dist/components/Entities/Points.svelte +22 -11
  27. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +6 -2
  28. package/dist/components/FileDrop/FileDrop.svelte +5 -1
  29. package/dist/components/KeyboardControls.svelte +2 -10
  30. package/dist/components/PCD.svelte +11 -4
  31. package/dist/components/PCD.svelte.d.ts +3 -1
  32. package/dist/components/SceneProviders.svelte +2 -0
  33. package/dist/components/Selected.svelte +2 -12
  34. package/dist/components/{Lasso → Selection}/Debug.svelte +8 -8
  35. package/dist/components/{Lasso → Selection}/Debug.svelte.d.ts +2 -2
  36. package/dist/components/Selection/Ellipse.svelte +294 -0
  37. package/dist/components/Selection/Ellipse.svelte.d.ts +7 -0
  38. package/dist/components/{Lasso → Selection}/Lasso.svelte +33 -61
  39. package/dist/components/{Lasso → Selection}/Lasso.svelte.d.ts +1 -0
  40. package/dist/components/Selection/Tool.svelte +94 -0
  41. package/dist/components/{Lasso → Selection}/Tool.svelte.d.ts +2 -2
  42. package/dist/components/{Lasso → Selection}/traits.d.ts +11 -2
  43. package/dist/components/{Lasso → Selection}/traits.js +7 -2
  44. package/dist/components/Selection/useSelectionPlugin.svelte.d.ts +8 -0
  45. package/dist/components/Selection/useSelectionPlugin.svelte.js +24 -0
  46. package/dist/components/Selection/utils.d.ts +5 -0
  47. package/dist/components/Selection/utils.js +38 -0
  48. package/dist/components/Snapshot.svelte +4 -2
  49. package/dist/components/overlay/AddRelationship.svelte +1 -2
  50. package/dist/components/overlay/AddRelationship.svelte.d.ts +1 -1
  51. package/dist/components/overlay/Details.svelte +12 -12
  52. package/dist/components/overlay/Details.svelte.d.ts +8 -1
  53. package/dist/components/overlay/settings/Settings.svelte +8 -1
  54. package/dist/components/xr/OriginMarker.svelte +94 -17
  55. package/dist/components/xr/XR.svelte +1 -1
  56. package/dist/draw.d.ts +13 -0
  57. package/dist/draw.js +428 -0
  58. package/dist/ecs/traits.d.ts +31 -13
  59. package/dist/ecs/traits.js +25 -8
  60. package/dist/geometry.js +3 -0
  61. package/dist/hooks/useDrawAPI.svelte.js +61 -24
  62. package/dist/hooks/useDrawService.svelte.d.ts +12 -0
  63. package/dist/hooks/useDrawService.svelte.js +240 -0
  64. package/dist/hooks/usePointcloudObjects.svelte.js +7 -2
  65. package/dist/hooks/usePointclouds.svelte.js +7 -2
  66. package/dist/hooks/useSettings.svelte.d.ts +3 -2
  67. package/dist/hooks/useSettings.svelte.js +2 -2
  68. package/dist/hooks/useWorldState.svelte.js +5 -52
  69. package/dist/index.d.ts +9 -1
  70. package/dist/index.js +10 -1
  71. package/dist/lib.d.ts +2 -0
  72. package/dist/lib.js +2 -0
  73. package/dist/loaders/pcd/index.d.ts +1 -1
  74. package/dist/loaders/pcd/messages.d.ts +2 -2
  75. package/dist/loaders/pcd/worker.inline.d.ts +1 -1
  76. package/dist/loaders/pcd/worker.inline.js +229 -187
  77. package/dist/loaders/pcd/worker.js +2 -2
  78. package/dist/metadata.d.ts +9 -15
  79. package/dist/metadata.js +45 -9
  80. package/dist/plugins/bvh.svelte.js +6 -2
  81. package/dist/snapshot.d.ts +3 -9
  82. package/dist/snapshot.js +11 -204
  83. package/dist/three/InstancedArrows/InstancedArrows.js +3 -2
  84. package/package.json +14 -11
  85. package/dist/components/Lasso/Tool.svelte +0 -108
  86. package/dist/components/xr/Hands.svelte +0 -23
  87. package/dist/components/xr/Hands.svelte.d.ts +0 -18
@@ -0,0 +1,294 @@
1
+ <script lang="ts">
2
+ import type { ShapecastCallbacks } from 'three-mesh-bvh'
3
+
4
+ import { useThrelte } from '@threlte/core'
5
+ import earcut from 'earcut'
6
+ import { Not } from 'koota'
7
+ import { Box3, Triangle, Vector3 } from 'three'
8
+
9
+ import { createBufferGeometry } from '../../attribute'
10
+ import { traits, useQuery, useWorld } from '../../ecs'
11
+ import { useCameraControls } from '../../hooks/useControls.svelte'
12
+
13
+ import Debug from './Debug.svelte'
14
+ import * as selectionTraits from './traits'
15
+ import { getTriangleBoxesFromIndices, getTriangleFromIndex, raycast } from './utils'
16
+
17
+ interface Props {
18
+ active?: boolean
19
+ debug?: boolean
20
+ }
21
+
22
+ let { active = false, debug = false }: Props = $props()
23
+
24
+ const world = useWorld()
25
+ const controls = useCameraControls()
26
+ const { scene, dom, camera } = useThrelte()
27
+
28
+ const box3 = new Box3()
29
+ const min = new Vector3()
30
+ const max = new Vector3()
31
+
32
+ const triangle = new Triangle()
33
+ const triangleBox = new Box3()
34
+
35
+ let frameScheduled = false
36
+ let drawing = false
37
+
38
+ const onpointerdown = (event: PointerEvent) => {
39
+ if (!event.shiftKey || !active) return
40
+
41
+ const { x, y } = raycast(event, camera.current)
42
+
43
+ drawing = true
44
+
45
+ world.spawn(
46
+ traits.LinePositions(new Float32Array([x, y, 0])),
47
+ selectionTraits.StartPoint({ x, y }),
48
+ traits.LineWidth(1.5),
49
+ traits.RenderOrder(999),
50
+ traits.Material({ depthTest: false }),
51
+ traits.Color({ r: 1, g: 0, b: 0 }),
52
+ selectionTraits.Box({ minX: x, minY: y, maxX: x, maxY: y }),
53
+ selectionTraits.Ellipse
54
+ )
55
+
56
+ if (controls.current) {
57
+ controls.current.enabled = false
58
+ }
59
+ }
60
+
61
+ const onpointermove = (event: PointerEvent) => {
62
+ if (!drawing || !active) return
63
+
64
+ let ellipse = world.query(selectionTraits.Ellipse).at(-1)
65
+
66
+ if (!ellipse) return
67
+
68
+ if (frameScheduled) return
69
+
70
+ frameScheduled = true
71
+
72
+ /**
73
+ * pointermove can execute at a rate much higher than screen
74
+ * refresh, creating huge polygon vertex counts, so we cap it.
75
+ */
76
+ requestAnimationFrame(() => {
77
+ frameScheduled = false
78
+
79
+ const { x, y } = raycast(event, camera.current)
80
+ const positions = ellipse.get(traits.LinePositions)
81
+ const startPoint = ellipse.get(selectionTraits.StartPoint)
82
+ const box = ellipse.get(selectionTraits.Box)
83
+
84
+ if (!positions || !box || !startPoint) return
85
+
86
+ let minX = startPoint.x
87
+ let minY = startPoint.y
88
+ let maxX = startPoint.x
89
+ let maxY = startPoint.y
90
+
91
+ if (x < minX) minX = x
92
+ else if (x > maxX) maxX = x
93
+
94
+ if (y < minY) minY = y
95
+ else if (y > maxY) maxY = y
96
+
97
+ const nextPositions = ellipsePoints(minX, maxX, minY, maxY, 100)
98
+
99
+ ellipse.set(traits.LinePositions, new Float32Array(nextPositions))
100
+ ellipse.set(selectionTraits.Box, { minX, minY, maxX, maxY })
101
+ })
102
+ }
103
+
104
+ const ellipsePoints = (
105
+ minX: number,
106
+ maxX: number,
107
+ minY: number,
108
+ maxY: number,
109
+ numPoints: number
110
+ ): Float32Array => {
111
+ const cx = (minX + maxX) / 2
112
+ const cy = (minY + maxY) / 2
113
+ const rx = (maxX - minX) / 2
114
+ const ry = (maxY - minY) / 2
115
+
116
+ const points = new Float32Array(numPoints * 3)
117
+
118
+ for (let i = 0; i < numPoints; i++) {
119
+ const t = (i / numPoints) * 2 * Math.PI
120
+
121
+ points[i * 3] = cx + rx * Math.cos(t)
122
+ points[i * 3 + 1] = cy + ry * Math.sin(t)
123
+ points[i * 3 + 2] = 0
124
+ }
125
+
126
+ return points
127
+ }
128
+
129
+ const onpointerleave = () => {
130
+ if (!drawing || !active) return
131
+
132
+ onpointerup()
133
+ }
134
+
135
+ const onpointerup = () => {
136
+ if (!drawing || !active) return
137
+
138
+ drawing = false
139
+
140
+ let ellipse = world.query(selectionTraits.Ellipse).at(-1)
141
+
142
+ if (!ellipse) return
143
+
144
+ let positions = ellipse.get(traits.LinePositions)
145
+
146
+ if (!positions) return
147
+
148
+ if (controls.current) {
149
+ controls.current.enabled = true
150
+ }
151
+
152
+ const indices = earcut(positions, undefined, 3)
153
+ if (debug) {
154
+ ellipse.add(selectionTraits.Indices(new Uint16Array(indices)))
155
+ }
156
+
157
+ const boxes: selectionTraits.AABB[] = getTriangleBoxesFromIndices(indices, positions)
158
+ if (debug) {
159
+ ellipse.add(selectionTraits.Boxes(boxes))
160
+ }
161
+
162
+ const ellipseBox = ellipse.get(selectionTraits.Box)
163
+
164
+ if (!ellipseBox) return
165
+
166
+ min.set(ellipseBox.minX, ellipseBox.minY, Number.NEGATIVE_INFINITY)
167
+ max.set(ellipseBox.maxX, ellipseBox.maxY, Number.POSITIVE_INFINITY)
168
+ box3.set(min, max)
169
+
170
+ const enclosedPoints: number[] = []
171
+
172
+ for (const pointsEntity of world.query(
173
+ traits.Points,
174
+ traits.SelectToolInteractionLayer,
175
+ Not(selectionTraits.SelectionEnclosedPoints)
176
+ )) {
177
+ const geometry = pointsEntity.get(traits.BufferGeometry)
178
+
179
+ if (!geometry) return
180
+
181
+ const points = scene.getObjectByName(pointsEntity as unknown as string)
182
+
183
+ if (!points) {
184
+ return
185
+ }
186
+
187
+ geometry.boundsTree?.shapecast({
188
+ intersectsBounds: (box) => {
189
+ return box.intersectsBox(box3)
190
+ },
191
+
192
+ intersectsPoint: (point: Vector3) => {
193
+ for (let i = 0, j = 0, l = indices.length; i < l; i += 3, j += 1) {
194
+ const { minX, minY, maxX, maxY } = boxes[j]
195
+
196
+ min.set(minX, minY, Number.NEGATIVE_INFINITY)
197
+ max.set(maxX, maxY, Number.POSITIVE_INFINITY)
198
+ triangleBox.set(min, max)
199
+
200
+ if (triangleBox.containsPoint(point)) {
201
+ getTriangleFromIndex(i, indices, positions, triangle)
202
+
203
+ if (triangle.containsPoint(point)) {
204
+ enclosedPoints.push(point.x, point.y, point.z)
205
+ }
206
+ }
207
+ }
208
+ },
209
+ // intersectsPoint is not yet in typedef, this can be removed when it is added
210
+ } as ShapecastCallbacks)
211
+ }
212
+
213
+ const ellipseResultGeometry = createBufferGeometry(new Float32Array(enclosedPoints))
214
+
215
+ world.spawn(
216
+ traits.Name('Ellipse result'),
217
+ traits.BufferGeometry(ellipseResultGeometry),
218
+ traits.Color({ r: 1, g: 0, b: 0 }),
219
+ traits.RenderOrder(999),
220
+ traits.Material({ depthTest: false }),
221
+ traits.Points,
222
+ traits.Removable,
223
+ selectionTraits.SelectionEnclosedPoints,
224
+ selectionTraits.PointsCapturedBy(ellipse)
225
+ )
226
+ }
227
+
228
+ const onkeydown = (event: KeyboardEvent) => {
229
+ if (event.key === 'Shift') {
230
+ dom.style.cursor = 'crosshair'
231
+ }
232
+ }
233
+
234
+ const onkeyup = (event: KeyboardEvent) => {
235
+ if (event.key === 'Shift') {
236
+ dom.style.removeProperty('cursor')
237
+ }
238
+ }
239
+
240
+ $effect(() => {
241
+ globalThis.addEventListener('keydown', onkeydown)
242
+ globalThis.addEventListener('keyup', onkeyup)
243
+ dom.addEventListener('pointerdown', onpointerdown)
244
+ dom.addEventListener('pointermove', onpointermove)
245
+ dom.addEventListener('pointerup', onpointerup)
246
+ dom.addEventListener('pointerleave', onpointerleave)
247
+
248
+ return () => {
249
+ globalThis.removeEventListener('keydown', onkeydown)
250
+ globalThis.removeEventListener('keyup', onkeyup)
251
+ dom.removeEventListener('pointerdown', onpointerdown)
252
+ dom.removeEventListener('pointermove', onpointermove)
253
+ dom.removeEventListener('pointerup', onpointerup)
254
+ dom.removeEventListener('pointerleave', onpointerleave)
255
+ }
256
+ })
257
+
258
+ const ellipses = useQuery(selectionTraits.Ellipse)
259
+
260
+ $effect(() => {
261
+ if (!controls.current) return
262
+
263
+ const currentControls = controls.current
264
+
265
+ const { minPolarAngle, maxPolarAngle } = currentControls
266
+
267
+ // Locks the camera to top down while this component is mounted
268
+ currentControls.polarAngle = 0
269
+ currentControls.minPolarAngle = 0
270
+ currentControls.maxPolarAngle = 0
271
+
272
+ return () => {
273
+ currentControls.minPolarAngle = minPolarAngle
274
+ currentControls.maxPolarAngle = maxPolarAngle
275
+ }
276
+ })
277
+
278
+ // On unmount, destroy all lasso related entities
279
+ $effect(() => {
280
+ return () => {
281
+ for (const entity of world.query(selectionTraits.SelectionEnclosedPoints)) {
282
+ if (world.has(entity)) {
283
+ entity.destroy()
284
+ }
285
+ }
286
+ }
287
+ })
288
+ </script>
289
+
290
+ {#if debug}
291
+ {#each ellipses.current as ellipse (ellipse)}
292
+ <Debug selection={ellipse} />
293
+ {/each}
294
+ {/if}
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ active?: boolean;
3
+ debug?: boolean;
4
+ }
5
+ declare const Ellipse: import("svelte").Component<Props, {}, "">;
6
+ type Ellipse = ReturnType<typeof Ellipse>;
7
+ export default Ellipse;
@@ -4,20 +4,22 @@
4
4
  import { useThrelte } from '@threlte/core'
5
5
  import earcut from 'earcut'
6
6
  import { Not } from 'koota'
7
- import { Box3, Plane, Raycaster, Triangle, Vector2, Vector3 } from 'three'
7
+ import { Box3, Triangle, Vector3 } from 'three'
8
8
 
9
9
  import { createBufferGeometry } from '../../attribute'
10
10
  import { traits, useQuery, useWorld } from '../../ecs'
11
11
  import { useCameraControls } from '../../hooks/useControls.svelte'
12
12
 
13
13
  import Debug from './Debug.svelte'
14
- import * as lassoTraits from './traits'
14
+ import * as selectionTraits from './traits'
15
+ import { getTriangleBoxesFromIndices, getTriangleFromIndex, raycast } from './utils'
15
16
 
16
17
  interface Props {
18
+ active?: boolean
17
19
  debug?: boolean
18
20
  }
19
21
 
20
- let { debug = false }: Props = $props()
22
+ let { active = false, debug = false }: Props = $props()
21
23
 
22
24
  const world = useWorld()
23
25
  const controls = useCameraControls()
@@ -29,44 +31,26 @@
29
31
 
30
32
  const triangle = new Triangle()
31
33
  const triangleBox = new Box3()
32
- const a = new Vector3()
33
- const b = new Vector3()
34
- const c = new Vector3()
35
34
 
36
35
  let frameScheduled = false
37
36
  let drawing = false
38
37
 
39
- const raycaster = new Raycaster()
40
- const mouse = new Vector2()
41
- const plane = new Plane(new Vector3(0, 0, 1), 0)
42
- const point = new Vector3()
43
-
44
- const raycast = (event: PointerEvent) => {
45
- const element = event.target as HTMLElement
46
- const rect = element.getBoundingClientRect()
47
- mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
48
- mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
49
-
50
- raycaster.setFromCamera(mouse, camera.current)
51
- raycaster.ray.intersectPlane(plane, point)
52
- return point
53
- }
54
-
55
38
  const onpointerdown = (event: PointerEvent) => {
56
- if (!event.shiftKey) return
39
+ if (!event.shiftKey || !active) return
57
40
 
58
- const { x, y } = raycast(event)
41
+ const { x, y } = raycast(event, camera.current)
59
42
 
60
43
  drawing = true
61
44
 
62
45
  world.spawn(
63
46
  traits.LinePositions(new Float32Array([x, y, 0])),
64
47
  traits.LineWidth(1.5),
48
+ traits.ScreenSpace,
65
49
  traits.RenderOrder(999),
66
50
  traits.Material({ depthTest: false }),
67
51
  traits.Color({ r: 1, g: 0, b: 0 }),
68
- lassoTraits.Box({ minX: x, minY: y, maxX: x, maxY: y }),
69
- lassoTraits.Lasso
52
+ selectionTraits.Box({ minX: x, minY: y, maxX: x, maxY: y }),
53
+ selectionTraits.Lasso
70
54
  )
71
55
 
72
56
  if (controls.current) {
@@ -75,9 +59,9 @@
75
59
  }
76
60
 
77
61
  const onpointermove = (event: PointerEvent) => {
78
- if (!drawing) return
62
+ if (!drawing || !active) return
79
63
 
80
- let lasso = world.query(lassoTraits.Lasso).at(-1)
64
+ let lasso = world.query(selectionTraits.Lasso).at(-1)
81
65
 
82
66
  if (!lasso) return
83
67
 
@@ -92,9 +76,9 @@
92
76
  requestAnimationFrame(() => {
93
77
  frameScheduled = false
94
78
 
95
- const { x, y } = raycast(event)
79
+ const { x, y } = raycast(event, camera.current)
96
80
  const positions = lasso.get(traits.LinePositions)
97
- const box = lasso.get(lassoTraits.Box)
81
+ const box = lasso.get(selectionTraits.Box)
98
82
 
99
83
  if (!positions || !box) return
100
84
 
@@ -110,22 +94,22 @@
110
94
  if (y < box.minY) box.minY = y
111
95
  else if (y > box.maxY) box.maxY = y
112
96
 
113
- lasso.set(lassoTraits.Box, box)
97
+ lasso.set(selectionTraits.Box, box)
114
98
  })
115
99
  }
116
100
 
117
101
  const onpointerleave = () => {
118
- if (!drawing) return
102
+ if (!drawing || !active) return
119
103
 
120
104
  onpointerup()
121
105
  }
122
106
 
123
107
  const onpointerup = () => {
124
- if (!drawing) return
108
+ if (!drawing || !active) return
125
109
 
126
110
  drawing = false
127
111
 
128
- let lasso = world.query(lassoTraits.Lasso).at(-1)
112
+ let lasso = world.query(selectionTraits.Lasso).at(-1)
129
113
 
130
114
  if (!lasso) return
131
115
 
@@ -149,31 +133,15 @@
149
133
 
150
134
  const indices = earcut(positions, undefined, 3)
151
135
  if (debug) {
152
- lasso.add(lassoTraits.Indices(new Uint16Array(indices)))
136
+ lasso.add(selectionTraits.Indices(new Uint16Array(indices)))
153
137
  }
154
138
 
155
- const getTriangleFromIndex = (i: number, triangle: Triangle) => {
156
- const stride = 3
157
- const ia = indices[i + 0] * stride
158
- const ib = indices[i + 1] * stride
159
- const ic = indices[i + 2] * stride
160
- a.set(positions[ia + 0], positions[ia + 1], positions[ia + 2])
161
- b.set(positions[ib + 0], positions[ib + 1], positions[ib + 2])
162
- c.set(positions[ic + 0], positions[ic + 1], positions[ic + 2])
163
- triangle.set(a, b, c)
164
- }
165
-
166
- const boxes: lassoTraits.AABB[] = []
167
- for (let i = 0, l = indices.length; i < l; i += 3) {
168
- getTriangleFromIndex(i, triangle)
169
- box3.setFromPoints([triangle.a, triangle.b, triangle.c])
170
- boxes.push({ minX: box3.min.x, minY: box3.min.y, maxX: box3.max.x, maxY: box3.max.y })
171
- }
139
+ const boxes: selectionTraits.AABB[] = getTriangleBoxesFromIndices(indices, positions)
172
140
  if (debug) {
173
- lasso.add(lassoTraits.Boxes(boxes))
141
+ lasso.add(selectionTraits.Boxes(boxes))
174
142
  }
175
143
 
176
- const lassoBox = lasso.get(lassoTraits.Box)
144
+ const lassoBox = lasso.get(selectionTraits.Box)
177
145
 
178
146
  if (!lassoBox) return
179
147
 
@@ -183,7 +151,11 @@
183
151
 
184
152
  const enclosedPoints: number[] = []
185
153
 
186
- for (const pointsEntity of world.query(traits.Points, Not(lassoTraits.LassoEnclosedPoints))) {
154
+ for (const pointsEntity of world.query(
155
+ traits.Points,
156
+ traits.SelectToolInteractionLayer,
157
+ Not(selectionTraits.SelectionEnclosedPoints)
158
+ )) {
187
159
  const geometry = pointsEntity.get(traits.BufferGeometry)
188
160
 
189
161
  if (!geometry) return
@@ -208,7 +180,7 @@
208
180
  triangleBox.set(min, max)
209
181
 
210
182
  if (triangleBox.containsPoint(point)) {
211
- getTriangleFromIndex(i, triangle)
183
+ getTriangleFromIndex(i, indices, positions, triangle)
212
184
 
213
185
  if (triangle.containsPoint(point)) {
214
186
  enclosedPoints.push(point.x, point.y, point.z)
@@ -230,8 +202,8 @@
230
202
  traits.Material({ depthTest: false }),
231
203
  traits.Points,
232
204
  traits.Removable,
233
- lassoTraits.LassoEnclosedPoints,
234
- lassoTraits.PointsCapturedBy(lasso)
205
+ selectionTraits.SelectionEnclosedPoints,
206
+ selectionTraits.PointsCapturedBy(lasso)
235
207
  )
236
208
  }
237
209
 
@@ -265,7 +237,7 @@
265
237
  }
266
238
  })
267
239
 
268
- const lassos = useQuery(lassoTraits.Lasso)
240
+ const lassos = useQuery(selectionTraits.Lasso)
269
241
 
270
242
  $effect(() => {
271
243
  if (!controls.current) return
@@ -288,7 +260,7 @@
288
260
  // On unmount, destroy all lasso related entities
289
261
  $effect(() => {
290
262
  return () => {
291
- for (const entity of world.query(lassoTraits.LassoEnclosedPoints)) {
263
+ for (const entity of world.query(selectionTraits.SelectionEnclosedPoints)) {
292
264
  if (world.has(entity)) {
293
265
  entity.destroy()
294
266
  }
@@ -299,6 +271,6 @@
299
271
 
300
272
  {#if debug}
301
273
  {#each lassos.current as lasso (lasso)}
302
- <Debug {lasso} />
274
+ <Debug selection={lasso} />
303
275
  {/each}
304
276
  {/if}
@@ -1,4 +1,5 @@
1
1
  interface Props {
2
+ active?: boolean;
2
3
  debug?: boolean;
3
4
  }
4
5
  declare const Lasso: import("svelte").Component<Props, {}, "">;
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+
4
+ import { useThrelte } from '@threlte/core'
5
+ import { Portal } from '@threlte/extras'
6
+ import { ElementRect } from 'runed'
7
+
8
+ import DashboardButton from '../overlay/dashboard/Button.svelte'
9
+ import { useSettings } from '../../hooks/useSettings.svelte'
10
+
11
+ import Popover from '../overlay/Popover.svelte'
12
+ import ToggleGroup from '../overlay/ToggleGroup.svelte'
13
+ import Ellipse from './Ellipse.svelte'
14
+ import Lasso from './Lasso.svelte'
15
+ import { provideSelectionPlugin } from './useSelectionPlugin.svelte'
16
+
17
+ interface Props {
18
+ /** Whether to auto-enable lasso mode when the component mounts */
19
+ enabled?: boolean
20
+ children?: Snippet
21
+ }
22
+
23
+ type SelectionType = 'lasso' | 'ellipse'
24
+
25
+ let { enabled = false, children }: Props = $props()
26
+
27
+ const { dom } = useThrelte()
28
+ const settings = useSettings()
29
+ const isSelectionMode = $derived(settings.current.interactionMode === 'select')
30
+
31
+ provideSelectionPlugin()
32
+ let selectionType = $state<SelectionType>('lasso')
33
+
34
+ $effect(() => {
35
+ if (isSelectionMode) {
36
+ settings.current.cameraMode = 'orthographic'
37
+ }
38
+ })
39
+
40
+ $effect(() => {
41
+ if (enabled) {
42
+ settings.current.interactionMode = 'select'
43
+ }
44
+ })
45
+
46
+ const rect = new ElementRect(() => dom)
47
+ </script>
48
+
49
+ <Portal id="dashboard">
50
+ <fieldset>
51
+ <div class="flex">
52
+ <DashboardButton
53
+ active={isSelectionMode}
54
+ icon="selection-drag"
55
+ description="{isSelectionMode ? 'Disable' : 'Enable'} selection"
56
+ onclick={() => {
57
+ settings.current.interactionMode = isSelectionMode ? 'navigate' : 'select'
58
+ }}
59
+ />
60
+ <Popover>
61
+ {#snippet trigger(triggerProps)}
62
+ <DashboardButton
63
+ {...triggerProps}
64
+ active={isSelectionMode}
65
+ class="border-l-0"
66
+ icon="filter-sliders"
67
+ description="Selection settings"
68
+ />
69
+ {/snippet}
70
+
71
+ <div class="border-medium m-2 border bg-white p-2 text-xs">
72
+ <div class="flex items-center gap-2">
73
+ Selection type
74
+ <ToggleGroup
75
+ options={[
76
+ { label: 'Lasso', selected: selectionType === 'lasso' },
77
+ { label: 'Ellipse', selected: selectionType === 'ellipse' },
78
+ ]}
79
+ onSelect={(details) => {
80
+ selectionType = details.includes('Lasso') ? 'lasso' : 'ellipse'
81
+ }}
82
+ />
83
+ </div>
84
+ </div>
85
+ </Popover>
86
+ </div>
87
+ </fieldset>
88
+ </Portal>
89
+
90
+ {#if isSelectionMode && rect.height > 0 && rect.width > 0}
91
+ <Ellipse active={selectionType === 'ellipse'} />
92
+ <Lasso active={selectionType === 'lasso'} />
93
+ {@render children?.()}
94
+ {/if}
@@ -1,8 +1,8 @@
1
+ import type { Snippet } from 'svelte';
1
2
  interface Props {
2
3
  /** Whether to auto-enable lasso mode when the component mounts */
3
4
  enabled?: boolean;
4
- /** Fires when the user has committed to a lasso selection */
5
- onSelection: (pcd: Blob) => void;
5
+ children?: Snippet;
6
6
  }
7
7
  declare const Tool: import("svelte").Component<Props, {}, "">;
8
8
  type Tool = ReturnType<typeof Tool>;
@@ -1,8 +1,9 @@
1
1
  export declare const Lasso: import("koota").Trait<() => boolean>;
2
- export declare const LassoEnclosedPoints: import("koota").Trait<() => boolean>;
2
+ export declare const Ellipse: import("koota").Trait<() => boolean>;
3
+ export declare const SelectionEnclosedPoints: import("koota").Trait<() => boolean>;
3
4
  /**
4
5
  * Captured points are removable, so we want to also destroy
5
- * the source lasso every time a user deletes one.
6
+ * the source selection every time a user deletes one.
6
7
  */
7
8
  export declare const PointsCapturedBy: import("koota").Relation<import("koota").Trait<Record<string, never>>>;
8
9
  export interface AABB {
@@ -11,11 +12,19 @@ export interface AABB {
11
12
  maxX: number;
12
13
  maxY: number;
13
14
  }
15
+ export interface Point {
16
+ x: number;
17
+ y: number;
18
+ }
14
19
  export declare const Box: import("koota").Trait<{
15
20
  minX: number;
16
21
  minY: number;
17
22
  maxX: number;
18
23
  maxY: number;
19
24
  }>;
25
+ export declare const StartPoint: import("koota").Trait<{
26
+ x: number;
27
+ y: number;
28
+ }>;
20
29
  export declare const Indices: import("koota").Trait<() => Uint16Array<ArrayBuffer>>;
21
30
  export declare const Boxes: import("koota").Trait<() => AABB[]>;
@@ -1,9 +1,10 @@
1
1
  import { relation, trait } from 'koota';
2
2
  export const Lasso = trait(() => true);
3
- export const LassoEnclosedPoints = trait(() => true);
3
+ export const Ellipse = trait(() => true);
4
+ export const SelectionEnclosedPoints = trait(() => true);
4
5
  /**
5
6
  * Captured points are removable, so we want to also destroy
6
- * the source lasso every time a user deletes one.
7
+ * the source selection every time a user deletes one.
7
8
  */
8
9
  export const PointsCapturedBy = relation({ autoDestroy: 'target' });
9
10
  export const Box = trait({
@@ -12,5 +13,9 @@ export const Box = trait({
12
13
  maxX: 0,
13
14
  maxY: 0,
14
15
  });
16
+ export const StartPoint = trait({
17
+ x: 0,
18
+ y: 0,
19
+ });
15
20
  export const Indices = trait(() => new Uint16Array());
16
21
  export const Boxes = trait(() => []);
@@ -0,0 +1,8 @@
1
+ import type { QueryResult, Trait } from 'koota';
2
+ interface SelectionPluginContext {
3
+ current: QueryResult<[Trait<() => boolean>]>;
4
+ clearSelections: () => void;
5
+ }
6
+ export declare const provideSelectionPlugin: () => SelectionPluginContext;
7
+ export declare const useSelectionPlugin: () => SelectionPluginContext;
8
+ export {};