@viamrobotics/motion-tools 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/App.svelte +6 -1
- package/dist/components/CameraControls.svelte +1 -1
- package/dist/components/Focus.svelte +1 -8
- package/dist/components/Geometry.svelte +13 -13
- package/dist/components/Lasso/Debug.svelte +72 -0
- package/dist/components/Lasso/Debug.svelte.d.ts +8 -0
- package/dist/components/Lasso/Lasso.svelte +299 -0
- package/dist/components/Lasso/Lasso.svelte.d.ts +6 -0
- package/dist/components/Lasso/Tool.svelte +94 -0
- package/dist/components/Lasso/Tool.svelte.d.ts +9 -0
- package/dist/components/Lasso/traits.d.ts +21 -0
- package/dist/components/Lasso/traits.js +16 -0
- package/dist/components/LineGeometry.svelte +20 -0
- package/dist/components/LineGeometry.svelte.d.ts +12 -0
- package/dist/components/MeasureTool/MeasureTool.svelte +2 -2
- package/dist/components/PCD.svelte +34 -0
- package/dist/components/PCD.svelte.d.ts +6 -0
- package/dist/components/PointerMissBox.svelte +1 -2
- package/dist/components/Points.svelte +1 -1
- package/dist/components/Scene.svelte +10 -8
- package/dist/components/hover/HoveredEntityTooltip.svelte +2 -1
- package/dist/components/overlay/FloatingPanel.svelte +17 -10
- package/dist/components/overlay/FloatingPanel.svelte.d.ts +5 -0
- package/dist/components/xr/CameraFeed.svelte +3 -0
- package/dist/components/xr/CameraFeed.svelte.d.ts +1 -0
- package/dist/components/xr/JointLimitsWidget.svelte +25 -22
- package/dist/components/xr/XR.svelte +28 -20
- package/dist/ecs/traits.d.ts +8 -0
- package/dist/ecs/traits.js +8 -0
- package/dist/hooks/useControls.svelte.d.ts +2 -1
- package/dist/hooks/useControls.svelte.js +7 -2
- package/dist/hooks/usePartConfig.svelte.js +1 -1
- package/dist/hooks/useSettings.svelte.d.ts +1 -1
- package/dist/hooks/useSettings.svelte.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/pcd.d.ts +1 -0
- package/dist/pcd.js +44 -0
- package/dist/plugins/bvh.svelte.d.ts +8 -0
- package/dist/plugins/bvh.svelte.js +74 -0
- package/dist/ply.d.ts +1 -1
- package/dist/ply.js +5 -0
- package/dist/test/createRandomPcdBinary.d.ts +1 -1
- package/dist/test/createRandomPcdBinary.js +14 -27
- package/package.json +4 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte'
|
|
3
3
|
import { Canvas } from '@threlte/core'
|
|
4
|
+
import { PortalTarget } from '@threlte/extras'
|
|
4
5
|
import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools'
|
|
5
6
|
import { provideToast, ToastContainer } from '@viamrobotics/prime-core'
|
|
6
7
|
import type { Struct } from '@viamrobotics/sdk'
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
import Camera from './overlay/widgets/Camera.svelte'
|
|
30
31
|
import HoveredEntities from './hover/HoveredEntities.svelte'
|
|
31
32
|
import Settings from './overlay/settings/Settings.svelte'
|
|
33
|
+
import { useXR } from '@threlte/xr'
|
|
32
34
|
|
|
33
35
|
interface LocalConfigProps {
|
|
34
36
|
getLocalPartConfig: () => Struct
|
|
@@ -67,6 +69,7 @@
|
|
|
67
69
|
const settings = provideSettings()
|
|
68
70
|
const environment = provideEnvironment()
|
|
69
71
|
const currentRobotCameraWidgets = $derived(settings.current.openCameraWidgets[partID] || [])
|
|
72
|
+
const { isPresenting } = useXR()
|
|
70
73
|
|
|
71
74
|
$effect(() => {
|
|
72
75
|
settings.current.enableKeybindings = enableKeybindings
|
|
@@ -148,11 +151,13 @@
|
|
|
148
151
|
<ArmPositions />
|
|
149
152
|
{/if}
|
|
150
153
|
|
|
151
|
-
{#if !focus}
|
|
154
|
+
{#if !focus && !$isPresenting}
|
|
152
155
|
{#each currentRobotCameraWidgets as cameraName (cameraName)}
|
|
153
156
|
<Camera name={cameraName} />
|
|
154
157
|
{/each}
|
|
155
158
|
{/if}
|
|
159
|
+
|
|
160
|
+
<PortalTarget id="dom" />
|
|
156
161
|
</div>
|
|
157
162
|
{/snippet}
|
|
158
163
|
</SceneProviders>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { type Snippet } from 'svelte'
|
|
4
4
|
import { meshBounds } from '@threlte/extras'
|
|
5
5
|
import { BufferGeometry, Color, DoubleSide, FrontSide, Group, Mesh } from 'three'
|
|
6
|
-
import { Line2,
|
|
6
|
+
import { Line2, LineMaterial } from 'three/examples/jsm/Addons.js'
|
|
7
7
|
import { CapsuleGeometry } from '../three/CapsuleGeometry'
|
|
8
8
|
import { colors, darkenColor } from '../color'
|
|
9
9
|
import AxesHelper from './AxesHelper.svelte'
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { traits, useTrait } from '../ecs'
|
|
12
12
|
import { poseToObject3d } from '../transform'
|
|
13
13
|
import type { Pose } from '@viamrobotics/sdk'
|
|
14
|
+
import LineGeometry from './LineGeometry.svelte'
|
|
14
15
|
|
|
15
16
|
interface Props extends ThrelteProps<Group> {
|
|
16
17
|
entity: Entity
|
|
@@ -47,6 +48,8 @@
|
|
|
47
48
|
const lineWidth = useTrait(() => entity, traits.LineWidth)
|
|
48
49
|
const center = useTrait(() => entity, traits.Center)
|
|
49
50
|
const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
|
|
51
|
+
const materialProps = useTrait(() => entity, traits.Material)
|
|
52
|
+
const renderOrder = useTrait(() => entity, traits.RenderOrder)
|
|
50
53
|
|
|
51
54
|
const geometryType = $derived.by(() => {
|
|
52
55
|
if (box.current) return 'box'
|
|
@@ -136,7 +139,7 @@
|
|
|
136
139
|
is={mesh}
|
|
137
140
|
name={entity}
|
|
138
141
|
userData.name={name}
|
|
139
|
-
|
|
142
|
+
renderOrder={renderOrder.current}
|
|
140
143
|
>
|
|
141
144
|
{#if model && renderMode.includes('model')}
|
|
142
145
|
<T is={model} />
|
|
@@ -144,14 +147,7 @@
|
|
|
144
147
|
|
|
145
148
|
{#if !model || renderMode.includes('colliders')}
|
|
146
149
|
{#if linePositions.current}
|
|
147
|
-
<
|
|
148
|
-
is={LineGeometry}
|
|
149
|
-
oncreate={(ref) => {
|
|
150
|
-
if (linePositions.current) {
|
|
151
|
-
ref.setPositions(linePositions.current)
|
|
152
|
-
}
|
|
153
|
-
}}
|
|
154
|
-
/>
|
|
150
|
+
<LineGeometry positions={linePositions.current} />
|
|
155
151
|
{:else if box.current}
|
|
156
152
|
{@const { x, y, z } = box.current ?? { x: 0, y: 0, z: 0 }}
|
|
157
153
|
<T.BoxGeometry
|
|
@@ -179,16 +175,20 @@
|
|
|
179
175
|
is={LineMaterial}
|
|
180
176
|
{color}
|
|
181
177
|
width={lineWidth.current ? lineWidth.current * 0.001 : 0.5}
|
|
178
|
+
depthTest={materialProps.current?.depthTest}
|
|
182
179
|
/>
|
|
183
180
|
{:else}
|
|
181
|
+
{@const currentOpacity = opacity.current ?? 0.7}
|
|
184
182
|
<T.MeshToonMaterial
|
|
185
183
|
{color}
|
|
186
184
|
side={geometryType === 'buffer' ? DoubleSide : FrontSide}
|
|
187
|
-
transparent={
|
|
188
|
-
|
|
185
|
+
transparent={currentOpacity < 1}
|
|
186
|
+
depthWrite={currentOpacity === 1}
|
|
187
|
+
opacity={currentOpacity}
|
|
188
|
+
depthTest={materialProps.current?.depthTest}
|
|
189
189
|
/>
|
|
190
190
|
|
|
191
|
-
{#if geo && renderMode.includes('colliders')}
|
|
191
|
+
{#if geo && (renderMode.includes('colliders') || !model)}
|
|
192
192
|
<T.LineSegments
|
|
193
193
|
raycast={() => null}
|
|
194
194
|
bvh={{ enabled: false }}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
|
|
4
|
+
Shows all steps for querying points within a lasso selection
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { T } from '@threlte/core'
|
|
8
|
+
import { Box3, BufferAttribute, BufferGeometry, Vector3 } from 'three'
|
|
9
|
+
import { useTrait, traits } from '../../ecs'
|
|
10
|
+
import type { Entity } from 'koota'
|
|
11
|
+
import * as lassoTraits from './traits'
|
|
12
|
+
|
|
13
|
+
const box3 = new Box3()
|
|
14
|
+
const min = new Vector3()
|
|
15
|
+
const max = new Vector3()
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
lasso: Entity
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let { lasso }: Props = $props()
|
|
22
|
+
|
|
23
|
+
const indices = useTrait(() => lasso, lassoTraits.Indices)
|
|
24
|
+
const positions = useTrait(() => lasso, traits.LinePositions)
|
|
25
|
+
const box = useTrait(() => lasso, lassoTraits.Box)
|
|
26
|
+
const boxes = useTrait(() => lasso, lassoTraits.Boxes)
|
|
27
|
+
|
|
28
|
+
const geometry = new BufferGeometry()
|
|
29
|
+
|
|
30
|
+
$effect(() => {
|
|
31
|
+
if (indices.current) {
|
|
32
|
+
geometry.setIndex(new BufferAttribute(indices.current, 1))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (positions.current) {
|
|
36
|
+
geometry.setAttribute('position', new BufferAttribute(positions.current, 3))
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
{#if positions.current && indices.current}
|
|
42
|
+
<T.Mesh>
|
|
43
|
+
<T is={geometry} />
|
|
44
|
+
<T.MeshBasicMaterial
|
|
45
|
+
wireframe
|
|
46
|
+
color="green"
|
|
47
|
+
/>
|
|
48
|
+
</T.Mesh>
|
|
49
|
+
{/if}
|
|
50
|
+
|
|
51
|
+
{#if boxes.current}
|
|
52
|
+
{#each boxes.current as box (box)}
|
|
53
|
+
<T.Box3Helper
|
|
54
|
+
args={[
|
|
55
|
+
new Box3().set(min.set(box.minX, box.minY, 0), max.set(box.maxX, box.maxY, 0)),
|
|
56
|
+
'lightgreen',
|
|
57
|
+
]}
|
|
58
|
+
/>
|
|
59
|
+
{/each}
|
|
60
|
+
{/if}
|
|
61
|
+
|
|
62
|
+
{#if box.current}
|
|
63
|
+
<T.Box3Helper
|
|
64
|
+
args={[
|
|
65
|
+
box3.set(
|
|
66
|
+
min.set(box.current.minX, box.current.minY, 0),
|
|
67
|
+
max.set(box.current.maxX, box.current.maxY, 0)
|
|
68
|
+
),
|
|
69
|
+
'red',
|
|
70
|
+
]}
|
|
71
|
+
/>
|
|
72
|
+
{/if}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Entity } from 'koota';
|
|
2
|
+
interface Props {
|
|
3
|
+
lasso: Entity;
|
|
4
|
+
}
|
|
5
|
+
/** Shows all steps for querying points within a lasso selection */
|
|
6
|
+
declare const Debug: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type Debug = ReturnType<typeof Debug>;
|
|
8
|
+
export default Debug;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Raycaster, Box3, Vector3, Vector2, Plane, Triangle } from 'three'
|
|
3
|
+
import { useThrelte } from '@threlte/core'
|
|
4
|
+
import { Not } from 'koota'
|
|
5
|
+
import { useCameraControls } from '../../hooks/useControls.svelte'
|
|
6
|
+
import earcut from 'earcut'
|
|
7
|
+
import { traits, useQuery, useWorld } from '../../ecs'
|
|
8
|
+
import type { ShapecastCallbacks } from 'three-mesh-bvh'
|
|
9
|
+
import { createBufferGeometry } from '../../attribute'
|
|
10
|
+
import * as lassoTraits from './traits'
|
|
11
|
+
import Debug from './Debug.svelte'
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
debug?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { debug = false }: Props = $props()
|
|
18
|
+
|
|
19
|
+
const world = useWorld()
|
|
20
|
+
const controls = useCameraControls()
|
|
21
|
+
const { scene, dom, camera } = useThrelte()
|
|
22
|
+
|
|
23
|
+
const box3 = new Box3()
|
|
24
|
+
const min = new Vector3()
|
|
25
|
+
const max = new Vector3()
|
|
26
|
+
|
|
27
|
+
const triangle = new Triangle()
|
|
28
|
+
const triangleBox = new Box3()
|
|
29
|
+
const a = new Vector3()
|
|
30
|
+
const b = new Vector3()
|
|
31
|
+
const c = new Vector3()
|
|
32
|
+
|
|
33
|
+
let frameScheduled = false
|
|
34
|
+
let drawing = false
|
|
35
|
+
|
|
36
|
+
const raycaster = new Raycaster()
|
|
37
|
+
const mouse = new Vector2()
|
|
38
|
+
const plane = new Plane(new Vector3(0, 0, 1), 0)
|
|
39
|
+
const point = new Vector3()
|
|
40
|
+
|
|
41
|
+
const raycast = (event: PointerEvent) => {
|
|
42
|
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1
|
|
43
|
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
|
|
44
|
+
|
|
45
|
+
raycaster.setFromCamera(mouse, camera.current)
|
|
46
|
+
raycaster.ray.intersectPlane(plane, point)
|
|
47
|
+
return point
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const onpointerdown = (event: PointerEvent) => {
|
|
51
|
+
if (!event.shiftKey) return
|
|
52
|
+
|
|
53
|
+
const { x, y } = raycast(event)
|
|
54
|
+
|
|
55
|
+
drawing = true
|
|
56
|
+
|
|
57
|
+
world.spawn(
|
|
58
|
+
traits.LinePositions(new Float32Array([x, y, 0])),
|
|
59
|
+
traits.LineWidth(1.5),
|
|
60
|
+
traits.RenderOrder(999),
|
|
61
|
+
traits.Material({ depthTest: false }),
|
|
62
|
+
traits.Color({ r: 1, g: 0, b: 0 }),
|
|
63
|
+
lassoTraits.Box({ minX: x, minY: y, maxX: x, maxY: y }),
|
|
64
|
+
lassoTraits.Lasso
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if (controls.current) {
|
|
68
|
+
controls.current.enabled = false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const onpointermove = (event: PointerEvent) => {
|
|
73
|
+
if (!drawing) return
|
|
74
|
+
|
|
75
|
+
let lasso = world.query(lassoTraits.Lasso).at(-1)
|
|
76
|
+
|
|
77
|
+
if (!lasso) return
|
|
78
|
+
|
|
79
|
+
if (frameScheduled) return
|
|
80
|
+
|
|
81
|
+
frameScheduled = true
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* pointermove can execute at a rate much higher than screen
|
|
85
|
+
* refresh, creating huge polygon vertex counts, so we cap it.
|
|
86
|
+
*/
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
frameScheduled = false
|
|
89
|
+
|
|
90
|
+
const { x, y } = raycast(event)
|
|
91
|
+
const positions = lasso.get(traits.LinePositions)
|
|
92
|
+
const box = lasso.get(lassoTraits.Box)
|
|
93
|
+
|
|
94
|
+
if (!positions || !box) return
|
|
95
|
+
|
|
96
|
+
const nextPositions = new Float32Array(positions.length + 3)
|
|
97
|
+
nextPositions.set(positions)
|
|
98
|
+
nextPositions[positions.length] = x
|
|
99
|
+
nextPositions[positions.length + 1] = y
|
|
100
|
+
lasso.set(traits.LinePositions, nextPositions)
|
|
101
|
+
|
|
102
|
+
if (x < box.minX) box.minX = x
|
|
103
|
+
else if (x > box.maxX) box.maxX = x
|
|
104
|
+
|
|
105
|
+
if (y < box.minY) box.minY = y
|
|
106
|
+
else if (y > box.maxY) box.maxY = y
|
|
107
|
+
|
|
108
|
+
lasso.set(lassoTraits.Box, box)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const onpointerleave = () => {
|
|
113
|
+
if (!drawing) return
|
|
114
|
+
|
|
115
|
+
onpointerup()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const onpointerup = () => {
|
|
119
|
+
if (!drawing) return
|
|
120
|
+
|
|
121
|
+
drawing = false
|
|
122
|
+
|
|
123
|
+
let lasso = world.query(lassoTraits.Lasso).at(-1)
|
|
124
|
+
|
|
125
|
+
if (!lasso) return
|
|
126
|
+
|
|
127
|
+
let positions = lasso.get(traits.LinePositions)
|
|
128
|
+
|
|
129
|
+
if (!positions) return
|
|
130
|
+
|
|
131
|
+
const [startX, startY] = positions
|
|
132
|
+
|
|
133
|
+
if (controls.current) {
|
|
134
|
+
controls.current.enabled = true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Close the loop
|
|
138
|
+
const nextPositions = new Float32Array(positions.length + 3)
|
|
139
|
+
nextPositions.set(positions)
|
|
140
|
+
nextPositions[positions.length] = startX
|
|
141
|
+
nextPositions[positions.length + 1] = startY
|
|
142
|
+
lasso.set(traits.LinePositions, nextPositions)
|
|
143
|
+
positions = nextPositions
|
|
144
|
+
|
|
145
|
+
const indices = earcut(positions, undefined, 3)
|
|
146
|
+
if (debug) {
|
|
147
|
+
lasso.add(lassoTraits.Indices(new Uint16Array(indices)))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const getTriangleFromIndex = (i: number, triangle: Triangle) => {
|
|
151
|
+
const stride = 3
|
|
152
|
+
const ia = indices[i + 0] * stride
|
|
153
|
+
const ib = indices[i + 1] * stride
|
|
154
|
+
const ic = indices[i + 2] * stride
|
|
155
|
+
a.set(positions[ia + 0], positions[ia + 1], positions[ia + 2])
|
|
156
|
+
b.set(positions[ib + 0], positions[ib + 1], positions[ib + 2])
|
|
157
|
+
c.set(positions[ic + 0], positions[ic + 1], positions[ic + 2])
|
|
158
|
+
triangle.set(a, b, c)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const boxes: lassoTraits.AABB[] = []
|
|
162
|
+
for (let i = 0, l = indices.length; i < l; i += 3) {
|
|
163
|
+
getTriangleFromIndex(i, triangle)
|
|
164
|
+
box3.setFromPoints([triangle.a, triangle.b, triangle.c])
|
|
165
|
+
boxes.push({ minX: box3.min.x, minY: box3.min.y, maxX: box3.max.x, maxY: box3.max.y })
|
|
166
|
+
}
|
|
167
|
+
if (debug) {
|
|
168
|
+
lasso.add(lassoTraits.Boxes(boxes))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const lassoBox = lasso.get(lassoTraits.Box)
|
|
172
|
+
|
|
173
|
+
if (!lassoBox) return
|
|
174
|
+
|
|
175
|
+
min.set(lassoBox.minX, lassoBox.minY, Number.NEGATIVE_INFINITY)
|
|
176
|
+
max.set(lassoBox.maxX, lassoBox.maxY, Number.POSITIVE_INFINITY)
|
|
177
|
+
box3.set(min, max)
|
|
178
|
+
|
|
179
|
+
const enclosedPoints: number[] = []
|
|
180
|
+
|
|
181
|
+
for (const pointsEntity of world.query(traits.Points, Not(lassoTraits.LassoEnclosedPoints))) {
|
|
182
|
+
const geometry = pointsEntity.get(traits.BufferGeometry)
|
|
183
|
+
|
|
184
|
+
if (!geometry) return
|
|
185
|
+
|
|
186
|
+
const points = scene.getObjectByName(pointsEntity as unknown as string)
|
|
187
|
+
|
|
188
|
+
if (!points) {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
geometry.boundsTree?.shapecast({
|
|
193
|
+
intersectsBounds: (box) => {
|
|
194
|
+
return box.intersectsBox(box3)
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
intersectsPoint: (point: Vector3) => {
|
|
198
|
+
for (let i = 0, j = 0, l = indices.length; i < l; i += 3, j += 1) {
|
|
199
|
+
const { minX, minY, maxX, maxY } = boxes[j]
|
|
200
|
+
|
|
201
|
+
min.set(minX, minY, Number.NEGATIVE_INFINITY)
|
|
202
|
+
max.set(maxX, maxY, Number.POSITIVE_INFINITY)
|
|
203
|
+
triangleBox.set(min, max)
|
|
204
|
+
|
|
205
|
+
if (triangleBox.containsPoint(point)) {
|
|
206
|
+
getTriangleFromIndex(i, triangle)
|
|
207
|
+
|
|
208
|
+
if (triangle.containsPoint(point)) {
|
|
209
|
+
enclosedPoints.push(point.x, point.y, point.z)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
// intersectsPoint is not yet in typedef, this can be removed when it is added
|
|
215
|
+
} as ShapecastCallbacks)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const lassoResultGeometry = createBufferGeometry(new Float32Array(enclosedPoints))
|
|
219
|
+
|
|
220
|
+
world.spawn(
|
|
221
|
+
traits.Name('Lasso result'),
|
|
222
|
+
traits.BufferGeometry(lassoResultGeometry),
|
|
223
|
+
traits.Color({ r: 1, g: 0, b: 0 }),
|
|
224
|
+
traits.RenderOrder(999),
|
|
225
|
+
traits.Material({ depthTest: false }),
|
|
226
|
+
traits.Points,
|
|
227
|
+
traits.Removable,
|
|
228
|
+
lassoTraits.LassoEnclosedPoints,
|
|
229
|
+
lassoTraits.PointsCapturedBy(lasso)
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const onkeydown = (event: KeyboardEvent) => {
|
|
234
|
+
if (event.key === 'Shift') {
|
|
235
|
+
dom.style.cursor = 'crosshair'
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const onkeyup = (event: KeyboardEvent) => {
|
|
240
|
+
if (event.key === 'Shift') {
|
|
241
|
+
dom.style.removeProperty('cursor')
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
$effect(() => {
|
|
246
|
+
window.addEventListener('keydown', onkeydown)
|
|
247
|
+
window.addEventListener('keyup', onkeyup)
|
|
248
|
+
dom.addEventListener('pointerdown', onpointerdown)
|
|
249
|
+
dom.addEventListener('pointermove', onpointermove)
|
|
250
|
+
dom.addEventListener('pointerup', onpointerup)
|
|
251
|
+
dom.addEventListener('pointerleave', onpointerleave)
|
|
252
|
+
|
|
253
|
+
return () => {
|
|
254
|
+
window.removeEventListener('keydown', onkeydown)
|
|
255
|
+
window.removeEventListener('keyup', onkeyup)
|
|
256
|
+
dom.removeEventListener('pointerdown', onpointerdown)
|
|
257
|
+
dom.removeEventListener('pointermove', onpointermove)
|
|
258
|
+
dom.removeEventListener('pointerup', onpointerup)
|
|
259
|
+
dom.removeEventListener('pointerleave', onpointerleave)
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const lassos = useQuery(lassoTraits.Lasso)
|
|
264
|
+
|
|
265
|
+
$effect(() => {
|
|
266
|
+
if (!controls.current) return
|
|
267
|
+
|
|
268
|
+
const currentControls = controls.current
|
|
269
|
+
|
|
270
|
+
const { minPolarAngle, maxPolarAngle } = currentControls
|
|
271
|
+
|
|
272
|
+
// Locks the camera to top down while this component is mounted
|
|
273
|
+
currentControls.polarAngle = 0
|
|
274
|
+
currentControls.minPolarAngle = 0
|
|
275
|
+
currentControls.maxPolarAngle = 0
|
|
276
|
+
|
|
277
|
+
return () => {
|
|
278
|
+
currentControls.minPolarAngle = minPolarAngle
|
|
279
|
+
currentControls.maxPolarAngle = maxPolarAngle
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// On unmount, destroy all lasso related entities
|
|
284
|
+
$effect(() => {
|
|
285
|
+
return () => {
|
|
286
|
+
for (const entity of world.query(lassoTraits.LassoEnclosedPoints)) {
|
|
287
|
+
if (world.has(entity)) {
|
|
288
|
+
entity.destroy()
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
</script>
|
|
294
|
+
|
|
295
|
+
{#if debug}
|
|
296
|
+
{#each lassos.current as lasso (lasso)}
|
|
297
|
+
<Debug {lasso} />
|
|
298
|
+
{/each}
|
|
299
|
+
{/if}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Portal } from '@threlte/extras'
|
|
3
|
+
import { Button } from '@viamrobotics/prime-core'
|
|
4
|
+
import Lasso from './Lasso.svelte'
|
|
5
|
+
import DashboardButton from '../overlay/dashboard/Button.svelte'
|
|
6
|
+
import { useSettings } from '../../hooks/useSettings.svelte'
|
|
7
|
+
import FloatingPanel from '../overlay/FloatingPanel.svelte'
|
|
8
|
+
import { traits, useWorld } from '../../ecs'
|
|
9
|
+
import * as lassoTraits from './traits'
|
|
10
|
+
import { BufferGeometryUtils } from 'three/examples/jsm/Addons.js'
|
|
11
|
+
import { createBinaryPCD } from '../../pcd'
|
|
12
|
+
import type { BufferGeometry } from 'three'
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Whether to auto-enable lasso mode when the component mounts */
|
|
16
|
+
enabled?: boolean
|
|
17
|
+
|
|
18
|
+
/** Fires when the user has committed to a lasso selection */
|
|
19
|
+
onSelection: (pcd: Blob) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { enabled = false, onSelection }: Props = $props()
|
|
23
|
+
|
|
24
|
+
const world = useWorld()
|
|
25
|
+
const settings = useSettings()
|
|
26
|
+
const isLassoMode = $derived(settings.current.interactionMode === 'lasso')
|
|
27
|
+
|
|
28
|
+
const onCommitClick = () => {
|
|
29
|
+
const entities = world.query(lassoTraits.LassoEnclosedPoints)
|
|
30
|
+
|
|
31
|
+
const geometries: BufferGeometry[] = []
|
|
32
|
+
for (const entity of entities) {
|
|
33
|
+
const geometry = entity.get(traits.BufferGeometry)
|
|
34
|
+
|
|
35
|
+
if (geometry) {
|
|
36
|
+
geometries.push(geometry)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries)
|
|
41
|
+
const positions = mergedGeometry.getAttribute('position').array as Float32Array
|
|
42
|
+
|
|
43
|
+
const pcd = createBinaryPCD(positions)
|
|
44
|
+
|
|
45
|
+
onSelection(pcd)
|
|
46
|
+
|
|
47
|
+
for (const entity of entities) {
|
|
48
|
+
if (world.has(entity)) {
|
|
49
|
+
entity.destroy()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$effect(() => {
|
|
55
|
+
if (enabled) {
|
|
56
|
+
settings.current.interactionMode = 'lasso'
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<Portal id="dashboard">
|
|
62
|
+
<fieldset>
|
|
63
|
+
<DashboardButton
|
|
64
|
+
active={isLassoMode}
|
|
65
|
+
icon="selection-drag"
|
|
66
|
+
description="{isLassoMode ? 'Disable' : 'Enable'} lasso selection"
|
|
67
|
+
onclick={() => {
|
|
68
|
+
settings.current.interactionMode = isLassoMode ? 'navigate' : 'lasso'
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</fieldset>
|
|
72
|
+
</Portal>
|
|
73
|
+
|
|
74
|
+
{#if isLassoMode}
|
|
75
|
+
<Lasso />
|
|
76
|
+
|
|
77
|
+
<Portal id="dom">
|
|
78
|
+
<FloatingPanel
|
|
79
|
+
isOpen
|
|
80
|
+
exitable={false}
|
|
81
|
+
title="Lasso"
|
|
82
|
+
defaultSize={{ width: 445, height: 100 }}
|
|
83
|
+
defaultPosition={{ x: window.innerWidth / 2 - 200, y: window.innerHeight - 10 - 100 }}
|
|
84
|
+
>
|
|
85
|
+
<div class="flex items-center gap-4 p-4 text-xs">
|
|
86
|
+
Shift + click and drag to make a lasso selection.
|
|
87
|
+
<Button
|
|
88
|
+
onclick={onCommitClick}
|
|
89
|
+
variant="success">Commit selection</Button
|
|
90
|
+
>
|
|
91
|
+
</div>
|
|
92
|
+
</FloatingPanel>
|
|
93
|
+
</Portal>
|
|
94
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Whether to auto-enable lasso mode when the component mounts */
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
/** Fires when the user has committed to a lasso selection */
|
|
5
|
+
onSelection: (pcd: Blob) => void;
|
|
6
|
+
}
|
|
7
|
+
declare const Tool: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type Tool = ReturnType<typeof Tool>;
|
|
9
|
+
export default Tool;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const Lasso: import("koota").Trait<() => boolean>;
|
|
2
|
+
export declare const LassoEnclosedPoints: import("koota").Trait<() => boolean>;
|
|
3
|
+
/**
|
|
4
|
+
* Captured points are removable, so we want to also destroy
|
|
5
|
+
* the source lasso every time a user deletes one.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PointsCapturedBy: import("koota").Relation<import("koota").Trait<Record<string, never>>>;
|
|
8
|
+
export interface AABB {
|
|
9
|
+
minX: number;
|
|
10
|
+
minY: number;
|
|
11
|
+
maxX: number;
|
|
12
|
+
maxY: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const Box: import("koota").Trait<{
|
|
15
|
+
minX: number;
|
|
16
|
+
minY: number;
|
|
17
|
+
maxX: number;
|
|
18
|
+
maxY: number;
|
|
19
|
+
}>;
|
|
20
|
+
export declare const Indices: import("koota").Trait<() => Uint16Array<ArrayBuffer>>;
|
|
21
|
+
export declare const Boxes: import("koota").Trait<() => AABB[]>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { relation, trait } from 'koota';
|
|
2
|
+
export const Lasso = trait(() => true);
|
|
3
|
+
export const LassoEnclosedPoints = trait(() => true);
|
|
4
|
+
/**
|
|
5
|
+
* Captured points are removable, so we want to also destroy
|
|
6
|
+
* the source lasso every time a user deletes one.
|
|
7
|
+
*/
|
|
8
|
+
export const PointsCapturedBy = relation({ autoDestroy: 'target' });
|
|
9
|
+
export const Box = trait({
|
|
10
|
+
minX: 0,
|
|
11
|
+
minY: 0,
|
|
12
|
+
maxX: 0,
|
|
13
|
+
maxY: 0,
|
|
14
|
+
});
|
|
15
|
+
export const Indices = trait(() => new Uint16Array());
|
|
16
|
+
export const Boxes = trait(() => []);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { LineGeometry } from 'three/examples/jsm/Addons.js'
|
|
3
|
+
import { T } from '@threlte/core'
|
|
4
|
+
import { untrack } from 'svelte'
|
|
5
|
+
|
|
6
|
+
let { positions } = $props()
|
|
7
|
+
|
|
8
|
+
let geometry = $state.raw(new LineGeometry())
|
|
9
|
+
|
|
10
|
+
$effect(() => {
|
|
11
|
+
if (positions) {
|
|
12
|
+
untrack(() => {
|
|
13
|
+
geometry = new LineGeometry()
|
|
14
|
+
geometry.setPositions(positions)
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<T is={geometry} />
|