@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.
- package/dist/components/{Lasso → Selection}/Debug.svelte +8 -8
- package/dist/components/{Lasso → Selection}/Debug.svelte.d.ts +2 -2
- package/dist/components/Selection/Ellipse.svelte +293 -0
- package/dist/components/Selection/Ellipse.svelte.d.ts +7 -0
- package/dist/components/{Lasso → Selection}/Lasso.svelte +31 -61
- package/dist/components/{Lasso → Selection}/Lasso.svelte.d.ts +1 -0
- package/dist/components/{Lasso → Selection}/Tool.svelte +50 -15
- package/dist/components/{Lasso → Selection}/traits.d.ts +11 -2
- package/dist/components/{Lasso → Selection}/traits.js +7 -2
- package/dist/components/Selection/utils.d.ts +5 -0
- package/dist/components/Selection/utils.js +38 -0
- package/dist/components/xr/OriginMarker.svelte +94 -17
- package/dist/hooks/useSettings.svelte.d.ts +1 -1
- package/dist/hooks/useSettings.svelte.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- /package/dist/components/{Lasso → Selection}/Tool.svelte.d.ts +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
3
|
|
|
4
|
-
Shows all steps for querying points within a
|
|
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
|
|
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
|
-
|
|
21
|
+
selection: Entity
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
let {
|
|
24
|
+
let { selection }: Props = $props()
|
|
25
25
|
|
|
26
|
-
const indices = useTrait(() =>
|
|
27
|
-
const positions = useTrait(() =>
|
|
28
|
-
const box = useTrait(() =>
|
|
29
|
-
const boxes = useTrait(() =>
|
|
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
|
-
|
|
3
|
+
selection: Entity;
|
|
4
4
|
}
|
|
5
|
-
/** Shows all steps for querying points within a
|
|
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}
|
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
135
|
+
lasso.add(selectionTraits.Indices(new Uint16Array(indices)))
|
|
153
136
|
}
|
|
154
137
|
|
|
155
|
-
const
|
|
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(
|
|
140
|
+
lasso.add(selectionTraits.Boxes(boxes))
|
|
174
141
|
}
|
|
175
142
|
|
|
176
|
-
const lassoBox = lasso.get(
|
|
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(
|
|
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
|
-
|
|
234
|
-
|
|
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(
|
|
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(
|
|
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}
|
|
@@ -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
|
|
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
|
|
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(
|
|
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 (
|
|
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 = '
|
|
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
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
89
|
-
<
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
/>
|
|
@@ -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
|
|
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
|
|
3
|
+
export { default as SelectionTool } from './components/Selection/Tool.svelte';
|
|
4
4
|
export { default as PCD } from './components/PCD.svelte';
|
package/package.json
CHANGED
|
File without changes
|