@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 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
@@ -19,6 +19,8 @@ export const STRIDE = {
19
19
  NURBS_KNOTS: 1,
20
20
  /** Colors: [r, g, b, a] per color (uint8) */
21
21
  COLORS_RGBA: 4,
22
+ /** Colors: [r, g, b] */
23
+ COLORS_RGB: 3,
22
24
  };
23
25
  /**
24
26
  * Creates a Float32Array view over a Uint8Array without copying data.
@@ -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}
@@ -0,0 +1,3 @@
1
+ declare const Arrows: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Arrows = ReturnType<typeof Arrows>;
3
+ export default Arrows;
@@ -31,10 +31,7 @@
31
31
 
32
32
  const { ...rest } = $props()
33
33
 
34
- const dragPosition = new PersistedState<Vector2Like | undefined>(
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()
@@ -84,7 +84,7 @@
84
84
  <T
85
85
  is={$gltf.scene as Object3D}
86
86
  scale={[scale.current?.x ?? 1, scale.current?.y ?? 1, scale.current?.z ?? 1]}
87
- name={name.current}
87
+ name={entity}
88
88
  {...objectProps}
89
89
  {...rest}
90
90
  >
@@ -131,7 +131,7 @@
131
131
 
132
132
  <T
133
133
  is={mesh}
134
- name={name.current}
134
+ name={entity}
135
135
  bvh={{ enabled: geometryType === 'buffer' }}
136
136
  >
137
137
  {#if model && renderMode.includes('model')}
@@ -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={name.current}
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 { scene, invalidate } = useThrelte()
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
- // Create a clone so that our bounding box doesn't include children
19
- const clone = $derived.by(() => {
20
- if (instance.current) {
21
- return
17
+ const object = $derived.by(() => {
18
+ if (!isInstanceOf(selectedObject3d.current, 'Mesh')) {
19
+ return selectedObject3d.current
22
20
  }
23
21
 
24
- return selectedObject3d.current?.clone(false)
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 (selectedEntity.current === undefined) {
35
+ if (object === undefined) {
30
36
  return
31
37
  }
32
38
 
33
- if (instance.current) {
34
- const mesh = scene.getObjectById(instance.current.meshID) as BatchedMesh
35
- mesh?.getBoundingBoxAt(instance.current.instanceID, box3)
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
- invalidate()
39
- return
47
+ } else {
48
+ obbHelper.setFromObject(object)
40
49
  }
41
50
 
42
- if (clone) {
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,
@@ -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
  */
@@ -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 Float32Reader {
49
+ class BinaryReader {
49
50
  littleEndian = true;
50
- offset = 0;
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.header.requestID = UuidTool.toString([...new Uint8Array(this.buffer.slice(0, 16))]);
58
- this.header.type = new Float32Array(this.buffer.slice(16, 20))[0];
59
- this.buffer = this.buffer.slice(20);
60
- this.array = new Float32Array(this.buffer);
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 result = this.array[this.offset];
65
- this.offset += 1;
66
- return result;
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
- for (let i = 0; i < nPoints; i += 1) {
192
- origin.set(reader.read(), reader.read(), reader.read());
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.array.slice(reader.offset, reader.offset + nPointsElements);
229
- reader.offset += nPointsElements;
232
+ const positions = reader.readF32Array(nPointsElements);
230
233
  const nColorsElements = nColors * 3;
231
- const rawColors = reader.array.slice(reader.offset, reader.offset + nColorsElements);
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 Float32Reader().init(event.data);
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 === 'arm' || parentResource?.subtype === 'gantry'
29
+ const resolvedParent = $derived(origingFrameComponentTypes.includes(parentResource?.subtype ?? '')
29
30
  ? `${parent()}_origin`
30
31
  : parent());
31
- const resolvedName = $derived(resource?.subtype === 'arm' || resource?.subtype === 'gantry'
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
- set(entity?: Entity): void;
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
- set(entity?: Entity): void;
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
- set(entity: Entity): void;
16
+ readonly instance: number | undefined;
17
+ set(entity: Entity, instance?: number): void;
15
18
  };
16
19
  focus: {
17
20
  readonly current: Entity | undefined;
18
- set(entity: Entity): void;
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 { BatchedMesh, Matrix4, Object3D } from 'three';
4
- import { traits, useTrait, useWorld } from '../ecs';
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
- set(entity) {
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
- set(entity) {
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
- if (instance.current) {
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 { Color, Vector3, Vector4 } from 'three';
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 rootEntityTraits = [
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.ReferenceFrame,
112
+ traits.Positions(poses),
116
113
  ];
117
114
  if (parent) {
118
- rootEntityTraits.push(traits.Parent(parent));
115
+ entityTraits.push(traits.Parent(parent));
119
116
  }
120
- const rootEntity = world.spawn(...rootEntityTraits, traits.SnapshotAPI);
121
- entities.push(rootEntity);
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,3 @@
1
+ import { BufferGeometry } from 'three';
2
+ import type { InstancedArrows } from './InstancedArrows';
3
+ export declare function computeBoundingBox(this: InstancedArrows, geometry: BufferGeometry): void;
@@ -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,7 @@
1
+ precision highp float;
2
+
3
+ varying vec3 vColor;
4
+
5
+ void main() {
6
+ gl_FragColor = vec4(vColor, 1.0);
7
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",