@viamrobotics/motion-tools 1.15.8 → 1.16.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.
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  @component
3
3
 
4
- Shows all steps for querying points within a lasso selection
4
+ Shows all steps for querying points within a selection
5
5
  -->
6
6
  <script lang="ts">
7
7
  import type { Entity } from 'koota'
@@ -11,22 +11,22 @@ Shows all steps for querying points within a lasso selection
11
11
 
12
12
  import { traits, useTrait } from '../../ecs'
13
13
 
14
- import * as lassoTraits from './traits'
14
+ import * as selectionTraits from './traits'
15
15
 
16
16
  const box3 = new Box3()
17
17
  const min = new Vector3()
18
18
  const max = new Vector3()
19
19
 
20
20
  interface Props {
21
- lasso: Entity
21
+ selection: Entity
22
22
  }
23
23
 
24
- let { lasso }: Props = $props()
24
+ let { selection }: Props = $props()
25
25
 
26
- const indices = useTrait(() => lasso, lassoTraits.Indices)
27
- const positions = useTrait(() => lasso, traits.LinePositions)
28
- const box = useTrait(() => lasso, lassoTraits.Box)
29
- const boxes = useTrait(() => lasso, lassoTraits.Boxes)
26
+ const indices = useTrait(() => selection, selectionTraits.Indices)
27
+ const positions = useTrait(() => selection, traits.LinePositions)
28
+ const box = useTrait(() => selection, selectionTraits.Box)
29
+ const boxes = useTrait(() => selection, selectionTraits.Boxes)
30
30
 
31
31
  const geometry = new BufferGeometry()
32
32
 
@@ -1,8 +1,8 @@
1
1
  import type { Entity } from 'koota';
2
2
  interface Props {
3
- lasso: Entity;
3
+ selection: Entity;
4
4
  }
5
- /** Shows all steps for querying points within a lasso selection */
5
+ /** Shows all steps for querying points within a selection */
6
6
  declare const Debug: import("svelte").Component<Props, {}, "">;
7
7
  type Debug = ReturnType<typeof Debug>;
8
8
  export default Debug;
@@ -0,0 +1,293 @@
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
+ Not(selectionTraits.SelectionEnclosedPoints)
175
+ )) {
176
+ const geometry = pointsEntity.get(traits.BufferGeometry)
177
+
178
+ if (!geometry) return
179
+
180
+ const points = scene.getObjectByName(pointsEntity as unknown as string)
181
+
182
+ if (!points) {
183
+ return
184
+ }
185
+
186
+ geometry.boundsTree?.shapecast({
187
+ intersectsBounds: (box) => {
188
+ return box.intersectsBox(box3)
189
+ },
190
+
191
+ intersectsPoint: (point: Vector3) => {
192
+ for (let i = 0, j = 0, l = indices.length; i < l; i += 3, j += 1) {
193
+ const { minX, minY, maxX, maxY } = boxes[j]
194
+
195
+ min.set(minX, minY, Number.NEGATIVE_INFINITY)
196
+ max.set(maxX, maxY, Number.POSITIVE_INFINITY)
197
+ triangleBox.set(min, max)
198
+
199
+ if (triangleBox.containsPoint(point)) {
200
+ getTriangleFromIndex(i, indices, positions, triangle)
201
+
202
+ if (triangle.containsPoint(point)) {
203
+ enclosedPoints.push(point.x, point.y, point.z)
204
+ }
205
+ }
206
+ }
207
+ },
208
+ // intersectsPoint is not yet in typedef, this can be removed when it is added
209
+ } as ShapecastCallbacks)
210
+ }
211
+
212
+ const ellipseResultGeometry = createBufferGeometry(new Float32Array(enclosedPoints))
213
+
214
+ world.spawn(
215
+ traits.Name('Ellipse result'),
216
+ traits.BufferGeometry(ellipseResultGeometry),
217
+ traits.Color({ r: 1, g: 0, b: 0 }),
218
+ traits.RenderOrder(999),
219
+ traits.Material({ depthTest: false }),
220
+ traits.Points,
221
+ traits.Removable,
222
+ selectionTraits.SelectionEnclosedPoints,
223
+ selectionTraits.PointsCapturedBy(ellipse)
224
+ )
225
+ }
226
+
227
+ const onkeydown = (event: KeyboardEvent) => {
228
+ if (event.key === 'Shift') {
229
+ dom.style.cursor = 'crosshair'
230
+ }
231
+ }
232
+
233
+ const onkeyup = (event: KeyboardEvent) => {
234
+ if (event.key === 'Shift') {
235
+ dom.style.removeProperty('cursor')
236
+ }
237
+ }
238
+
239
+ $effect(() => {
240
+ globalThis.addEventListener('keydown', onkeydown)
241
+ globalThis.addEventListener('keyup', onkeyup)
242
+ dom.addEventListener('pointerdown', onpointerdown)
243
+ dom.addEventListener('pointermove', onpointermove)
244
+ dom.addEventListener('pointerup', onpointerup)
245
+ dom.addEventListener('pointerleave', onpointerleave)
246
+
247
+ return () => {
248
+ globalThis.removeEventListener('keydown', onkeydown)
249
+ globalThis.removeEventListener('keyup', onkeyup)
250
+ dom.removeEventListener('pointerdown', onpointerdown)
251
+ dom.removeEventListener('pointermove', onpointermove)
252
+ dom.removeEventListener('pointerup', onpointerup)
253
+ dom.removeEventListener('pointerleave', onpointerleave)
254
+ }
255
+ })
256
+
257
+ const ellipses = useQuery(selectionTraits.Ellipse)
258
+
259
+ $effect(() => {
260
+ if (!controls.current) return
261
+
262
+ const currentControls = controls.current
263
+
264
+ const { minPolarAngle, maxPolarAngle } = currentControls
265
+
266
+ // Locks the camera to top down while this component is mounted
267
+ currentControls.polarAngle = 0
268
+ currentControls.minPolarAngle = 0
269
+ currentControls.maxPolarAngle = 0
270
+
271
+ return () => {
272
+ currentControls.minPolarAngle = minPolarAngle
273
+ currentControls.maxPolarAngle = maxPolarAngle
274
+ }
275
+ })
276
+
277
+ // On unmount, destroy all lasso related entities
278
+ $effect(() => {
279
+ return () => {
280
+ for (const entity of world.query(selectionTraits.SelectionEnclosedPoints)) {
281
+ if (world.has(entity)) {
282
+ entity.destroy()
283
+ }
284
+ }
285
+ }
286
+ })
287
+ </script>
288
+
289
+ {#if debug}
290
+ {#each ellipses.current as ellipse (ellipse)}
291
+ <Debug selection={ellipse} />
292
+ {/each}
293
+ {/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,33 +31,14 @@
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
 
@@ -65,8 +48,8 @@
65
48
  traits.RenderOrder(999),
66
49
  traits.Material({ depthTest: false }),
67
50
  traits.Color({ r: 1, g: 0, b: 0 }),
68
- lassoTraits.Box({ minX: x, minY: y, maxX: x, maxY: y }),
69
- lassoTraits.Lasso
51
+ selectionTraits.Box({ minX: x, minY: y, maxX: x, maxY: y }),
52
+ selectionTraits.Lasso
70
53
  )
71
54
 
72
55
  if (controls.current) {
@@ -75,9 +58,9 @@
75
58
  }
76
59
 
77
60
  const onpointermove = (event: PointerEvent) => {
78
- if (!drawing) return
61
+ if (!drawing || !active) return
79
62
 
80
- let lasso = world.query(lassoTraits.Lasso).at(-1)
63
+ let lasso = world.query(selectionTraits.Lasso).at(-1)
81
64
 
82
65
  if (!lasso) return
83
66
 
@@ -92,9 +75,9 @@
92
75
  requestAnimationFrame(() => {
93
76
  frameScheduled = false
94
77
 
95
- const { x, y } = raycast(event)
78
+ const { x, y } = raycast(event, camera.current)
96
79
  const positions = lasso.get(traits.LinePositions)
97
- const box = lasso.get(lassoTraits.Box)
80
+ const box = lasso.get(selectionTraits.Box)
98
81
 
99
82
  if (!positions || !box) return
100
83
 
@@ -110,22 +93,22 @@
110
93
  if (y < box.minY) box.minY = y
111
94
  else if (y > box.maxY) box.maxY = y
112
95
 
113
- lasso.set(lassoTraits.Box, box)
96
+ lasso.set(selectionTraits.Box, box)
114
97
  })
115
98
  }
116
99
 
117
100
  const onpointerleave = () => {
118
- if (!drawing) return
101
+ if (!drawing || !active) return
119
102
 
120
103
  onpointerup()
121
104
  }
122
105
 
123
106
  const onpointerup = () => {
124
- if (!drawing) return
107
+ if (!drawing || !active) return
125
108
 
126
109
  drawing = false
127
110
 
128
- let lasso = world.query(lassoTraits.Lasso).at(-1)
111
+ let lasso = world.query(selectionTraits.Lasso).at(-1)
129
112
 
130
113
  if (!lasso) return
131
114
 
@@ -149,31 +132,15 @@
149
132
 
150
133
  const indices = earcut(positions, undefined, 3)
151
134
  if (debug) {
152
- lasso.add(lassoTraits.Indices(new Uint16Array(indices)))
135
+ lasso.add(selectionTraits.Indices(new Uint16Array(indices)))
153
136
  }
154
137
 
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
- }
138
+ const boxes: selectionTraits.AABB[] = getTriangleBoxesFromIndices(indices, positions)
172
139
  if (debug) {
173
- lasso.add(lassoTraits.Boxes(boxes))
140
+ lasso.add(selectionTraits.Boxes(boxes))
174
141
  }
175
142
 
176
- const lassoBox = lasso.get(lassoTraits.Box)
143
+ const lassoBox = lasso.get(selectionTraits.Box)
177
144
 
178
145
  if (!lassoBox) return
179
146
 
@@ -183,7 +150,10 @@
183
150
 
184
151
  const enclosedPoints: number[] = []
185
152
 
186
- for (const pointsEntity of world.query(traits.Points, Not(lassoTraits.LassoEnclosedPoints))) {
153
+ for (const pointsEntity of world.query(
154
+ traits.Points,
155
+ Not(selectionTraits.SelectionEnclosedPoints)
156
+ )) {
187
157
  const geometry = pointsEntity.get(traits.BufferGeometry)
188
158
 
189
159
  if (!geometry) return
@@ -208,7 +178,7 @@
208
178
  triangleBox.set(min, max)
209
179
 
210
180
  if (triangleBox.containsPoint(point)) {
211
- getTriangleFromIndex(i, triangle)
181
+ getTriangleFromIndex(i, indices, positions, triangle)
212
182
 
213
183
  if (triangle.containsPoint(point)) {
214
184
  enclosedPoints.push(point.x, point.y, point.z)
@@ -230,8 +200,8 @@
230
200
  traits.Material({ depthTest: false }),
231
201
  traits.Points,
232
202
  traits.Removable,
233
- lassoTraits.LassoEnclosedPoints,
234
- lassoTraits.PointsCapturedBy(lasso)
203
+ selectionTraits.SelectionEnclosedPoints,
204
+ selectionTraits.PointsCapturedBy(lasso)
235
205
  )
236
206
  }
237
207
 
@@ -265,7 +235,7 @@
265
235
  }
266
236
  })
267
237
 
268
- const lassos = useQuery(lassoTraits.Lasso)
238
+ const lassos = useQuery(selectionTraits.Lasso)
269
239
 
270
240
  $effect(() => {
271
241
  if (!controls.current) return
@@ -288,7 +258,7 @@
288
258
  // On unmount, destroy all lasso related entities
289
259
  $effect(() => {
290
260
  return () => {
291
- for (const entity of world.query(lassoTraits.LassoEnclosedPoints)) {
261
+ for (const entity of world.query(selectionTraits.SelectionEnclosedPoints)) {
292
262
  if (world.has(entity)) {
293
263
  entity.destroy()
294
264
  }
@@ -299,6 +269,6 @@
299
269
 
300
270
  {#if debug}
301
271
  {#each lassos.current as lasso (lasso)}
302
- <Debug {lasso} />
272
+ <Debug selection={lasso} />
303
273
  {/each}
304
274
  {/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, {}, "">;
@@ -13,8 +13,11 @@
13
13
  import { createBinaryPCD } from '../../pcd'
14
14
 
15
15
  import FloatingPanel from '../overlay/FloatingPanel.svelte'
16
+ import Popover from '../overlay/Popover.svelte'
17
+ import ToggleGroup from '../overlay/ToggleGroup.svelte'
18
+ import Ellipse from './Ellipse.svelte'
16
19
  import Lasso from './Lasso.svelte'
17
- import * as lassoTraits from './traits'
20
+ import * as selectionTraits from './traits'
18
21
 
19
22
  interface Props {
20
23
  /** Whether to auto-enable lasso mode when the component mounts */
@@ -24,15 +27,18 @@
24
27
  onSelection: (pcd: Blob) => void
25
28
  }
26
29
 
30
+ type SelectionType = 'lasso' | 'ellipse'
31
+
27
32
  let { enabled = false, onSelection }: Props = $props()
28
33
 
29
34
  const { dom } = useThrelte()
30
35
  const world = useWorld()
31
36
  const settings = useSettings()
32
- const isLassoMode = $derived(settings.current.interactionMode === 'lasso')
37
+ const isSelectionMode = $derived(settings.current.interactionMode === 'select')
38
+ let selectionType = $state<SelectionType>('lasso')
33
39
 
34
40
  const onCommitClick = () => {
35
- const entities = world.query(lassoTraits.LassoEnclosedPoints)
41
+ const entities = world.query(selectionTraits.SelectionEnclosedPoints)
36
42
 
37
43
  const geometries: BufferGeometry[] = []
38
44
  for (const entity of entities) {
@@ -58,14 +64,14 @@
58
64
  }
59
65
 
60
66
  $effect(() => {
61
- if (isLassoMode) {
67
+ if (isSelectionMode) {
62
68
  settings.current.cameraMode = 'orthographic'
63
69
  }
64
70
  })
65
71
 
66
72
  $effect(() => {
67
73
  if (enabled) {
68
- settings.current.interactionMode = 'lasso'
74
+ settings.current.interactionMode = 'select'
69
75
  }
70
76
  })
71
77
 
@@ -74,19 +80,48 @@
74
80
 
75
81
  <Portal id="dashboard">
76
82
  <fieldset>
77
- <DashboardButton
78
- active={isLassoMode}
79
- icon="selection-drag"
80
- description="{isLassoMode ? 'Disable' : 'Enable'} lasso selection"
81
- onclick={() => {
82
- settings.current.interactionMode = isLassoMode ? 'navigate' : 'lasso'
83
- }}
84
- />
83
+ <div class="flex">
84
+ <DashboardButton
85
+ active={isSelectionMode}
86
+ icon="selection-drag"
87
+ description="{isSelectionMode ? 'Disable' : 'Enable'} selection"
88
+ onclick={() => {
89
+ settings.current.interactionMode = isSelectionMode ? 'navigate' : 'select'
90
+ }}
91
+ />
92
+ <Popover>
93
+ {#snippet trigger(triggerProps)}
94
+ <DashboardButton
95
+ {...triggerProps}
96
+ active={isSelectionMode}
97
+ class="border-l-0"
98
+ icon="filter-sliders"
99
+ description="Selection settings"
100
+ />
101
+ {/snippet}
102
+
103
+ <div class="border-medium m-2 border bg-white p-2 text-xs">
104
+ <div class="flex items-center gap-2">
105
+ Selection type
106
+ <ToggleGroup
107
+ options={[
108
+ { label: 'Lasso', selected: selectionType === 'lasso' },
109
+ { label: 'Ellipse', selected: selectionType === 'ellipse' },
110
+ ]}
111
+ onSelect={(details) => {
112
+ selectionType = details.includes('Lasso') ? 'lasso' : 'ellipse'
113
+ }}
114
+ />
115
+ </div>
116
+ </div>
117
+ </Popover>
118
+ </div>
85
119
  </fieldset>
86
120
  </Portal>
87
121
 
88
- {#if isLassoMode && rect.height > 0 && rect.width > 0}
89
- <Lasso />
122
+ {#if isSelectionMode && rect.height > 0 && rect.width > 0}
123
+ <Ellipse active={selectionType === 'ellipse'} />
124
+ <Lasso active={selectionType === 'lasso'} />
90
125
 
91
126
  <Portal id="dom">
92
127
  <FloatingPanel
@@ -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,5 @@
1
+ import { Camera, Triangle, Vector3 } from 'three';
2
+ import type * as selectionTraits from './traits';
3
+ export declare const raycast: (event: PointerEvent, camera: Camera) => Vector3;
4
+ export declare const getTriangleFromIndex: (i: number, indices: number[], positions: Float32Array, outTriangle: Triangle) => void;
5
+ export declare const getTriangleBoxesFromIndices: (indices: number[], positions: Float32Array) => selectionTraits.AABB[];
@@ -0,0 +1,38 @@
1
+ import { Box3, Camera, Plane, Raycaster, Triangle, Vector2, Vector3 } from 'three';
2
+ const raycaster = new Raycaster();
3
+ const mouse = new Vector2();
4
+ const plane = new Plane(new Vector3(0, 0, 1), 0);
5
+ const point = new Vector3();
6
+ const triangle = new Triangle();
7
+ const box3 = new Box3();
8
+ const a = new Vector3();
9
+ const b = new Vector3();
10
+ const c = new Vector3();
11
+ export const raycast = (event, camera) => {
12
+ const element = event.target;
13
+ const rect = element.getBoundingClientRect();
14
+ mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
15
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
16
+ raycaster.setFromCamera(mouse, camera);
17
+ raycaster.ray.intersectPlane(plane, point);
18
+ return point;
19
+ };
20
+ export const getTriangleFromIndex = (i, indices, positions, outTriangle) => {
21
+ const stride = 3;
22
+ const ia = indices[i + 0] * stride;
23
+ const ib = indices[i + 1] * stride;
24
+ const ic = indices[i + 2] * stride;
25
+ a.set(positions[ia + 0], positions[ia + 1], positions[ia + 2]);
26
+ b.set(positions[ib + 0], positions[ib + 1], positions[ib + 2]);
27
+ c.set(positions[ic + 0], positions[ic + 1], positions[ic + 2]);
28
+ outTriangle.set(a, b, c);
29
+ };
30
+ export const getTriangleBoxesFromIndices = (indices, positions) => {
31
+ const boxes = [];
32
+ for (let i = 0, l = indices.length; i < l; i += 3) {
33
+ getTriangleFromIndex(i, indices, positions, triangle);
34
+ box3.setFromPoints([triangle.a, triangle.b, triangle.c]);
35
+ boxes.push({ minX: box3.min.x, minY: box3.min.y, maxX: box3.max.x, maxY: box3.max.y });
36
+ }
37
+ return boxes;
38
+ };
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
- import { T } from '@threlte/core'
2
+ import { useTask, useThrelte } from '@threlte/core'
3
3
  import { Grid, useGamepad } from '@threlte/extras'
4
+ import { Hand, useHand, useXR } from '@threlte/xr'
4
5
  import { Group, Quaternion, Vector2, Vector3 } from 'three'
5
6
 
6
7
  import { useAnchors } from './useAnchors.svelte'
@@ -9,17 +10,20 @@
9
10
  const origin = useOrigin()
10
11
  const anchors = useAnchors()
11
12
 
12
- const group = new Group()
13
13
  const anchorObject = new Group()
14
14
 
15
15
  const leftPad = useGamepad({ xr: true, hand: 'left' })
16
16
  const rightPad = useGamepad({ xr: true, hand: 'right' })
17
17
 
18
- const speed = 0.05
18
+ let speed = $state(0.05)
19
19
 
20
20
  const vec2 = new Vector2()
21
21
  const target = new Vector2()
22
22
 
23
+ leftPad.squeeze.on('change', () => {
24
+ speed = leftPad.squeeze.pressed ? 0.005 : 0.05
25
+ })
26
+
23
27
  leftPad.thumbstick.on('change', ({ value }) => {
24
28
  if (typeof value === 'number') {
25
29
  return
@@ -29,6 +33,8 @@
29
33
  const [x, y, z] = origin.position
30
34
  const r = origin.rotation
31
35
 
36
+ vec2.set(z, r).lerp(target.set(z + vy * speed, r + vx * speed), 0.5)
37
+
32
38
  origin.set([x, y, z + vy * speed], r + vx * speed)
33
39
  })
34
40
 
@@ -56,19 +62,90 @@
56
62
  anchors.bindAnchorObject(anchor, anchorObject)
57
63
  })
58
64
  })
65
+
66
+ let startLeftPinchTranslation = new Vector3()
67
+ let leftPinchTranslation = new Vector3()
68
+ let startRightPinchTranslation = new Vector3()
69
+ let rightPinchTranslation = new Vector3()
70
+
71
+ const leftHand = useHand('left')
72
+ const rightHand = useHand('right')
73
+
74
+ let translating = $state(false)
75
+ let rotating = $state(false)
76
+
77
+ const { renderer } = useThrelte()
78
+ const { isPresenting } = useXR()
79
+
80
+ $effect(() => {
81
+ if (!$isPresenting) {
82
+ return
83
+ }
84
+ renderer.xr.getHand(0).addEventListener('pinchstart', () => {
85
+ if (leftHand.current?.targetRay.position) {
86
+ translating = true
87
+ startLeftPinchTranslation.copy(leftHand.current.targetRay.position)
88
+ }
89
+ })
90
+ })
91
+
92
+ useTask(
93
+ () => {
94
+ if (leftHand.current?.targetRay && translating) {
95
+ leftPinchTranslation
96
+ .copy(leftHand.current.targetRay.position)
97
+ .sub(startLeftPinchTranslation)
98
+ origin.set(leftPinchTranslation.toArray(), origin.rotation)
99
+ }
100
+ },
101
+ {
102
+ running: () => translating,
103
+ }
104
+ )
105
+
106
+ useTask(
107
+ () => {
108
+ if (rightHand.current?.targetRay && rotating) {
109
+ rightPinchTranslation.copy(rightHand.current.targetRay.position)
110
+ const rotation =
111
+ origin.rotation + rightPinchTranslation.distanceTo(startRightPinchTranslation)
112
+ origin.set(leftPinchTranslation.toArray(), rotation)
113
+ }
114
+ },
115
+ {
116
+ running: () => rotating,
117
+ }
118
+ )
59
119
  </script>
60
120
 
61
- <T
62
- is={group}
63
- position={[0, 0.05, 0]}
64
- >
65
- <Grid
66
- plane="xy"
67
- position.y={0.05}
68
- fadeDistance={5}
69
- fadeOrigin={new Vector3()}
70
- cellSize={0.1}
71
- cellColor="#fff"
72
- sectionColor="#fff"
73
- />
74
- </T>
121
+ <Grid
122
+ plane="xy"
123
+ fadeDistance={5}
124
+ fadeOrigin={new Vector3()}
125
+ cellSize={0.1}
126
+ cellColor="#fff"
127
+ sectionColor="#fff"
128
+ />
129
+
130
+ <Hand
131
+ left
132
+ onpinchstart={() => {
133
+ console.log('pinchstart')
134
+ if (leftHand.current?.targetRay.position) {
135
+ translating = true
136
+ startLeftPinchTranslation.copy(leftHand.current.targetRay.position)
137
+ }
138
+ }}
139
+ onpinchend={() => (translating = false)}
140
+ />
141
+
142
+ <Hand
143
+ right
144
+ onpinchstart={() => {
145
+ if (rightHand.current?.targetRay.position) {
146
+ rotating = true
147
+ startRightPinchTranslation.copy(rightHand.current.targetRay.position)
148
+ }
149
+ }}
150
+ onpinchend={() => (rotating = false)}
151
+ />
@@ -1,6 +1,6 @@
1
1
  export interface Settings {
2
2
  cameraMode: 'orthographic' | 'perspective';
3
- interactionMode: 'navigate' | 'measure' | 'lasso';
3
+ interactionMode: 'navigate' | 'measure' | 'select';
4
4
  refreshRates: {
5
5
  poses: number;
6
6
  pointclouds: number;
@@ -65,7 +65,7 @@ export const provideSettings = () => {
65
65
  });
66
66
  $effect(() => {
67
67
  if (isLoaded) {
68
- set('motion-tools-settings', $state.snapshot(settings));
68
+ set('motion-tools-settings', $state.snapshot({ ...settings, interactionMode: 'navigate' }));
69
69
  }
70
70
  });
71
71
  const context = {
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default as MotionTools } from './components/App.svelte';
2
- export { default as LassoTool } from './components/Lasso/Tool.svelte';
2
+ export { default as SelectionTool } from './components/Selection/Tool.svelte';
3
3
  export { default as PCD } from './components/PCD.svelte';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { default as MotionTools } from './components/App.svelte';
2
2
  // Plugins
3
- export { default as LassoTool } from './components/Lasso/Tool.svelte';
3
+ export { default as SelectionTool } from './components/Selection/Tool.svelte';
4
4
  export { default as PCD } from './components/PCD.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.15.8",
3
+ "version": "1.16.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",