@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.
- package/dist/attribute.d.ts +3 -2
- package/dist/attribute.js +24 -16
- package/dist/buf/draw/v1/drawing_pb.d.ts +33 -16
- package/dist/buf/draw/v1/drawing_pb.js +35 -17
- package/dist/buf/draw/v1/metadata_pb.d.ts +44 -3
- package/dist/buf/draw/v1/metadata_pb.js +54 -3
- package/dist/buf/draw/v1/scene_pb.d.ts +6 -6
- package/dist/buf/draw/v1/scene_pb.js +7 -7
- package/dist/buffer.d.ts +54 -45
- package/dist/buffer.js +91 -57
- package/dist/color.d.ts +1 -2
- package/dist/color.js +5 -12
- package/dist/components/App.svelte +18 -3
- package/dist/components/App.svelte.d.ts +15 -2
- package/dist/components/Entities/Arrows/ArrowGroups.svelte +5 -6
- package/dist/components/Entities/Arrows/Arrows.svelte +9 -0
- package/dist/components/Entities/Entities.svelte +18 -1
- package/dist/components/Entities/Frame.svelte +7 -1
- package/dist/components/Entities/GLTF.svelte +13 -2
- package/dist/components/Entities/Line.svelte +46 -18
- package/dist/components/Entities/LineDots.svelte +38 -8
- package/dist/components/Entities/LineDots.svelte.d.ts +2 -2
- package/dist/components/Entities/LineGeometry.svelte +2 -1
- package/dist/components/Entities/LineGeometry.svelte.d.ts +2 -0
- package/dist/components/Entities/Mesh.svelte +8 -1
- package/dist/components/Entities/Points.svelte +22 -11
- package/dist/components/Entities/hooks/useEntityEvents.svelte.js +6 -2
- package/dist/components/FileDrop/FileDrop.svelte +5 -1
- package/dist/components/KeyboardControls.svelte +2 -10
- package/dist/components/PCD.svelte +11 -4
- package/dist/components/PCD.svelte.d.ts +3 -1
- package/dist/components/SceneProviders.svelte +2 -0
- package/dist/components/Selected.svelte +2 -12
- 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 +294 -0
- package/dist/components/Selection/Ellipse.svelte.d.ts +7 -0
- package/dist/components/{Lasso → Selection}/Lasso.svelte +33 -61
- package/dist/components/{Lasso → Selection}/Lasso.svelte.d.ts +1 -0
- package/dist/components/Selection/Tool.svelte +94 -0
- package/dist/components/{Lasso → Selection}/Tool.svelte.d.ts +2 -2
- package/dist/components/{Lasso → Selection}/traits.d.ts +11 -2
- package/dist/components/{Lasso → Selection}/traits.js +7 -2
- package/dist/components/Selection/useSelectionPlugin.svelte.d.ts +8 -0
- package/dist/components/Selection/useSelectionPlugin.svelte.js +24 -0
- package/dist/components/Selection/utils.d.ts +5 -0
- package/dist/components/Selection/utils.js +38 -0
- package/dist/components/Snapshot.svelte +4 -2
- package/dist/components/overlay/AddRelationship.svelte +1 -2
- package/dist/components/overlay/AddRelationship.svelte.d.ts +1 -1
- package/dist/components/overlay/Details.svelte +12 -12
- package/dist/components/overlay/Details.svelte.d.ts +8 -1
- package/dist/components/overlay/settings/Settings.svelte +8 -1
- package/dist/components/xr/OriginMarker.svelte +94 -17
- package/dist/components/xr/XR.svelte +1 -1
- package/dist/draw.d.ts +13 -0
- package/dist/draw.js +428 -0
- package/dist/ecs/traits.d.ts +31 -13
- package/dist/ecs/traits.js +25 -8
- package/dist/geometry.js +3 -0
- package/dist/hooks/useDrawAPI.svelte.js +61 -24
- package/dist/hooks/useDrawService.svelte.d.ts +12 -0
- package/dist/hooks/useDrawService.svelte.js +240 -0
- package/dist/hooks/usePointcloudObjects.svelte.js +7 -2
- package/dist/hooks/usePointclouds.svelte.js +7 -2
- package/dist/hooks/useSettings.svelte.d.ts +3 -2
- package/dist/hooks/useSettings.svelte.js +2 -2
- package/dist/hooks/useWorldState.svelte.js +5 -52
- package/dist/index.d.ts +9 -1
- package/dist/index.js +10 -1
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +2 -0
- package/dist/loaders/pcd/index.d.ts +1 -1
- package/dist/loaders/pcd/messages.d.ts +2 -2
- package/dist/loaders/pcd/worker.inline.d.ts +1 -1
- package/dist/loaders/pcd/worker.inline.js +229 -187
- package/dist/loaders/pcd/worker.js +2 -2
- package/dist/metadata.d.ts +9 -15
- package/dist/metadata.js +45 -9
- package/dist/plugins/bvh.svelte.js +6 -2
- package/dist/snapshot.d.ts +3 -9
- package/dist/snapshot.js +11 -204
- package/dist/three/InstancedArrows/InstancedArrows.js +3 -2
- package/package.json +14 -11
- package/dist/components/Lasso/Tool.svelte +0 -108
- package/dist/components/xr/Hands.svelte +0 -23
- 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}
|
|
@@ -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,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
|
-
|
|
69
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
136
|
+
lasso.add(selectionTraits.Indices(new Uint16Array(indices)))
|
|
153
137
|
}
|
|
154
138
|
|
|
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
|
-
}
|
|
139
|
+
const boxes: selectionTraits.AABB[] = getTriangleBoxesFromIndices(indices, positions)
|
|
172
140
|
if (debug) {
|
|
173
|
-
lasso.add(
|
|
141
|
+
lasso.add(selectionTraits.Boxes(boxes))
|
|
174
142
|
}
|
|
175
143
|
|
|
176
|
-
const lassoBox = lasso.get(
|
|
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(
|
|
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
|
-
|
|
234
|
-
|
|
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(
|
|
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(
|
|
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}
|
|
@@ -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
|
-
|
|
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
|
|
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,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 {};
|