@viamrobotics/motion-tools 1.3.1 → 1.3.3
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/buffer.d.ts +2 -0
- package/dist/buffer.js +2 -0
- package/dist/components/Arrows.svelte +81 -0
- package/dist/components/Arrows.svelte.d.ts +3 -0
- package/dist/components/Details.svelte +1 -4
- package/dist/components/GLTF.svelte +1 -1
- package/dist/components/Geometry2.svelte +1 -1
- package/dist/components/Points.svelte +2 -3
- package/dist/components/Scene.svelte +2 -0
- package/dist/components/Selected.svelte +25 -21
- package/dist/ecs/traits.d.ts +13 -0
- package/dist/ecs/traits.js +13 -0
- package/dist/hooks/useDrawAPI.svelte.js +42 -40
- package/dist/hooks/useObjectEvents.svelte.js +2 -2
- package/dist/hooks/usePose.svelte.js +3 -2
- package/dist/hooks/useSelection.svelte.d.ts +8 -4
- package/dist/hooks/useSelection.svelte.js +15 -18
- package/dist/snapshot.js +10 -42
- package/dist/three/InstancedArrows/InstancedArrows.d.ts +30 -0
- package/dist/three/InstancedArrows/InstancedArrows.js +138 -0
- package/dist/three/InstancedArrows/box.d.ts +3 -0
- package/dist/three/InstancedArrows/box.js +92 -0
- package/dist/three/InstancedArrows/fragment.glsl +7 -0
- package/dist/three/InstancedArrows/geometry.d.ts +4 -0
- package/dist/three/InstancedArrows/geometry.js +58 -0
- package/dist/three/InstancedArrows/raycast.d.ts +9 -0
- package/dist/three/InstancedArrows/raycast.js +129 -0
- package/dist/three/InstancedArrows/vertex.glsl +104 -0
- package/package.json +1 -1
package/dist/buffer.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export declare const STRIDE: {
|
|
|
19
19
|
readonly NURBS_KNOTS: 1;
|
|
20
20
|
/** Colors: [r, g, b, a] per color (uint8) */
|
|
21
21
|
readonly COLORS_RGBA: 4;
|
|
22
|
+
/** Colors: [r, g, b] */
|
|
23
|
+
readonly COLORS_RGB: 3;
|
|
22
24
|
};
|
|
23
25
|
/**
|
|
24
26
|
* Creates a Float32Array view over a Uint8Array without copying data.
|
package/dist/buffer.js
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { T } from '@threlte/core'
|
|
3
|
+
import { Portal } from '@threlte/extras'
|
|
4
|
+
import { InstancedArrows } from '../three/InstancedArrows/InstancedArrows'
|
|
5
|
+
import { traits, useWorld } from '../ecs'
|
|
6
|
+
import type { Entity } from 'koota'
|
|
7
|
+
import { STRIDE } from '../buffer'
|
|
8
|
+
import { useObjectEvents } from '../hooks/useObjectEvents.svelte'
|
|
9
|
+
import { SvelteMap } from 'svelte/reactivity'
|
|
10
|
+
import { Color } from 'three'
|
|
11
|
+
import { meshBoundsRaycast } from '../three/InstancedArrows/raycast'
|
|
12
|
+
|
|
13
|
+
const world = useWorld()
|
|
14
|
+
|
|
15
|
+
const map = new SvelteMap<Entity, InstancedArrows>()
|
|
16
|
+
|
|
17
|
+
const onAdd = (entity: Entity) => {
|
|
18
|
+
const poses = entity.get(traits.Positions)
|
|
19
|
+
const colors = entity.get(traits.Colors)
|
|
20
|
+
const { headAtPose } = entity.get(traits.Arrows) ?? {}
|
|
21
|
+
|
|
22
|
+
if (!poses) return
|
|
23
|
+
|
|
24
|
+
const total = poses.length / STRIDE.ARROWS
|
|
25
|
+
const alpha = colors && colors.length / STRIDE.COLORS_RGBA === total
|
|
26
|
+
const uniformColor =
|
|
27
|
+
colors && (colors.length === 3 || colors.length === 4)
|
|
28
|
+
? new Color(colors[0], colors[1], colors[2])
|
|
29
|
+
: undefined
|
|
30
|
+
|
|
31
|
+
const arrows = new InstancedArrows({ count: total, alpha, uniformColor })
|
|
32
|
+
map.set(entity, arrows)
|
|
33
|
+
arrows.update({ poses, colors, headAtPose })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* TODO: more granular updates here, but this should be fine for now.
|
|
38
|
+
*/
|
|
39
|
+
const onChange = (entity: Entity) => {
|
|
40
|
+
onRemove(entity)
|
|
41
|
+
onAdd(entity)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const onRemove = (entity: Entity) => {
|
|
45
|
+
map.delete(entity)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
$effect(() => {
|
|
49
|
+
const unsubAdd = world.onAdd(traits.Arrows, onAdd)
|
|
50
|
+
const unsubRemove = world.onRemove(traits.Arrows, onRemove)
|
|
51
|
+
const unsubPoseChange = world.onChange(traits.Arrows, onChange)
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
unsubAdd()
|
|
55
|
+
unsubRemove()
|
|
56
|
+
unsubPoseChange()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
{#each map as [entity, arrows] (entity)}
|
|
62
|
+
{@const events = useObjectEvents(() => entity)}
|
|
63
|
+
<Portal id={entity.get(traits.Parent)}>
|
|
64
|
+
<T
|
|
65
|
+
is={arrows}
|
|
66
|
+
name={entity}
|
|
67
|
+
>
|
|
68
|
+
<T
|
|
69
|
+
is={arrows.headMesh}
|
|
70
|
+
bvh={{ enabled: false }}
|
|
71
|
+
raycast={() => null}
|
|
72
|
+
/>
|
|
73
|
+
<T
|
|
74
|
+
is={arrows.shaftMesh}
|
|
75
|
+
bvh={{ enabled: false }}
|
|
76
|
+
raycast={meshBoundsRaycast}
|
|
77
|
+
{...events}
|
|
78
|
+
/>
|
|
79
|
+
</T>
|
|
80
|
+
</Portal>
|
|
81
|
+
{/each}
|
|
@@ -31,10 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
const { ...rest } = $props()
|
|
33
33
|
|
|
34
|
-
const dragPosition = new PersistedState<Vector2Like
|
|
35
|
-
'details-drag-position',
|
|
36
|
-
undefined
|
|
37
|
-
)
|
|
34
|
+
const dragPosition = new PersistedState<Vector2Like>('details-drag-position', { x: 0, y: 0 })
|
|
38
35
|
|
|
39
36
|
const resourceByName = useResourceByName()
|
|
40
37
|
const frames = useFrames()
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
const { camera } = useThrelte()
|
|
20
20
|
const settings = useSettings()
|
|
21
21
|
|
|
22
|
-
const name = useTrait(() => entity, traits.Name)
|
|
23
22
|
const parent = useTrait(() => entity, traits.Parent)
|
|
24
23
|
const pose = useTrait(() => entity, traits.Pose)
|
|
25
24
|
const geometry = useTrait(() => entity, traits.BufferGeometry)
|
|
@@ -122,9 +121,9 @@
|
|
|
122
121
|
<Portal id={parent.current}>
|
|
123
122
|
<T
|
|
124
123
|
is={points}
|
|
125
|
-
name={
|
|
126
|
-
{...events}
|
|
124
|
+
name={entity}
|
|
127
125
|
bvh={{ maxDepth: 40, maxLeafTris: 20 }}
|
|
126
|
+
{...events}
|
|
128
127
|
>
|
|
129
128
|
<T is={geometry.current} />
|
|
130
129
|
<T is={material} />
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import MeasureTool from './MeasureTool.svelte'
|
|
18
18
|
import PointerMissBox from './PointerMissBox.svelte'
|
|
19
19
|
import BatchedArrows from './BatchedArrows.svelte'
|
|
20
|
+
import Arrows from './Arrows.svelte'
|
|
20
21
|
|
|
21
22
|
interface Props {
|
|
22
23
|
children?: Snippet
|
|
@@ -95,6 +96,7 @@
|
|
|
95
96
|
|
|
96
97
|
<Entities />
|
|
97
98
|
<BatchedArrows />
|
|
99
|
+
<Arrows />
|
|
98
100
|
</T.Group>
|
|
99
101
|
|
|
100
102
|
{@render children?.()}
|
|
@@ -1,50 +1,54 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { T, useTask, useThrelte } from '@threlte/core'
|
|
2
|
+
import { isInstanceOf, T, useTask, useThrelte } from '@threlte/core'
|
|
3
3
|
import { useSelectedEntity, useSelectedObject3d } from '../hooks/useSelection.svelte'
|
|
4
4
|
import { OBBHelper } from '../three/OBBHelper'
|
|
5
5
|
import { OBB } from 'three/addons/math/OBB.js'
|
|
6
|
-
import { traits, useTrait } from '../ecs'
|
|
7
6
|
import { BatchedMesh, Box3 } from 'three'
|
|
7
|
+
import type { InstancedArrows } from '../three/InstancedArrows/InstancedArrows'
|
|
8
8
|
|
|
9
9
|
const box3 = new Box3()
|
|
10
10
|
const obb = new OBB()
|
|
11
11
|
const obbHelper = new OBBHelper()
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const { invalidate } = useThrelte()
|
|
14
14
|
const selectedEntity = useSelectedEntity()
|
|
15
15
|
const selectedObject3d = useSelectedObject3d()
|
|
16
|
-
const instance = useTrait(() => selectedEntity.current, traits.Instance)
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return
|
|
17
|
+
const object = $derived.by(() => {
|
|
18
|
+
if (!isInstanceOf(selectedObject3d.current, 'Mesh')) {
|
|
19
|
+
return selectedObject3d.current
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
// Create a clone in the case of meshes, which could be frames with geometries,
|
|
23
|
+
// so that our bounding box doesn't include children
|
|
24
|
+
const result = selectedObject3d.current?.clone(false)
|
|
25
|
+
|
|
26
|
+
if (result) {
|
|
27
|
+
selectedObject3d.current?.getWorldPosition(result.position)
|
|
28
|
+
selectedObject3d.current?.getWorldQuaternion(result.quaternion)
|
|
29
|
+
return result
|
|
30
|
+
}
|
|
25
31
|
})
|
|
26
32
|
|
|
27
33
|
const { start, stop } = useTask(
|
|
28
34
|
() => {
|
|
29
|
-
if (
|
|
35
|
+
if (object === undefined) {
|
|
30
36
|
return
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
if (
|
|
40
|
+
selectedEntity.instance &&
|
|
41
|
+
(isInstanceOf(object, 'BatchedMesh') || (object as InstancedArrows).isInstancedArrows)
|
|
42
|
+
) {
|
|
43
|
+
const mesh = object as BatchedMesh | InstancedArrows
|
|
44
|
+
mesh.getBoundingBoxAt(selectedEntity.instance, box3)
|
|
36
45
|
obb.fromBox3(box3)
|
|
37
46
|
obbHelper.setFromOBB(obb)
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
} else {
|
|
48
|
+
obbHelper.setFromObject(object)
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
selectedObject3d.current?.getWorldPosition(clone.position)
|
|
44
|
-
selectedObject3d.current?.getWorldQuaternion(clone.quaternion)
|
|
45
|
-
obbHelper.setFromObject(clone)
|
|
46
|
-
invalidate()
|
|
47
|
-
}
|
|
51
|
+
invalidate()
|
|
48
52
|
},
|
|
49
53
|
{
|
|
50
54
|
autoStart: false,
|
package/dist/ecs/traits.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export declare const Center: import("koota").Trait<{
|
|
|
30
30
|
oZ: number;
|
|
31
31
|
theta: number;
|
|
32
32
|
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Represents that an entity is composed of many instances, so that the treeview and
|
|
35
|
+
* details panel may display all instances
|
|
36
|
+
*/
|
|
37
|
+
export declare const Instanced: import("koota").TagTrait;
|
|
33
38
|
export declare const Instance: import("koota").Trait<{
|
|
34
39
|
meshID: number;
|
|
35
40
|
instanceID: number;
|
|
@@ -45,6 +50,14 @@ export declare const Color: import("koota").Trait<{
|
|
|
45
50
|
b: number;
|
|
46
51
|
}>;
|
|
47
52
|
export declare const Arrow: import("koota").TagTrait;
|
|
53
|
+
export declare const Positions: import("koota").Trait<() => Float32Array<ArrayBuffer>>;
|
|
54
|
+
export declare const Colors: import("koota").Trait<() => Uint8Array<ArrayBuffer>>;
|
|
55
|
+
export declare const Instances: import("koota").Trait<{
|
|
56
|
+
count: number;
|
|
57
|
+
}>;
|
|
58
|
+
export declare const Arrows: import("koota").Trait<{
|
|
59
|
+
headAtPose: boolean;
|
|
60
|
+
}>;
|
|
48
61
|
/**
|
|
49
62
|
* Render entity as points
|
|
50
63
|
*/
|
package/dist/ecs/traits.js
CHANGED
|
@@ -8,6 +8,11 @@ export const Parent = trait(() => 'world');
|
|
|
8
8
|
export const Pose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
|
|
9
9
|
export const EditedPose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
|
|
10
10
|
export const Center = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
|
|
11
|
+
/**
|
|
12
|
+
* Represents that an entity is composed of many instances, so that the treeview and
|
|
13
|
+
* details panel may display all instances
|
|
14
|
+
*/
|
|
15
|
+
export const Instanced = trait();
|
|
11
16
|
export const Instance = trait({
|
|
12
17
|
meshID: -1,
|
|
13
18
|
instanceID: -1,
|
|
@@ -19,6 +24,14 @@ export const Opacity = trait(() => 1);
|
|
|
19
24
|
*/
|
|
20
25
|
export const Color = trait({ r: 0, g: 0, b: 0 });
|
|
21
26
|
export const Arrow = trait();
|
|
27
|
+
export const Positions = trait(() => new Float32Array());
|
|
28
|
+
export const Colors = trait(() => new Uint8Array());
|
|
29
|
+
export const Instances = trait({
|
|
30
|
+
count: 0,
|
|
31
|
+
});
|
|
32
|
+
export const Arrows = trait({
|
|
33
|
+
headAtPose: true,
|
|
34
|
+
});
|
|
22
35
|
/**
|
|
23
36
|
* Render entity as points
|
|
24
37
|
*/
|
|
@@ -13,6 +13,7 @@ import { useLogs } from './useLogs.svelte';
|
|
|
13
13
|
import { createBox, createCapsule, createSphere } from '../geometry';
|
|
14
14
|
import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
|
|
15
15
|
import { createBufferGeometry, updateBufferGeometry } from '../attribute';
|
|
16
|
+
import { STRIDE } from '../buffer';
|
|
16
17
|
const colorUtil = new Color();
|
|
17
18
|
const bufferTypes = {
|
|
18
19
|
DRAW_POINTS: 0,
|
|
@@ -45,25 +46,50 @@ const lowercaseKeys = (obj) => {
|
|
|
45
46
|
}
|
|
46
47
|
return obj;
|
|
47
48
|
};
|
|
48
|
-
class
|
|
49
|
+
class BinaryReader {
|
|
49
50
|
littleEndian = true;
|
|
50
|
-
|
|
51
|
-
buffer = new ArrayBuffer();
|
|
52
|
-
array = new Float32Array();
|
|
51
|
+
offsetBytes = 0;
|
|
52
|
+
buffer = new ArrayBuffer(0);
|
|
53
53
|
view = new DataView(this.buffer);
|
|
54
54
|
header = { requestID: '', type: -1 };
|
|
55
55
|
async init(data) {
|
|
56
56
|
this.buffer = await data.arrayBuffer();
|
|
57
|
-
this.
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
this.view = new DataView(this.buffer);
|
|
58
|
+
this.offsetBytes = 0;
|
|
59
|
+
// 16-byte UUID
|
|
60
|
+
const uuidBytes = new Uint8Array(this.buffer, 0, 16);
|
|
61
|
+
this.header.requestID = UuidTool.toString([...uuidBytes]);
|
|
62
|
+
// 4-byte float32 type at byte offset 16
|
|
63
|
+
this.header.type = this.view.getFloat32(16, this.littleEndian);
|
|
64
|
+
// payload starts after 20 bytes
|
|
65
|
+
this.offsetBytes = 20;
|
|
61
66
|
return this;
|
|
62
67
|
}
|
|
68
|
+
/** Read one float32 and advance. */
|
|
63
69
|
read() {
|
|
64
|
-
const
|
|
65
|
-
this.
|
|
66
|
-
return
|
|
70
|
+
const v = this.view.getFloat32(this.offsetBytes, this.littleEndian);
|
|
71
|
+
this.offsetBytes += 4;
|
|
72
|
+
return v;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get a Float32Array VIEW into the underlying buffer (no copy) and advance.
|
|
76
|
+
* Requires current offset to be 4-byte aligned (it will be, if you only readF32 so far).
|
|
77
|
+
*/
|
|
78
|
+
readF32Array(count) {
|
|
79
|
+
const byteOffset = this.offsetBytes;
|
|
80
|
+
const byteLength = count * 4;
|
|
81
|
+
const arr = new Float32Array(this.buffer, byteOffset, count);
|
|
82
|
+
this.offsetBytes += byteLength;
|
|
83
|
+
return arr;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get a Uint8Array VIEW (no copy) and advance.
|
|
87
|
+
*/
|
|
88
|
+
readU8Array(count) {
|
|
89
|
+
const byteOffset = this.offsetBytes;
|
|
90
|
+
const arr = new Uint8Array(this.buffer, byteOffset, count);
|
|
91
|
+
this.offsetBytes += count;
|
|
92
|
+
return arr;
|
|
67
93
|
}
|
|
68
94
|
}
|
|
69
95
|
export const provideDrawAPI = () => {
|
|
@@ -80,8 +106,6 @@ export const provideDrawAPI = () => {
|
|
|
80
106
|
const maxReconnectDelay = 5_000;
|
|
81
107
|
let ws;
|
|
82
108
|
let connectionStatus = $state('connecting');
|
|
83
|
-
const direction = new Vector3();
|
|
84
|
-
const origin = new Vector3();
|
|
85
109
|
const loader = new GLTFLoader();
|
|
86
110
|
const entities = new Map();
|
|
87
111
|
const sendResponse = (response) => {
|
|
@@ -181,34 +205,14 @@ export const provideDrawAPI = () => {
|
|
|
181
205
|
entities.set(name, entity);
|
|
182
206
|
};
|
|
183
207
|
const vec3 = new Vector3();
|
|
184
|
-
const pose = createPose();
|
|
185
208
|
const drawPoses = async (reader) => {
|
|
186
209
|
// Read counts
|
|
187
210
|
const nPoints = reader.read();
|
|
188
211
|
const nColors = reader.read();
|
|
189
212
|
const arrowHeadAtPose = reader.read();
|
|
190
213
|
const entities = [];
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
direction.set(reader.read(), reader.read(), reader.read());
|
|
194
|
-
if (arrowHeadAtPose === 1) {
|
|
195
|
-
// Compute the base position so the arrow ends at the origin
|
|
196
|
-
origin.sub(vec3.copy(direction).multiplyScalar(/** arrow length */ 100));
|
|
197
|
-
}
|
|
198
|
-
pose.x = origin.x;
|
|
199
|
-
pose.y = origin.y;
|
|
200
|
-
pose.z = origin.z;
|
|
201
|
-
pose.oX = direction.x;
|
|
202
|
-
pose.oY = direction.y;
|
|
203
|
-
pose.oZ = direction.z;
|
|
204
|
-
const entity = world.spawn(traits.Name(`Pose ${++poseIndex}`), traits.Pose(pose), traits.Color, traits.DrawAPI, traits.Arrow);
|
|
205
|
-
entities.push(entity);
|
|
206
|
-
}
|
|
207
|
-
for (let i = 0; i < nColors; i += 1) {
|
|
208
|
-
const entity = entities[i];
|
|
209
|
-
colorUtil.set(reader.read(), reader.read(), reader.read());
|
|
210
|
-
entity.set(traits.Color, colorUtil);
|
|
211
|
-
}
|
|
214
|
+
const entity = world.spawn(traits.Name(`Arrow group ${++poseIndex}`), traits.Positions(reader.readF32Array(nPoints * STRIDE.ARROWS)), traits.Colors(reader.readU8Array(nColors * STRIDE.COLORS_RGB)), traits.Arrows({ headAtPose: arrowHeadAtPose === 1 }), traits.DrawAPI);
|
|
215
|
+
entities.push(entity);
|
|
212
216
|
};
|
|
213
217
|
const drawPoints = async (reader) => {
|
|
214
218
|
// Read label length
|
|
@@ -225,11 +229,9 @@ export const provideDrawAPI = () => {
|
|
|
225
229
|
const g = reader.read();
|
|
226
230
|
const b = reader.read();
|
|
227
231
|
const nPointsElements = nPoints * 3;
|
|
228
|
-
const positions = reader.
|
|
229
|
-
reader.offset += nPointsElements;
|
|
232
|
+
const positions = reader.readF32Array(nPointsElements);
|
|
230
233
|
const nColorsElements = nColors * 3;
|
|
231
|
-
const rawColors = reader.
|
|
232
|
-
reader.offset += nColorsElements;
|
|
234
|
+
const rawColors = reader.readF32Array(nColorsElements);
|
|
233
235
|
const colors = new Float32Array(nPointsElements);
|
|
234
236
|
colors.set(rawColors);
|
|
235
237
|
// Cover the gap for any points not colored
|
|
@@ -347,7 +349,7 @@ export const provideDrawAPI = () => {
|
|
|
347
349
|
let requestID = '';
|
|
348
350
|
try {
|
|
349
351
|
if (typeof event.data === 'object' && 'arrayBuffer' in event.data) {
|
|
350
|
-
const reader = await new
|
|
352
|
+
const reader = await new BinaryReader().init(event.data);
|
|
351
353
|
requestID = reader.header.requestID;
|
|
352
354
|
const { type } = reader.header;
|
|
353
355
|
if (type === bufferTypes.DRAW_POINTS) {
|
|
@@ -26,7 +26,7 @@ export const useObjectEvents = (entity) => {
|
|
|
26
26
|
},
|
|
27
27
|
ondblclick: (event) => {
|
|
28
28
|
event.stopPropagation();
|
|
29
|
-
focusedEntity.set(currentEntity);
|
|
29
|
+
focusedEntity.set(currentEntity, event.instanceId ?? event.batchId);
|
|
30
30
|
},
|
|
31
31
|
onpointerdown: (event) => {
|
|
32
32
|
down.copy(event.pointer);
|
|
@@ -34,7 +34,7 @@ export const useObjectEvents = (entity) => {
|
|
|
34
34
|
onclick: (event) => {
|
|
35
35
|
event.stopPropagation();
|
|
36
36
|
if (down.distanceToSquared(event.pointer) < 0.1) {
|
|
37
|
-
selectedEntity.set(currentEntity);
|
|
37
|
+
selectedEntity.set(currentEntity, event.instanceId ?? event.batchId);
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
};
|
|
@@ -10,6 +10,7 @@ import { RefetchRates } from '../components/RefreshRate.svelte';
|
|
|
10
10
|
import { useLogs } from './useLogs.svelte';
|
|
11
11
|
import { useResourceByName } from './useResourceByName.svelte';
|
|
12
12
|
import { useRefetchPoses } from './useRefetchPoses';
|
|
13
|
+
const origingFrameComponentTypes = ['arm', 'gantry', 'gripper'];
|
|
13
14
|
export const usePose = (name, parent) => {
|
|
14
15
|
const environment = useEnvironment();
|
|
15
16
|
const logs = useLogs();
|
|
@@ -25,10 +26,10 @@ export const usePose = (name, parent) => {
|
|
|
25
26
|
const frames = useFrames();
|
|
26
27
|
let pose = $state(undefined);
|
|
27
28
|
const interval = $derived(refreshRates.get(RefreshRates.poses));
|
|
28
|
-
const resolvedParent = $derived(parentResource?.subtype
|
|
29
|
+
const resolvedParent = $derived(origingFrameComponentTypes.includes(parentResource?.subtype ?? '')
|
|
29
30
|
? `${parent()}_origin`
|
|
30
31
|
: parent());
|
|
31
|
-
const resolvedName = $derived(resource?.subtype
|
|
32
|
+
const resolvedName = $derived(origingFrameComponentTypes.includes(resource?.subtype ?? '')
|
|
32
33
|
? `${currentName}_origin`
|
|
33
34
|
: currentName);
|
|
34
35
|
const query = createRobotQuery(robotClient, 'getPose', () => [resolvedName, resolvedParent ?? 'world', []], () => ({
|
|
@@ -2,20 +2,24 @@ import { Object3D } from 'three';
|
|
|
2
2
|
import type { Entity } from 'koota';
|
|
3
3
|
interface SelectedEntityContext {
|
|
4
4
|
readonly current: Entity | undefined;
|
|
5
|
-
|
|
5
|
+
readonly instance: number | undefined;
|
|
6
|
+
set(entity?: Entity, instance?: number): void;
|
|
6
7
|
}
|
|
7
8
|
interface FocusedEntityContext {
|
|
8
9
|
readonly current: Entity | undefined;
|
|
9
|
-
|
|
10
|
+
readonly instance: number | undefined;
|
|
11
|
+
set(entity?: Entity, instance?: number): void;
|
|
10
12
|
}
|
|
11
13
|
export declare const provideSelection: () => {
|
|
12
14
|
selection: {
|
|
13
15
|
readonly current: Entity | undefined;
|
|
14
|
-
|
|
16
|
+
readonly instance: number | undefined;
|
|
17
|
+
set(entity: Entity, instance?: number): void;
|
|
15
18
|
};
|
|
16
19
|
focus: {
|
|
17
20
|
readonly current: Entity | undefined;
|
|
18
|
-
|
|
21
|
+
readonly instance: number | undefined;
|
|
22
|
+
set(entity: Entity, instance?: number): void;
|
|
19
23
|
};
|
|
20
24
|
};
|
|
21
25
|
export declare const useFocusedEntity: () => FocusedEntityContext;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isInstanceOf, useThrelte } from '@threlte/core';
|
|
2
2
|
import { getContext, setContext } from 'svelte';
|
|
3
|
-
import {
|
|
4
|
-
import { traits,
|
|
3
|
+
import { Object3D } from 'three';
|
|
4
|
+
import { traits, useWorld } from '../ecs';
|
|
5
5
|
const selectedKey = Symbol('selected-frame-context');
|
|
6
6
|
const focusedKey = Symbol('focused-frame-context');
|
|
7
7
|
const focusedObject3dKey = Symbol('focused-object-3d-context');
|
|
@@ -9,7 +9,9 @@ export const provideSelection = () => {
|
|
|
9
9
|
const world = useWorld();
|
|
10
10
|
const { scene } = useThrelte();
|
|
11
11
|
let selected = $state.raw();
|
|
12
|
+
let selectedInstance = $state();
|
|
12
13
|
let focused = $state.raw();
|
|
14
|
+
let focusedInstance = $state();
|
|
13
15
|
$effect(() => {
|
|
14
16
|
return world.onRemove(traits.Name, (entity) => {
|
|
15
17
|
if (entity === selected)
|
|
@@ -22,8 +24,12 @@ export const provideSelection = () => {
|
|
|
22
24
|
get current() {
|
|
23
25
|
return selected;
|
|
24
26
|
},
|
|
25
|
-
|
|
27
|
+
get instance() {
|
|
28
|
+
return selectedInstance;
|
|
29
|
+
},
|
|
30
|
+
set(entity, instance) {
|
|
26
31
|
selected = entity;
|
|
32
|
+
selectedInstance = instance;
|
|
27
33
|
},
|
|
28
34
|
};
|
|
29
35
|
setContext(selectedKey, selectedEntityContext);
|
|
@@ -31,8 +37,12 @@ export const provideSelection = () => {
|
|
|
31
37
|
get current() {
|
|
32
38
|
return focused;
|
|
33
39
|
},
|
|
34
|
-
|
|
40
|
+
get instance() {
|
|
41
|
+
return focusedInstance;
|
|
42
|
+
},
|
|
43
|
+
set(entity, instance) {
|
|
35
44
|
focused = entity;
|
|
45
|
+
focusedInstance = instance;
|
|
36
46
|
},
|
|
37
47
|
};
|
|
38
48
|
setContext(focusedKey, focusedEntityContext);
|
|
@@ -68,27 +78,14 @@ export const useSelectedEntity = () => {
|
|
|
68
78
|
export const useFocusedObject3d = () => {
|
|
69
79
|
return getContext(focusedObject3dKey);
|
|
70
80
|
};
|
|
71
|
-
const matrix = new Matrix4();
|
|
72
81
|
export const useSelectedObject3d = () => {
|
|
73
82
|
const selectedEntity = useSelectedEntity();
|
|
74
83
|
const { scene } = useThrelte();
|
|
75
|
-
const name = useTrait(() => selectedEntity.current, traits.Name);
|
|
76
|
-
const instance = useTrait(() => selectedEntity.current, traits.Instance);
|
|
77
84
|
const object = $derived.by(() => {
|
|
78
85
|
if (!selectedEntity.current) {
|
|
79
86
|
return;
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
const proxy = new Object3D();
|
|
83
|
-
const mesh = scene.getObjectById(instance.current.meshID);
|
|
84
|
-
mesh?.getMatrixAt(instance.current.instanceID, matrix);
|
|
85
|
-
proxy.applyMatrix4(matrix);
|
|
86
|
-
return proxy;
|
|
87
|
-
}
|
|
88
|
-
if (!name.current) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
return scene.getObjectByName(name.current);
|
|
88
|
+
return scene.getObjectByName(selectedEntity.current);
|
|
92
89
|
});
|
|
93
90
|
return {
|
|
94
91
|
get current() {
|
package/dist/snapshot.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Vector3, Vector4 } from 'three';
|
|
2
2
|
import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
|
|
3
3
|
import { RenderArmModels } from './draw/v1/scene_pb';
|
|
4
4
|
import {} from './draw/v1/drawing_pb';
|
|
@@ -7,13 +7,8 @@ import { Geometry } from '@viamrobotics/sdk';
|
|
|
7
7
|
import { parseMetadata } from './WorldObject.svelte';
|
|
8
8
|
import { rgbaBytesToFloat32, rgbaToHex } from './color';
|
|
9
9
|
import { asFloat32Array, STRIDE } from './buffer';
|
|
10
|
-
import { createPose } from './transform';
|
|
11
10
|
import { createBufferGeometry } from './attribute';
|
|
12
11
|
const vec3 = new Vector3();
|
|
13
|
-
const origin = new Vector3();
|
|
14
|
-
const direction = new Vector3();
|
|
15
|
-
const color = new Color();
|
|
16
|
-
const pose = createPose();
|
|
17
12
|
export const applySceneMetadata = (settings, metadata) => {
|
|
18
13
|
const next = { ...settings };
|
|
19
14
|
if (metadata.grid !== undefined) {
|
|
@@ -109,48 +104,21 @@ const spawnEntitiesFromDrawing = (world, drawing) => {
|
|
|
109
104
|
const parent = poseInFrame?.referenceFrame;
|
|
110
105
|
const { geometryType } = drawing.physicalObject ?? {};
|
|
111
106
|
if (geometryType?.case === 'arrows') {
|
|
112
|
-
const
|
|
107
|
+
const poses = asFloat32Array(geometryType.value.poses);
|
|
108
|
+
const colors = drawing.metadata?.colors;
|
|
109
|
+
const entityTraits = [
|
|
113
110
|
traits.Name(drawing.referenceFrame),
|
|
114
111
|
traits.Pose(poseInFrame?.pose),
|
|
115
|
-
traits.
|
|
112
|
+
traits.Positions(poses),
|
|
116
113
|
];
|
|
117
114
|
if (parent) {
|
|
118
|
-
|
|
115
|
+
entityTraits.push(traits.Parent(parent));
|
|
119
116
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const poses = asFloat32Array(geometryType.value.poses);
|
|
123
|
-
const colors = drawing.metadata?.colors
|
|
124
|
-
? asFloat32Array(drawing.metadata.colors)
|
|
125
|
-
: [];
|
|
126
|
-
for (let i = 0, j = 0, k = 0, l = poses.length; i < l; i += STRIDE.ARROWS, j += 1, k += 4) {
|
|
127
|
-
const entityTraits = [
|
|
128
|
-
traits.Name(`pose ${j}`),
|
|
129
|
-
traits.Parent(drawing.referenceFrame),
|
|
130
|
-
];
|
|
131
|
-
origin.set(poses[i + 0], poses[i + 1], poses[i + 2]);
|
|
132
|
-
direction.set(poses[i + 3], poses[i + 4], poses[i + 5]);
|
|
133
|
-
// Compute the base position so the arrow ends at the origin
|
|
134
|
-
origin.sub(vec3.copy(direction).multiplyScalar(/** arrow length */ 100));
|
|
135
|
-
pose.x = origin.x;
|
|
136
|
-
pose.y = origin.y;
|
|
137
|
-
pose.z = origin.z;
|
|
138
|
-
pose.oX = direction.x;
|
|
139
|
-
pose.oY = direction.y;
|
|
140
|
-
pose.oZ = direction.z;
|
|
141
|
-
entityTraits.push(traits.Pose(pose));
|
|
142
|
-
if (colors[k + 0] && colors[k + 1] && colors[k + 2]) {
|
|
143
|
-
color.r = colors[k + 0];
|
|
144
|
-
color.g = colors[k + 1];
|
|
145
|
-
color.b = colors[k + 2];
|
|
146
|
-
entityTraits.push(traits.Color(color));
|
|
147
|
-
}
|
|
148
|
-
if (colors[k + 3]) {
|
|
149
|
-
entityTraits.push(traits.Opacity(colors[k + 3]));
|
|
150
|
-
}
|
|
151
|
-
const entity = world.spawn(...entityTraits, traits.Arrow, traits.SnapshotAPI);
|
|
152
|
-
entities.push(entity);
|
|
117
|
+
if (colors) {
|
|
118
|
+
entityTraits.push(traits.Colors(colors));
|
|
153
119
|
}
|
|
120
|
+
const entity = world.spawn(...entityTraits, traits.SnapshotAPI, traits.Arrows({ headAtPose: true }), traits.Instances({ count: poses.length / STRIDE.ARROWS }));
|
|
121
|
+
entities.push(entity);
|
|
154
122
|
}
|
|
155
123
|
else if (geometryType?.case === 'model') {
|
|
156
124
|
const rootEntityTraits = [
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Group, Mesh, BufferGeometry, InstancedInterleavedBuffer, type ColorRepresentation, Vector3, Box3 } from 'three';
|
|
2
|
+
export declare class InstancedArrows extends Group {
|
|
3
|
+
isInstancedArrows: boolean;
|
|
4
|
+
count: number;
|
|
5
|
+
arrowLength: number;
|
|
6
|
+
shaftRadius: number;
|
|
7
|
+
headLength: number;
|
|
8
|
+
headWidth: number;
|
|
9
|
+
shaftMesh: Mesh;
|
|
10
|
+
headMesh: Mesh;
|
|
11
|
+
attributes: BufferGeometry['attributes'];
|
|
12
|
+
poses: InstancedInterleavedBuffer;
|
|
13
|
+
constructor(options?: {
|
|
14
|
+
count?: number;
|
|
15
|
+
length?: number;
|
|
16
|
+
shaftRadius?: number;
|
|
17
|
+
headLength?: number;
|
|
18
|
+
headWidth?: number;
|
|
19
|
+
alpha?: boolean;
|
|
20
|
+
uniformColor?: ColorRepresentation;
|
|
21
|
+
});
|
|
22
|
+
update(arrows: {
|
|
23
|
+
poses?: Float32Array;
|
|
24
|
+
colors?: Uint8Array;
|
|
25
|
+
headAtPose?: boolean;
|
|
26
|
+
}): void;
|
|
27
|
+
getBoundingBoxAt(instanceId: number, target: Box3): Box3;
|
|
28
|
+
getPoseAt(instanceID: number, origin: Vector3, direction: Vector3): void;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { RawShaderMaterial, FrontSide, Group, InstancedBufferAttribute, DynamicDrawUsage, Mesh, BufferGeometry, InstancedInterleavedBuffer, InterleavedBufferAttribute, Material, Color, Vector3, Box3, } from 'three';
|
|
2
|
+
import vertexShader from './vertex.glsl?raw';
|
|
3
|
+
import fragmentShader from './fragment.glsl?raw';
|
|
4
|
+
import { createHeadGeometry, createShaftGeometry, toInstanced } from './geometry';
|
|
5
|
+
import { computeBoundingBox } from './box';
|
|
6
|
+
const defaults = {
|
|
7
|
+
LENGTH: 0.1,
|
|
8
|
+
HEAD_LENGTH: 0.02,
|
|
9
|
+
HEAD_WIDTH: 0.005,
|
|
10
|
+
SHAFT_RADIUS: 0.001,
|
|
11
|
+
};
|
|
12
|
+
const origin = new Vector3();
|
|
13
|
+
const direction = new Vector3();
|
|
14
|
+
const min = new Vector3();
|
|
15
|
+
const max = new Vector3();
|
|
16
|
+
const createMaterial = (options) => {
|
|
17
|
+
return new RawShaderMaterial({
|
|
18
|
+
vertexShader,
|
|
19
|
+
fragmentShader,
|
|
20
|
+
uniforms: {
|
|
21
|
+
headAtOrigin: { value: 1.0 },
|
|
22
|
+
shaftRadius: { value: defaults.SHAFT_RADIUS },
|
|
23
|
+
headLength: { value: defaults.HEAD_LENGTH },
|
|
24
|
+
headWidth: { value: defaults.HEAD_WIDTH },
|
|
25
|
+
arrowLength: { value: defaults.LENGTH },
|
|
26
|
+
minimumArrowLength: { value: 1e-6 },
|
|
27
|
+
uniformColor: { value: new Color() },
|
|
28
|
+
},
|
|
29
|
+
defines: {
|
|
30
|
+
...(options.isHead ? { IS_HEAD: 1 } : {}),
|
|
31
|
+
...(options.useColorAttribute ? { USE_COLOR_ATTRIBUTE: 1 } : {}),
|
|
32
|
+
},
|
|
33
|
+
side: FrontSide,
|
|
34
|
+
depthTest: true,
|
|
35
|
+
depthWrite: true,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
export class InstancedArrows extends Group {
|
|
39
|
+
isInstancedArrows = true;
|
|
40
|
+
count;
|
|
41
|
+
arrowLength;
|
|
42
|
+
shaftRadius;
|
|
43
|
+
headLength;
|
|
44
|
+
headWidth;
|
|
45
|
+
shaftMesh;
|
|
46
|
+
headMesh;
|
|
47
|
+
attributes;
|
|
48
|
+
poses;
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
super();
|
|
51
|
+
this.count = options?.count ?? 0;
|
|
52
|
+
this.shaftRadius = options?.shaftRadius ?? defaults.SHAFT_RADIUS;
|
|
53
|
+
this.headLength = options?.headLength ?? defaults.HEAD_LENGTH;
|
|
54
|
+
this.headWidth = options?.headWidth ?? defaults.HEAD_WIDTH;
|
|
55
|
+
this.arrowLength = options?.length ?? defaults.LENGTH;
|
|
56
|
+
const stride = 6;
|
|
57
|
+
const posesInterleaved = new Float32Array(this.count * stride);
|
|
58
|
+
this.poses = new InstancedInterleavedBuffer(posesInterleaved, stride);
|
|
59
|
+
this.poses.setUsage(DynamicDrawUsage);
|
|
60
|
+
const instanceOrigin = new InterleavedBufferAttribute(this.poses, 3, 0, false);
|
|
61
|
+
const instanceDirection = new InterleavedBufferAttribute(this.poses, 3, 3, false);
|
|
62
|
+
this.attributes = {
|
|
63
|
+
instanceOrigin,
|
|
64
|
+
instanceDirection,
|
|
65
|
+
};
|
|
66
|
+
if (!options.uniformColor) {
|
|
67
|
+
const colors = new Uint8Array(this.count * (options?.alpha ? 4 : 3));
|
|
68
|
+
const instanceColor = new InstancedBufferAttribute(colors, options?.alpha ? 4 : 3, true);
|
|
69
|
+
instanceColor.setUsage(DynamicDrawUsage);
|
|
70
|
+
this.attributes.instanceColor = instanceColor;
|
|
71
|
+
}
|
|
72
|
+
const shaftGeometry = toInstanced(createShaftGeometry(), this.count, this.attributes);
|
|
73
|
+
shaftGeometry.computeBoundingBox = computeBoundingBox.bind(this, shaftGeometry);
|
|
74
|
+
const headGeometry = toInstanced(createHeadGeometry(), this.count, this.attributes);
|
|
75
|
+
headGeometry.computeBoundingBox = computeBoundingBox.bind(this, headGeometry);
|
|
76
|
+
const shaftMaterial = createMaterial({
|
|
77
|
+
isHead: false,
|
|
78
|
+
useColorAttribute: !options.uniformColor,
|
|
79
|
+
});
|
|
80
|
+
const headMaterial = createMaterial({
|
|
81
|
+
isHead: true,
|
|
82
|
+
useColorAttribute: !options.uniformColor,
|
|
83
|
+
});
|
|
84
|
+
for (const { uniforms } of [shaftMaterial, headMaterial]) {
|
|
85
|
+
uniforms.shaftRadius.value = this.shaftRadius;
|
|
86
|
+
uniforms.headLength.value = this.headLength;
|
|
87
|
+
uniforms.headWidth.value = this.headWidth;
|
|
88
|
+
uniforms.arrowLength.value = this.arrowLength;
|
|
89
|
+
if (options.uniformColor) {
|
|
90
|
+
uniforms.uniformColor.value.set(options.uniformColor);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
this.shaftMesh = new Mesh(shaftGeometry, shaftMaterial);
|
|
94
|
+
this.headMesh = new Mesh(headGeometry, headMaterial);
|
|
95
|
+
this.shaftMesh.frustumCulled = false;
|
|
96
|
+
this.headMesh.frustumCulled = false;
|
|
97
|
+
this.shaftMesh.raycast = () => null;
|
|
98
|
+
this.headMesh.raycast = () => null;
|
|
99
|
+
this.add(this.shaftMesh, this.headMesh);
|
|
100
|
+
}
|
|
101
|
+
update(arrows) {
|
|
102
|
+
if (arrows.poses) {
|
|
103
|
+
this.poses.array.set(arrows.poses);
|
|
104
|
+
this.poses.needsUpdate = true;
|
|
105
|
+
}
|
|
106
|
+
if (arrows.colors && this.attributes.instanceColor) {
|
|
107
|
+
this.attributes.instanceColor.array.set(arrows.colors);
|
|
108
|
+
this.attributes.instanceColor.needsUpdate = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
getBoundingBoxAt(instanceId, target) {
|
|
112
|
+
this.getPoseAt(instanceId, origin, direction);
|
|
113
|
+
const r = Math.max(this.shaftRadius, this.headWidth);
|
|
114
|
+
const directionLength = direction.length();
|
|
115
|
+
if (directionLength > 0)
|
|
116
|
+
direction.multiplyScalar(1 / directionLength);
|
|
117
|
+
else
|
|
118
|
+
direction.set(0, 1, 0);
|
|
119
|
+
direction.multiplyScalar(this.arrowLength).add(origin);
|
|
120
|
+
min.set(Math.min(origin.x, direction.x) - r, Math.min(origin.y, direction.y) - r, Math.min(origin.z, direction.z) - r);
|
|
121
|
+
max.set(Math.max(origin.x, direction.x) + r, Math.max(origin.y, direction.y) + r, Math.max(origin.z, direction.z) + r);
|
|
122
|
+
target.min.copy(min);
|
|
123
|
+
target.max.copy(max);
|
|
124
|
+
return target;
|
|
125
|
+
}
|
|
126
|
+
getPoseAt(instanceID, origin, direction) {
|
|
127
|
+
const i = instanceID * 6;
|
|
128
|
+
const { array } = this.poses;
|
|
129
|
+
origin.set(array[i], array[i + 1], array[i + 2]);
|
|
130
|
+
direction.set(array[i + 3], array[i + 4], array[i + 5]);
|
|
131
|
+
}
|
|
132
|
+
dispose() {
|
|
133
|
+
this.shaftMesh.geometry.dispose();
|
|
134
|
+
this.headMesh.geometry.dispose();
|
|
135
|
+
this.shaftMesh.material.dispose();
|
|
136
|
+
this.headMesh.material.dispose();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { RawShaderMaterial, Box3, BufferGeometry } from 'three';
|
|
2
|
+
const bounds = new Box3();
|
|
3
|
+
export function computeBoundingBox(geometry) {
|
|
4
|
+
const src = this.poses.array;
|
|
5
|
+
const poseScale = this.shaftMesh.material.uniforms.poseScale?.value ?? 0.001;
|
|
6
|
+
const headAtOrigin = this.shaftMesh.material.uniforms.headAtOrigin?.value ?? 1.0;
|
|
7
|
+
const r = Math.max(this.shaftRadius, this.headWidth);
|
|
8
|
+
let minX = +Infinity;
|
|
9
|
+
let minY = +Infinity;
|
|
10
|
+
let minZ = +Infinity;
|
|
11
|
+
let maxX = -Infinity;
|
|
12
|
+
let maxY = -Infinity;
|
|
13
|
+
let maxZ = -Infinity;
|
|
14
|
+
for (let i = 0, l = src.length; i < l; i += 6) {
|
|
15
|
+
// origin in rendered units
|
|
16
|
+
const ox = src[i + 0] * poseScale;
|
|
17
|
+
const oy = src[i + 1] * poseScale;
|
|
18
|
+
const oz = src[i + 2] * poseScale;
|
|
19
|
+
// normalize direction
|
|
20
|
+
let dx = src[i + 3];
|
|
21
|
+
let dy = src[i + 4];
|
|
22
|
+
let dz = src[i + 5];
|
|
23
|
+
const dLen = Math.hypot(dx, dy, dz);
|
|
24
|
+
if (dLen > 0) {
|
|
25
|
+
const inv = 1 / dLen;
|
|
26
|
+
dx *= inv;
|
|
27
|
+
dy *= inv;
|
|
28
|
+
dz *= inv;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
dx = 0;
|
|
32
|
+
dy = 1;
|
|
33
|
+
dz = 0;
|
|
34
|
+
}
|
|
35
|
+
// segment endpoints
|
|
36
|
+
let ax, ay, az;
|
|
37
|
+
let bx, by, bz;
|
|
38
|
+
if (headAtOrigin > 0.5) {
|
|
39
|
+
// origin is tip
|
|
40
|
+
bx = ox;
|
|
41
|
+
by = oy;
|
|
42
|
+
bz = oz;
|
|
43
|
+
ax = ox - dx * this.arrowLength;
|
|
44
|
+
ay = oy - dy * this.arrowLength;
|
|
45
|
+
az = oz - dz * this.arrowLength;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// origin is tail
|
|
49
|
+
ax = ox;
|
|
50
|
+
ay = oy;
|
|
51
|
+
az = oz;
|
|
52
|
+
bx = ox + dx * this.arrowLength;
|
|
53
|
+
by = oy + dy * this.arrowLength;
|
|
54
|
+
bz = oz + dz * this.arrowLength;
|
|
55
|
+
}
|
|
56
|
+
// expand with both endpoints
|
|
57
|
+
if (ax < minX)
|
|
58
|
+
minX = ax;
|
|
59
|
+
if (ay < minY)
|
|
60
|
+
minY = ay;
|
|
61
|
+
if (az < minZ)
|
|
62
|
+
minZ = az;
|
|
63
|
+
if (ax > maxX)
|
|
64
|
+
maxX = ax;
|
|
65
|
+
if (ay > maxY)
|
|
66
|
+
maxY = ay;
|
|
67
|
+
if (az > maxZ)
|
|
68
|
+
maxZ = az;
|
|
69
|
+
if (bx < minX)
|
|
70
|
+
minX = bx;
|
|
71
|
+
if (by < minY)
|
|
72
|
+
minY = by;
|
|
73
|
+
if (bz < minZ)
|
|
74
|
+
minZ = bz;
|
|
75
|
+
if (bx > maxX)
|
|
76
|
+
maxX = bx;
|
|
77
|
+
if (by > maxY)
|
|
78
|
+
maxY = by;
|
|
79
|
+
if (bz > maxZ)
|
|
80
|
+
maxZ = bz;
|
|
81
|
+
}
|
|
82
|
+
// pad by radius so the box contains arrow thickness
|
|
83
|
+
minX -= r;
|
|
84
|
+
minY -= r;
|
|
85
|
+
minZ -= r;
|
|
86
|
+
maxX += r;
|
|
87
|
+
maxY += r;
|
|
88
|
+
maxZ += r;
|
|
89
|
+
bounds.min.set(minX, minY, minZ);
|
|
90
|
+
bounds.max.set(maxX, maxY, maxZ);
|
|
91
|
+
geometry.boundingBox = bounds.clone();
|
|
92
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { BufferGeometry, InstancedBufferGeometry } from 'three';
|
|
2
|
+
export declare const createShaftGeometry: () => BufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>;
|
|
3
|
+
export declare const createHeadGeometry: () => BufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>;
|
|
4
|
+
export declare const toInstanced: (baseGeometry: BufferGeometry, instanceCount: number, attributes: BufferGeometry["attributes"]) => InstancedBufferGeometry;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BufferGeometry, BufferAttribute, InstancedBufferGeometry } from 'three';
|
|
2
|
+
export const createShaftGeometry = () => {
|
|
3
|
+
// Triangular prism aligned to +Y, base at y=0, top at y=1.
|
|
4
|
+
// No caps, 6 verts, 6 side triangles.
|
|
5
|
+
const positions = new Float32Array([
|
|
6
|
+
// bottom (y=0)
|
|
7
|
+
1, 0, 0,
|
|
8
|
+
-0.5, 0, 0.8660254,
|
|
9
|
+
-0.5, 0, -0.8660254,
|
|
10
|
+
// top (y=1)
|
|
11
|
+
1, 1, 0,
|
|
12
|
+
-0.5, 1, 0.8660254,
|
|
13
|
+
-0.5, 1, -0.8660254,
|
|
14
|
+
]);
|
|
15
|
+
const indices = new Uint16Array([
|
|
16
|
+
0, 3, 4, 0, 4, 1,
|
|
17
|
+
1, 4, 5, 1, 5, 2,
|
|
18
|
+
2, 5, 3, 2, 3, 0,
|
|
19
|
+
]);
|
|
20
|
+
const geometry = new BufferGeometry();
|
|
21
|
+
geometry.setAttribute('position', new BufferAttribute(positions, 3));
|
|
22
|
+
geometry.setIndex(new BufferAttribute(indices, 1));
|
|
23
|
+
geometry.computeBoundingSphere();
|
|
24
|
+
return geometry;
|
|
25
|
+
};
|
|
26
|
+
export const createHeadGeometry = () => {
|
|
27
|
+
// Triangular pyramid aligned to +Y, base at y=0, tip at y=1.
|
|
28
|
+
// 4 verts, 3 side triangles.
|
|
29
|
+
const positions = new Float32Array([
|
|
30
|
+
// base (y=0)
|
|
31
|
+
1, 0, 0,
|
|
32
|
+
-0.5, 0, 0.8660254,
|
|
33
|
+
-0.5, 0, -0.8660254,
|
|
34
|
+
// tip (y=1)
|
|
35
|
+
0, 1, 0,
|
|
36
|
+
]);
|
|
37
|
+
const indices = new Uint16Array([
|
|
38
|
+
0, 1, 3,
|
|
39
|
+
1, 2, 3,
|
|
40
|
+
2, 0, 3,
|
|
41
|
+
0, 2, 1,
|
|
42
|
+
]);
|
|
43
|
+
const geometry = new BufferGeometry();
|
|
44
|
+
geometry.setAttribute('position', new BufferAttribute(positions, 3));
|
|
45
|
+
geometry.setIndex(new BufferAttribute(indices, 1));
|
|
46
|
+
geometry.computeBoundingSphere();
|
|
47
|
+
return geometry;
|
|
48
|
+
};
|
|
49
|
+
export const toInstanced = (baseGeometry, instanceCount, attributes) => {
|
|
50
|
+
const geometry = new InstancedBufferGeometry();
|
|
51
|
+
geometry.index = baseGeometry.index;
|
|
52
|
+
geometry.attributes.position = baseGeometry.attributes.position;
|
|
53
|
+
for (const [name, attribute] of Object.entries(attributes)) {
|
|
54
|
+
geometry.setAttribute(name, attribute);
|
|
55
|
+
}
|
|
56
|
+
geometry.instanceCount = instanceCount;
|
|
57
|
+
return geometry;
|
|
58
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Object3D, BufferGeometry, Raycaster, type Intersection } from 'three';
|
|
2
|
+
import type { InstancedArrows } from './InstancedArrows';
|
|
3
|
+
export declare function meshBoundsRaycast(this: Object3D & {
|
|
4
|
+
geometry: BufferGeometry;
|
|
5
|
+
}, raycaster: Raycaster, intersects: Intersection[]): void;
|
|
6
|
+
/**
|
|
7
|
+
* Currently unused. In the future will be used for click only (not mousemove) due to complexity.
|
|
8
|
+
*/
|
|
9
|
+
export declare function raycast(this: InstancedArrows, raycaster: Raycaster, intersects: Intersection[]): void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Object3D, BufferGeometry, Ray, Matrix4, Raycaster, Vector3, Box3, RawShaderMaterial, } from 'three';
|
|
2
|
+
const vec3 = new Vector3();
|
|
3
|
+
const inverseMatrix = new Matrix4();
|
|
4
|
+
const localRay = new Ray();
|
|
5
|
+
const ray = new Ray();
|
|
6
|
+
const box = new Box3();
|
|
7
|
+
const segmentStart = new Vector3();
|
|
8
|
+
const segmentEnd = new Vector3();
|
|
9
|
+
const direction = new Vector3();
|
|
10
|
+
const closestPointRay = new Vector3();
|
|
11
|
+
const closestPointSeg = new Vector3();
|
|
12
|
+
function closestPointsRaySegment(ray, a, b, outRay, outSeg) {
|
|
13
|
+
// Ray: O + tD, t>=0. Segment: A + u(B-A), u in [0,1]
|
|
14
|
+
const O = ray.origin;
|
|
15
|
+
const D = ray.direction; // assume normalized
|
|
16
|
+
const AB = direction.copy(b).sub(a);
|
|
17
|
+
const a0 = 1.0; // D·D
|
|
18
|
+
const b0 = D.dot(AB);
|
|
19
|
+
const c0 = AB.dot(AB);
|
|
20
|
+
const w0x = O.x - a.x, w0y = O.y - a.y, w0z = O.z - a.z;
|
|
21
|
+
const d0 = D.x * w0x + D.y * w0y + D.z * w0z;
|
|
22
|
+
const e0 = AB.x * w0x + AB.y * w0y + AB.z * w0z;
|
|
23
|
+
const denom = a0 * c0 - b0 * b0;
|
|
24
|
+
let t = 0.0;
|
|
25
|
+
let u = 0.0;
|
|
26
|
+
if (denom > 1e-8) {
|
|
27
|
+
t = (b0 * e0 - c0 * d0) / denom;
|
|
28
|
+
u = (a0 * e0 - b0 * d0) / denom;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
t = 0.0;
|
|
32
|
+
u = c0 > 0.0 ? e0 / c0 : 0.0;
|
|
33
|
+
}
|
|
34
|
+
if (t < 0.0)
|
|
35
|
+
t = 0.0;
|
|
36
|
+
if (u < 0.0)
|
|
37
|
+
u = 0.0;
|
|
38
|
+
else if (u > 1.0)
|
|
39
|
+
u = 1.0;
|
|
40
|
+
outRay.copy(D).multiplyScalar(t).add(O);
|
|
41
|
+
outSeg.copy(AB).multiplyScalar(u).add(a);
|
|
42
|
+
return outRay.distanceToSquared(outSeg);
|
|
43
|
+
}
|
|
44
|
+
export function meshBoundsRaycast(raycaster, intersects) {
|
|
45
|
+
if (this.geometry.boundingBox === null) {
|
|
46
|
+
this.geometry.computeBoundingBox();
|
|
47
|
+
}
|
|
48
|
+
box.copy(this.geometry.boundingBox ?? box);
|
|
49
|
+
box.applyMatrix4(this.matrixWorld);
|
|
50
|
+
if (!raycaster.ray.intersectsBox(box)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
inverseMatrix.copy(this.matrixWorld).invert();
|
|
54
|
+
ray.copy(raycaster.ray).applyMatrix4(inverseMatrix);
|
|
55
|
+
const distance = vec3.distanceTo(raycaster.ray.origin);
|
|
56
|
+
const point = vec3.clone();
|
|
57
|
+
intersects.push({
|
|
58
|
+
distance,
|
|
59
|
+
point,
|
|
60
|
+
object: this,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Currently unused. In the future will be used for click only (not mousemove) due to complexity.
|
|
65
|
+
*/
|
|
66
|
+
export function raycast(raycaster, intersects) {
|
|
67
|
+
// Ensure transforms are current
|
|
68
|
+
this.shaftMesh.updateMatrixWorld(true);
|
|
69
|
+
// Transform ray into local space of the mesh
|
|
70
|
+
inverseMatrix.copy(this.shaftMesh.matrixWorld).invert();
|
|
71
|
+
localRay.copy(raycaster.ray).applyMatrix4(inverseMatrix);
|
|
72
|
+
localRay.direction.normalize();
|
|
73
|
+
const material = this.shaftMesh.material;
|
|
74
|
+
const poseScale = material.uniforms?.poseScale?.value ?? 0.001;
|
|
75
|
+
const headAtOrigin = material.uniforms?.headAtOrigin?.value ?? 0.0;
|
|
76
|
+
// pick radius in local space (same units as rendered)
|
|
77
|
+
const radius = Math.max(this.shaftRadius, this.headWidth);
|
|
78
|
+
const array = this.poses.array;
|
|
79
|
+
const stride = 6;
|
|
80
|
+
// Optional quick coarse reject: if you maintain a global bounds box/sphere, test it here.
|
|
81
|
+
let bestDistance = Infinity;
|
|
82
|
+
let bestPointWorld = null;
|
|
83
|
+
let bestInstanceId = -1;
|
|
84
|
+
for (let instanceId = 0; instanceId < this.count; instanceId++) {
|
|
85
|
+
const i = instanceId * stride;
|
|
86
|
+
// origin is in mm in your data, so scale it to match render
|
|
87
|
+
const ox = array[i + 0] * poseScale;
|
|
88
|
+
const oy = array[i + 1] * poseScale;
|
|
89
|
+
const oz = array[i + 2] * poseScale;
|
|
90
|
+
const dx = array[i + 3];
|
|
91
|
+
const dy = array[i + 4];
|
|
92
|
+
const dz = array[i + 5];
|
|
93
|
+
segmentStart.set(ox, oy, oz);
|
|
94
|
+
direction.set(dx, dy, dz);
|
|
95
|
+
const dlen = direction.length();
|
|
96
|
+
if (dlen > 0)
|
|
97
|
+
direction.multiplyScalar(1 / dlen);
|
|
98
|
+
else
|
|
99
|
+
direction.set(0, 1, 0);
|
|
100
|
+
// If shader shifts so the TIP is at origin, mirror it here
|
|
101
|
+
if (headAtOrigin > 0.5) {
|
|
102
|
+
segmentStart.addScaledVector(direction, -this.arrowLength);
|
|
103
|
+
}
|
|
104
|
+
segmentEnd.copy(segmentStart).addScaledVector(direction, this.arrowLength);
|
|
105
|
+
const distSq = closestPointsRaySegment(localRay, segmentStart, segmentEnd, closestPointRay, closestPointSeg);
|
|
106
|
+
if (distSq > radius * radius)
|
|
107
|
+
continue;
|
|
108
|
+
// Param distance along the local ray
|
|
109
|
+
const t = closestPointRay.clone().sub(localRay.origin).dot(localRay.direction);
|
|
110
|
+
if (t < raycaster.near || t > raycaster.far)
|
|
111
|
+
continue;
|
|
112
|
+
// Convert closest point back to world for intersection result
|
|
113
|
+
const worldPoint = closestPointRay.clone().applyMatrix4(this.shaftMesh.matrixWorld);
|
|
114
|
+
const worldDistance = raycaster.ray.origin.distanceTo(worldPoint);
|
|
115
|
+
if (worldDistance < bestDistance) {
|
|
116
|
+
bestDistance = worldDistance;
|
|
117
|
+
bestPointWorld = worldPoint;
|
|
118
|
+
bestInstanceId = instanceId;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (bestInstanceId >= 0 && bestPointWorld) {
|
|
122
|
+
intersects.push({
|
|
123
|
+
distance: bestDistance,
|
|
124
|
+
point: bestPointWorld,
|
|
125
|
+
object: this.shaftMesh,
|
|
126
|
+
instanceId: bestInstanceId,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
precision highp float;
|
|
2
|
+
|
|
3
|
+
attribute vec3 position;
|
|
4
|
+
|
|
5
|
+
attribute vec3 instanceOrigin;
|
|
6
|
+
attribute vec3 instanceDirection;
|
|
7
|
+
|
|
8
|
+
#ifdef USE_COLOR_ATTRIBUTE
|
|
9
|
+
attribute vec3 instanceColor;
|
|
10
|
+
#else
|
|
11
|
+
uniform vec3 uniformColor;
|
|
12
|
+
#endif
|
|
13
|
+
|
|
14
|
+
uniform float shaftRadius;
|
|
15
|
+
uniform float headLength;
|
|
16
|
+
uniform float headWidth;
|
|
17
|
+
uniform float arrowLength;
|
|
18
|
+
uniform float minimumArrowLength;
|
|
19
|
+
|
|
20
|
+
uniform float headAtOrigin; // 0.0 = base at origin, 1.0 = head tip at origin
|
|
21
|
+
|
|
22
|
+
uniform mat4 modelViewMatrix;
|
|
23
|
+
uniform mat4 projectionMatrix;
|
|
24
|
+
|
|
25
|
+
varying vec3 vColor;
|
|
26
|
+
|
|
27
|
+
void buildOrthonormalBasisFromDirection(
|
|
28
|
+
in vec3 normalizedDirection,
|
|
29
|
+
out vec3 basisX,
|
|
30
|
+
out vec3 basisY,
|
|
31
|
+
out vec3 basisZ
|
|
32
|
+
) {
|
|
33
|
+
basisY = normalizedDirection;
|
|
34
|
+
|
|
35
|
+
vec3 helperAxis =
|
|
36
|
+
abs(basisY.z) < 0.999
|
|
37
|
+
? vec3(0.0, 0.0, 1.0)
|
|
38
|
+
: vec3(1.0, 0.0, 0.0);
|
|
39
|
+
|
|
40
|
+
basisX = normalize(cross(helperAxis, basisY));
|
|
41
|
+
basisZ = cross(basisY, basisX);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void main() {
|
|
45
|
+
#ifdef USE_COLOR_ATTRIBUTE
|
|
46
|
+
vColor = instanceColor;
|
|
47
|
+
#else
|
|
48
|
+
vColor = uniformColor;
|
|
49
|
+
#endif
|
|
50
|
+
|
|
51
|
+
float clampedArrowLength = max(arrowLength, minimumArrowLength);
|
|
52
|
+
|
|
53
|
+
// Normalize direction, with a safe fallback if the vector is zero-length.
|
|
54
|
+
vec3 normalizedDirection = instanceDirection;
|
|
55
|
+
float directionMagnitude = length(normalizedDirection);
|
|
56
|
+
normalizedDirection =
|
|
57
|
+
(directionMagnitude > 0.0)
|
|
58
|
+
? (normalizedDirection / directionMagnitude)
|
|
59
|
+
: vec3(0.0, 1.0, 0.0);
|
|
60
|
+
|
|
61
|
+
vec3 basisX, basisY, basisZ;
|
|
62
|
+
buildOrthonormalBasisFromDirection(normalizedDirection, basisX, basisY, basisZ);
|
|
63
|
+
|
|
64
|
+
vec3 scaledInstanceOrigin = instanceOrigin * 0.001;
|
|
65
|
+
vec3 effectiveOrigin = scaledInstanceOrigin;
|
|
66
|
+
|
|
67
|
+
// Shift the arrow so its head tip lands at the provided origin.
|
|
68
|
+
if (headAtOrigin > 0.5) {
|
|
69
|
+
effectiveOrigin -= basisY * clampedArrowLength;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
float computedHeadLength = min(headLength, clampedArrowLength);
|
|
73
|
+
float computedShaftLength = max(clampedArrowLength - computedHeadLength, 0.0);
|
|
74
|
+
|
|
75
|
+
vec3 localPosition = position.xyz;
|
|
76
|
+
|
|
77
|
+
#if defined(IS_HEAD)
|
|
78
|
+
// Scale unit cone to head dimensions.
|
|
79
|
+
localPosition.xz *= headWidth;
|
|
80
|
+
localPosition.y *= computedHeadLength;
|
|
81
|
+
|
|
82
|
+
// Head starts where shaft ends.
|
|
83
|
+
vec3 headOffsetAlongDirection = basisY * computedShaftLength;
|
|
84
|
+
|
|
85
|
+
vec3 worldPosition =
|
|
86
|
+
effectiveOrigin +
|
|
87
|
+
headOffsetAlongDirection +
|
|
88
|
+
(basisX * localPosition.x + basisY * localPosition.y + basisZ * localPosition.z);
|
|
89
|
+
#else
|
|
90
|
+
// Scale unit shaft to shaft dimensions.
|
|
91
|
+
localPosition.xz *= shaftRadius;
|
|
92
|
+
localPosition.y *= computedShaftLength;
|
|
93
|
+
|
|
94
|
+
if (computedShaftLength <= 0.0) {
|
|
95
|
+
localPosition *= 0.0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
vec3 worldPosition =
|
|
99
|
+
effectiveOrigin +
|
|
100
|
+
(basisX * localPosition.x + basisY * localPosition.y + basisZ * localPosition.z);
|
|
101
|
+
#endif
|
|
102
|
+
|
|
103
|
+
gl_Position = projectionMatrix * (modelViewMatrix * vec4(worldPosition, 1.0));
|
|
104
|
+
}
|