@viamrobotics/motion-tools 1.11.0 → 1.11.1

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.
@@ -29,6 +29,7 @@
29
29
  import Camera from './overlay/widgets/Camera.svelte'
30
30
  import HoveredEntities from './hover/HoveredEntities.svelte'
31
31
  import Settings from './overlay/settings/Settings.svelte'
32
+ import { useXR } from '@threlte/xr'
32
33
 
33
34
  interface LocalConfigProps {
34
35
  getLocalPartConfig: () => Struct
@@ -67,6 +68,7 @@
67
68
  const settings = provideSettings()
68
69
  const environment = provideEnvironment()
69
70
  const currentRobotCameraWidgets = $derived(settings.current.openCameraWidgets[partID] || [])
71
+ const { isPresenting } = useXR()
70
72
 
71
73
  $effect(() => {
72
74
  settings.current.enableKeybindings = enableKeybindings
@@ -148,7 +150,7 @@
148
150
  <ArmPositions />
149
151
  {/if}
150
152
 
151
- {#if !focus}
153
+ {#if !focus && !$isPresenting}
152
154
  {#each currentRobotCameraWidgets as cameraName (cameraName)}
153
155
  <Camera name={cameraName} />
154
156
  {/each}
@@ -49,14 +49,7 @@
49
49
  </TrackballControls>
50
50
  </Camera>
51
51
 
52
- <T
53
- is={object3d}
54
- bvh={{
55
- enabled: object3d.type === 'Points',
56
- maxDepth: 40,
57
- maxLeafTris: 20,
58
- }}
59
- />
52
+ <T is={object3d} />
60
53
 
61
54
  <T.BoxHelper
62
55
  args={[object3d, 'red']}
@@ -136,7 +136,6 @@
136
136
  is={mesh}
137
137
  name={entity}
138
138
  userData.name={name}
139
- bvh={{ enabled: geometryType === 'buffer' }}
140
139
  >
141
140
  {#if model && renderMode.includes('model')}
142
141
  <T is={model} />
@@ -0,0 +1,153 @@
1
+ <script lang="ts">
2
+ import { T } from '@threlte/core'
3
+ import type { IntersectionEvent } from '@threlte/extras'
4
+ import Line from './Line.svelte'
5
+ import { useCameraControls } from '../../hooks/useControls.svelte'
6
+ import earcut from 'earcut'
7
+ import { Box3, BufferAttribute, Vector3 } from 'three'
8
+
9
+ interface Props {
10
+ debug?: boolean
11
+ }
12
+
13
+ let { debug = true }: Props = $props()
14
+
15
+ const controls = useCameraControls()
16
+
17
+ const box3 = new Box3()
18
+ const a = new Vector3()
19
+ const b = new Vector3()
20
+ const c = new Vector3()
21
+
22
+ let drawing = false
23
+
24
+ let position = $state<[number, number, number]>([0, 0, 0])
25
+ let lassos = $state<
26
+ {
27
+ positions: number[]
28
+ indices: Uint16Array
29
+ boxes: Box3[]
30
+ min: { x: number; y: number }
31
+ max: { x: number; y: number }
32
+ }[]
33
+ >([])
34
+
35
+ const onpointerdown = (event: IntersectionEvent<PointerEvent>) => {
36
+ drawing = true
37
+
38
+ const { x, y } = event.point
39
+
40
+ lassos.push({
41
+ positions: [x, y, 0],
42
+ indices: new Uint16Array(),
43
+ boxes: [],
44
+ min: { x, y },
45
+ max: { x, y },
46
+ })
47
+
48
+ if (controls.current) {
49
+ controls.current.enabled = false
50
+ }
51
+ }
52
+
53
+ const onpointermove = (event: IntersectionEvent<PointerEvent>) => {
54
+ event.point.toArray(position)
55
+
56
+ if (!drawing) return
57
+
58
+ let line = lassos.at(-1)
59
+
60
+ if (!line) return
61
+
62
+ const { x, y } = event.point
63
+ line.positions.push(x, y, 0)
64
+
65
+ if (x < line.min.x) line.min.x = x
66
+ else if (x > line.max.x) line.max.x = x
67
+
68
+ if (y < line.min.y) line.min.y = y
69
+ else if (y > line.max.y) line.max.y = y
70
+ }
71
+
72
+ const onpointerup = () => {
73
+ drawing = false
74
+
75
+ let lasso = lassos.at(-1)
76
+
77
+ if (!lasso) return
78
+
79
+ const [x, y] = lasso.positions
80
+
81
+ if (controls.current) {
82
+ controls.current.enabled = true
83
+ }
84
+
85
+ // Close the loop
86
+ lasso.positions.push(x, y, 0)
87
+
88
+ const { positions } = lasso
89
+
90
+ const indices = earcut(positions, undefined, 3)
91
+ lasso.indices = new Uint16Array(indices)
92
+
93
+ for (let i = 0; i < indices.length; i += 6) {
94
+ const stride = 3
95
+ const ia = indices[i + 0] * stride
96
+ const ib = indices[i + 1] * stride
97
+ const ic = indices[i + 2] * stride
98
+
99
+ a.set(positions[ia + 0], positions[ia + 1], positions[ia + 2])
100
+ b.set(positions[ib + 0], positions[ib + 1], positions[ib + 2])
101
+ c.set(positions[ic + 0], positions[ic + 1], positions[ic + 2])
102
+ box3.setFromPoints([a, b, c])
103
+
104
+ lasso.boxes.push(box3.clone())
105
+ }
106
+ }
107
+ </script>
108
+
109
+ <T.Mesh
110
+ {onpointerdown}
111
+ {onpointerup}
112
+ {onpointermove}
113
+ >
114
+ <T.PlaneGeometry args={[7, 7, 10, 10]} />
115
+ <T.MeshBasicMaterial
116
+ wireframe
117
+ color="blue"
118
+ transparent
119
+ opacity={debug ? 1 : 0}
120
+ />
121
+ </T.Mesh>
122
+
123
+ {#each lassos as lasso (lasso)}
124
+ <Line positions={lasso.positions} />
125
+
126
+ {#if debug}
127
+ {#if lasso.indices.length > 0}
128
+ <T.Mesh>
129
+ <T.BufferGeometry
130
+ oncreate={(ref) => {
131
+ ref.setIndex(new BufferAttribute(lasso.indices, 1))
132
+ ref.setAttribute('position', new BufferAttribute(new Float32Array(lasso.positions), 3))
133
+ }}
134
+ />
135
+ <T.MeshBasicMaterial
136
+ wireframe
137
+ color="green"
138
+ />
139
+ </T.Mesh>
140
+ {/if}
141
+
142
+ {#each lasso.boxes as box (box)}
143
+ <T.Box3Helper args={[box, 'lightgreen']} />
144
+ {/each}
145
+
146
+ <T.Box3Helper
147
+ args={[
148
+ new Box3(a.set(lasso.min.x, lasso.min.y, 0), b.set(lasso.max.x, lasso.max.y, 0)),
149
+ 'red',
150
+ ]}
151
+ />
152
+ {/if}
153
+ {/each}
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ debug?: boolean;
3
+ }
4
+ declare const Lasso: import("svelte").Component<Props, {}, "">;
5
+ type Lasso = ReturnType<typeof Lasso>;
6
+ export default Lasso;
@@ -0,0 +1,44 @@
1
+ <script>
2
+ import { T, useThrelte } from '@threlte/core'
3
+ import { Line2 } from 'three/examples/jsm/lines/Line2.js'
4
+ import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'
5
+ import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js'
6
+ import { untrack } from 'svelte'
7
+
8
+ let { positions } = $props()
9
+
10
+ let geometry = $state.raw(new LineGeometry())
11
+
12
+ const material = new LineMaterial()
13
+ const line = new Line2()
14
+ material.linewidth = 1
15
+ material.color.set('red')
16
+ material.depthTest = false
17
+ material.depthWrite = false
18
+
19
+ const { invalidate } = useThrelte()
20
+
21
+ $effect(() => {
22
+ untrack(() => {
23
+ geometry = new LineGeometry()
24
+ invalidate()
25
+ })
26
+
27
+ if (!positions || positions.length === 0) {
28
+ return
29
+ }
30
+
31
+ untrack(() => {
32
+ geometry.setPositions(positions)
33
+ })
34
+ })
35
+ </script>
36
+
37
+ <T
38
+ is={line}
39
+ frustumCulled={false}
40
+ renderOrder={Number.POSITIVE_INFINITY}
41
+ >
42
+ <T is={geometry} />
43
+ <T is={material} />
44
+ </T>
@@ -0,0 +1,11 @@
1
+ export default Line;
2
+ type Line = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ declare const Line: import("svelte").Component<{
7
+ positions: any;
8
+ }, {}, "">;
9
+ type $$ComponentProps = {
10
+ positions: any;
11
+ };
@@ -19,7 +19,6 @@
19
19
 
20
20
  <T.Mesh
21
21
  raycast={enabled ? Mesh.prototype.raycast : () => null}
22
- bvh={{ enabled: false }}
23
22
  onpointerdown={() => {
24
23
  cameraDown.copy(camera.current.position)
25
24
  }}
@@ -122,7 +122,7 @@
122
122
  <T
123
123
  is={points}
124
124
  name={entity}
125
- bvh={{ maxDepth: 40, maxLeafTris: 20 }}
125
+ bvh={{ maxDepth: 40, maxLeafSize: 20 }}
126
126
  {...events}
127
127
  >
128
128
  <T is={geometry.current} />
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { Vector3 } from 'three'
3
3
  import { T } from '@threlte/core'
4
- import { Grid, interactivity, PerfMonitor, bvh, PortalTarget } from '@threlte/extras'
4
+ import { Grid, interactivity, PerfMonitor, PortalTarget } from '@threlte/extras'
5
5
  import Entities from './Entities.svelte'
6
6
  import Selected from './Selected.svelte'
7
7
  import Focus from './Focus.svelte'
@@ -10,7 +10,7 @@
10
10
  import { useFocusedObject3d } from '../hooks/useSelection.svelte'
11
11
  import type { Snippet } from 'svelte'
12
12
  import { useXR } from '@threlte/xr'
13
-
13
+ import { bvh } from '../plugins/bvh.svelte'
14
14
  import { useOrigin } from './xr/useOrigin.svelte'
15
15
  import { useSettings } from '../hooks/useSettings.svelte'
16
16
  import CameraControls from './CameraControls.svelte'
@@ -43,10 +43,7 @@
43
43
  enabled.set(!settings.current.enableMeasure)
44
44
  })
45
45
 
46
- raycaster.firstHitOnly = true
47
- raycaster.params.Points.threshold = 0.005
48
-
49
- bvh(() => ({ helper: false }))
46
+ bvh(raycaster, () => ({ helper: false }))
50
47
 
51
48
  const focusedObject = $derived(focusedObject3d.current)
52
49
 
@@ -14,9 +14,10 @@
14
14
  position={[hoverInfo.x, hoverInfo.y, hoverInfo.z]}
15
15
  class="pointer-events-none"
16
16
  zIndexRange={[3, 0]}
17
+ center
17
18
  >
18
19
  <div
19
- class="border-medium pointer-events-none relative -mb-2 -translate-x-1/2 -translate-y-full border bg-white px-3 py-2.5 text-xs shadow-md"
20
+ class="border-medium pointer-events-none relative -translate-y-1/2 border bg-white px-3 py-2.5 text-xs shadow-md"
20
21
  >
21
22
  <!-- Arrow -->
22
23
  <div
@@ -10,6 +10,7 @@
10
10
  offset?: { x?: number; y?: number; z?: number }
11
11
  scale?: number
12
12
  enableProfiling?: boolean
13
+ onAspectChange?: (aspect: number) => void
13
14
  }
14
15
 
15
16
  let {
@@ -17,6 +18,7 @@
17
18
  offset = {},
18
19
  scale = 0.7,
19
20
  enableProfiling = false,
21
+ onAspectChange,
20
22
  }: CameraFeedProps = $props()
21
23
 
22
24
  const partID = usePartID()
@@ -77,6 +79,7 @@
77
79
  const onReady = () => {
78
80
  const videoReadyTime = performance.now()
79
81
  aspect = video.videoWidth / video.videoHeight
82
+ onAspectChange?.(aspect)
80
83
 
81
84
  if (!texture) {
82
85
  texture = new VideoTexture(video)
@@ -7,6 +7,7 @@ interface CameraFeedProps {
7
7
  };
8
8
  scale?: number;
9
9
  enableProfiling?: boolean;
10
+ onAspectChange?: (aspect: number) => void;
10
11
  }
11
12
  declare const CameraFeed: import("svelte").Component<CameraFeedProps, {}, "">;
12
13
  type CameraFeed = ReturnType<typeof CameraFeed>;
@@ -11,12 +11,7 @@
11
11
  rotationY?: number
12
12
  }
13
13
 
14
- let {
15
- armName,
16
- offset = {},
17
- scale = 0.6,
18
- rotationY = -15 * (Math.PI / 180),
19
- }: JointLimitsWidgetProps = $props()
14
+ let { armName, offset = {}, scale = 0.6, rotationY = 0 }: JointLimitsWidgetProps = $props()
20
15
 
21
16
  const armClient = useArmClient()
22
17
  const armKinematics = useArmKinematics()
@@ -63,10 +58,11 @@
63
58
  })
64
59
  })
65
60
 
66
- // Canvas setup
67
- const CANVAS_WIDTH = 800
68
- const HEADER_HEIGHT = 80
69
- const ROW_HEIGHT = 120
61
+ // Canvas setup — use 2x resolution for sharper XR text
62
+ const RESOLUTION_SCALE = 4
63
+ const CANVAS_WIDTH = 800 * RESOLUTION_SCALE
64
+ const HEADER_HEIGHT = 80 * RESOLUTION_SCALE
65
+ const ROW_HEIGHT = 120 * RESOLUTION_SCALE
70
66
  let canvasHeight = $derived(HEADER_HEIGHT + (jointData?.length ?? 0) * ROW_HEIGHT)
71
67
 
72
68
  let canvas: HTMLCanvasElement | undefined = $state()
@@ -105,19 +101,21 @@
105
101
 
106
102
  // Render header with arm name
107
103
  function renderHeader(ctx: CanvasRenderingContext2D, width: number) {
104
+ const s = RESOLUTION_SCALE
105
+
108
106
  // Header background
109
107
  ctx.fillStyle = '#0a0a0a'
110
108
  ctx.fillRect(0, 0, width, HEADER_HEIGHT)
111
109
 
112
110
  // Arm name
113
111
  ctx.fillStyle = '#ffffff'
114
- ctx.font = 'bold 36px monospace'
112
+ ctx.font = `bold ${36 * s}px monospace`
115
113
  ctx.textBaseline = 'middle'
116
- ctx.fillText(armName, 20, HEADER_HEIGHT / 2)
114
+ ctx.fillText(armName, 20 * s, HEADER_HEIGHT / 2)
117
115
 
118
116
  // Separator line
119
117
  ctx.strokeStyle = '#444444'
120
- ctx.lineWidth = 4
118
+ ctx.lineWidth = 4 * s
121
119
  ctx.beginPath()
122
120
  ctx.moveTo(0, HEADER_HEIGHT)
123
121
  ctx.lineTo(width, HEADER_HEIGHT)
@@ -131,6 +129,7 @@
131
129
  width: number,
132
130
  height: number
133
131
  ) {
132
+ const s = RESOLUTION_SCALE
134
133
  const rowHeight = (height - HEADER_HEIGHT) / joints.length
135
134
 
136
135
  joints.forEach((joint, index) => {
@@ -142,15 +141,15 @@
142
141
 
143
142
  // Joint label
144
143
  ctx.fillStyle = '#ffffff'
145
- ctx.font = 'bold 32px monospace'
144
+ ctx.font = `bold ${32 * s}px monospace`
146
145
  ctx.textBaseline = 'middle'
147
- ctx.fillText(joint.jointId, 20, y + rowHeight / 2)
146
+ ctx.fillText(joint.jointId, 20 * s, y + rowHeight / 2)
148
147
 
149
148
  // Progress bar dimensions
150
- const barX = 240
151
- const barY = y + (rowHeight - 60) / 2
152
- const barWidth = 360
153
- const barHeight = 60
149
+ const barX = 240 * s
150
+ const barY = y + (rowHeight - 60 * s) / 2
151
+ const barWidth = 360 * s
152
+ const barHeight = 60 * s
154
153
 
155
154
  // Progress bar background
156
155
  ctx.fillStyle = '#333333'
@@ -164,13 +163,17 @@
164
163
 
165
164
  // Progress bar border
166
165
  ctx.strokeStyle = '#666666'
167
- ctx.lineWidth = 4
166
+ ctx.lineWidth = 4 * s
168
167
  ctx.strokeRect(barX, barY, barWidth, barHeight)
169
168
 
170
169
  // Current value text
171
170
  ctx.fillStyle = '#ffffff'
172
- ctx.font = '28px monospace'
173
- ctx.fillText(`${joint.currentPosition.toFixed(1)}°`, barX + barWidth + 20, y + rowHeight / 2)
171
+ ctx.font = `${28 * s}px monospace`
172
+ ctx.fillText(
173
+ `${joint.currentPosition.toFixed(1)}°`,
174
+ barX + barWidth + 20 * s,
175
+ y + rowHeight / 2
176
+ )
174
177
  })
175
178
  }
176
179
 
@@ -10,6 +10,7 @@
10
10
  import { usePartID } from '../../hooks/usePartID.svelte'
11
11
  import XRToast from './XRToast.svelte'
12
12
  import { useOrigin } from './useOrigin.svelte'
13
+ import { SvelteMap } from 'svelte/reactivity'
13
14
 
14
15
  const { ...rest } = $props()
15
16
 
@@ -27,6 +28,16 @@
27
28
  return openWidgets[currentPartID] || []
28
29
  })
29
30
 
31
+ // Track camera aspect ratios to compute proper spacing
32
+ const cameraAspects = new SvelteMap<string, number>()
33
+
34
+ const CAMERA_SCALE = 0.8
35
+ const CAMERA_GAP = 0.15 // gap between feed edges
36
+
37
+ // Compute spacing from the widest camera feed (default 16:9 before any aspect is known)
38
+ const maxAspect = $derived(cameraAspects.size > 0 ? Math.max(...cameraAspects.values()) : 16 / 9)
39
+ const feedSpacing = $derived(maxAspect * CAMERA_SCALE + CAMERA_GAP)
40
+
30
41
  // Get arms assigned to controllers
31
42
  const controllerConfig = $derived(settings.current.xrController)
32
43
  const leftArmName = $derived(controllerConfig.left.armName)
@@ -42,37 +53,34 @@
42
53
  origin.set([0, 0, 0])
43
54
  }}
44
55
  >
45
- <!-- Render all enabled camera feeds with horizontal spacing behind origin -->
46
- {#each enabledCameras as cameraName, index (cameraName)}
47
- {@const spacing = 1.2}
48
- {@const centerOffset = ((enabledCameras.length - 1) * spacing) / 2}
49
- <CameraFeed
50
- resourceName={cameraName}
51
- offset={{ x: index * spacing - centerOffset, y: 1.5, z: -2.5 }}
52
- scale={0.8}
53
- enableProfiling={false}
54
- />
55
- {/each}
56
+ <!-- Render camera feeds only when presenting to avoid conflicting with overlay Camera widgets -->
57
+ {#if $isPresenting}
58
+ {#each enabledCameras as cameraName, index (cameraName)}
59
+ {@const centerOffset = ((enabledCameras.length - 1) * feedSpacing) / 2}
60
+ <CameraFeed
61
+ resourceName={cameraName}
62
+ offset={{ x: index * feedSpacing - centerOffset, y: 1.5, z: -2.5 }}
63
+ scale={CAMERA_SCALE}
64
+ enableProfiling={false}
65
+ onAspectChange={(a) => {
66
+ cameraAspects.set(cameraName, a)
67
+ }}
68
+ />
69
+ {/each}
70
+ {/if}
56
71
 
57
72
  <!-- Render joint limits widgets only for arms assigned to controllers, on the matching side -->
58
73
  {#if leftArmName}
59
- {@const spacing = 1.2}
60
- {@const centerOffset = ((enabledCameras.length - 1) * spacing) / 2}
61
- {@const widgetX = -(centerOffset + spacing + 0.3)}
62
74
  <JointLimitsWidget
63
75
  armName={leftArmName}
64
- offset={{ x: widgetX, y: 1.5, z: -2.5 }}
76
+ offset={{ x: -0.5, y: 2.5, z: -2.5 }}
65
77
  scale={0.6}
66
- rotationY={15 * (Math.PI / 180)}
67
78
  />
68
79
  {/if}
69
80
  {#if rightArmName}
70
- {@const spacing = 1.2}
71
- {@const centerOffset = ((enabledCameras.length - 1) * spacing) / 2}
72
- {@const widgetX = centerOffset + spacing + 0.3}
73
81
  <JointLimitsWidget
74
82
  armName={rightArmName}
75
- offset={{ x: widgetX, y: 1.5, z: -2.5 }}
83
+ offset={{ x: 0.5, y: 2.5, z: -2.5 }}
76
84
  scale={0.6}
77
85
  />
78
86
  {/if}
@@ -0,0 +1,8 @@
1
+ import { type Raycaster } from 'three';
2
+ import { type BVHOptions } from 'three-mesh-bvh';
3
+ interface Options extends BVHOptions {
4
+ helper?: boolean;
5
+ enabled?: boolean;
6
+ }
7
+ export declare const bvh: (raycaster: Raycaster, options?: () => Options) => void;
8
+ export {};
@@ -0,0 +1,69 @@
1
+ import { injectPlugin, isInstanceOf } from '@threlte/core';
2
+ import { BatchedMesh, Points, Mesh } from 'three';
3
+ import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, computeBatchedBoundsTree, disposeBatchedBoundsTree, PointsBVH, SAH, BVHHelper, } from 'three-mesh-bvh';
4
+ export const bvh = (raycaster, options) => {
5
+ const bvhOptions = $derived({
6
+ strategy: SAH,
7
+ verbose: false,
8
+ setBoundingBox: true,
9
+ maxDepth: 20,
10
+ maxLeafSize: 10,
11
+ indirect: false,
12
+ helper: false,
13
+ ...options?.(),
14
+ });
15
+ raycaster.firstHitOnly = true;
16
+ raycaster.params.Points.threshold = 0.005;
17
+ injectPlugin('bvh', (args) => {
18
+ const { props } = $derived(args);
19
+ const opts = $derived(props.bvh ? { ...bvhOptions, ...props.bvh } : bvhOptions);
20
+ $effect(() => {
21
+ const { ref } = args;
22
+ if (opts.enabled === false) {
23
+ return;
24
+ }
25
+ if (isInstanceOf(ref, 'Points')) {
26
+ ref.geometry.computeBoundsTree = computeBoundsTree;
27
+ ref.geometry.disposeBoundsTree = disposeBoundsTree;
28
+ ref.raycast = acceleratedRaycast;
29
+ computeBoundsTree.call(ref.geometry, { type: PointsBVH, ...opts });
30
+ const helper = opts.helper ? new BVHHelper(ref) : undefined;
31
+ if (helper)
32
+ ref.add(helper);
33
+ return () => {
34
+ ref.raycast = Points.prototype.raycast;
35
+ if (helper)
36
+ ref.remove(helper);
37
+ };
38
+ }
39
+ else if (isInstanceOf(ref, 'BatchedMesh')) {
40
+ /* @ts-expect-error Some sort of ambient type is conflicing here, likely from @threlte/extras */
41
+ ref.geometry.computeBoundsTree = computeBatchedBoundsTree;
42
+ ref.geometry.disposeBoundsTree = disposeBatchedBoundsTree;
43
+ ref.raycast = acceleratedRaycast;
44
+ const helper = opts.helper ? new BVHHelper(ref) : undefined;
45
+ if (helper)
46
+ ref.add(helper);
47
+ return () => {
48
+ ref.raycast = BatchedMesh.prototype.raycast;
49
+ if (helper)
50
+ ref.remove(helper);
51
+ };
52
+ }
53
+ else if (isInstanceOf(ref, 'Mesh')) {
54
+ ref.geometry.computeBoundsTree = computeBoundsTree;
55
+ ref.geometry.disposeBoundsTree = disposeBoundsTree;
56
+ ref.raycast = acceleratedRaycast;
57
+ computeBoundsTree.call(ref.geometry, opts);
58
+ const helper = opts.helper ? new BVHHelper(ref) : undefined;
59
+ if (helper)
60
+ ref.add(helper);
61
+ return () => {
62
+ ref.raycast = Mesh.prototype.raycast;
63
+ if (helper)
64
+ ref.remove(helper);
65
+ };
66
+ }
67
+ });
68
+ });
69
+ };
package/dist/ply.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import type { BufferGeometry } from 'three';
1
+ import { BufferGeometry } from 'three';
2
2
  export declare const parsePlyInput: (mesh: string | Uint8Array) => BufferGeometry;
package/dist/ply.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { BufferGeometry } from 'three';
1
2
  import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
2
3
  const plyLoader = new PLYLoader();
3
4
  export const parsePlyInput = (mesh) => {
@@ -5,6 +6,10 @@ export const parsePlyInput = (mesh) => {
5
6
  if (typeof mesh === 'string') {
6
7
  return plyLoader.parse(atob(mesh));
7
8
  }
9
+ // First, determine if ply has any geometry
10
+ if (mesh.length === 0) {
11
+ return new BufferGeometry();
12
+ }
8
13
  // Case 2: detect text vs binary PLY in Uint8Array
9
14
  const header = new TextDecoder().decode(mesh.slice(0, 50));
10
15
  const isAscii = header.includes('format ascii');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.11.0",
3
+ "version": "1.11.1",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -30,6 +30,7 @@
30
30
  "@threlte/rapier": "3.2.0",
31
31
  "@threlte/xr": "1.0.8",
32
32
  "@types/bun": "1.2.21",
33
+ "@types/earcut": "^3.0.0",
33
34
  "@types/lodash-es": "4.17.12",
34
35
  "@types/three": "0.181.0",
35
36
  "@typescript-eslint/eslint-plugin": "8.42.0",
@@ -132,9 +133,11 @@
132
133
  "@connectrpc/connect-web": "1.7.0",
133
134
  "@neodrag/svelte": "^2.3.3",
134
135
  "@tanstack/svelte-query-devtools": "^6.0.2",
136
+ "earcut": "^3.0.2",
135
137
  "expr-eval": "^2.0.2",
136
138
  "koota": "0.6.5",
137
139
  "lodash-es": "4.17.23",
140
+ "three-mesh-bvh": "^0.9.8",
138
141
  "uuid-tool": "^2.0.3"
139
142
  },
140
143
  "scripts": {