@viamrobotics/motion-tools 1.1.6 → 1.2.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.
@@ -98,6 +98,29 @@ export const parseMetadata = (fields = {}) => {
98
98
  case 'lineDotColor':
99
99
  json[k] = readColor(unwrappedValue);
100
100
  break;
101
+ case 'colors': {
102
+ let colorBytes;
103
+ // Handle base64-encoded string (from protobuf Struct)
104
+ if (typeof unwrappedValue === 'string') {
105
+ const binary = atob(unwrappedValue);
106
+ colorBytes = new Uint8Array(binary.length);
107
+ for (let i = 0; i < binary.length; i++) {
108
+ colorBytes[i] = binary.charCodeAt(i);
109
+ }
110
+ }
111
+ else if (Array.isArray(unwrappedValue)) {
112
+ colorBytes = unwrappedValue;
113
+ }
114
+ if (colorBytes && colorBytes.length >= 3) {
115
+ const r = (colorBytes[0] ?? 0) / 255;
116
+ const g = (colorBytes[1] ?? 0) / 255;
117
+ const b = (colorBytes[2] ?? 0) / 255;
118
+ const a = colorBytes.length >= 4 ? (colorBytes[3] ?? 255) / 255 : 1;
119
+ json.color = new Color(r, g, b);
120
+ json.opacity = a;
121
+ }
122
+ break;
123
+ }
101
124
  case 'opacity':
102
125
  json[k] = parseOpacity(unwrappedValue);
103
126
  break;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Zero-copy buffer utilities for converting protobuf bytes to Three.js typed arrays.
3
+ *
4
+ * Proto messages pack float32 data as `Uint8Array` (bytes fields). These utilities
5
+ * provide efficient conversion to `Float32Array` for Three.js BufferAttributes.
6
+ */
7
+ /**
8
+ * Stride constants for proto binary data formats.
9
+ * Each value represents the number of float32 elements per item.
10
+ */
11
+ export declare const STRIDE: {
12
+ /** Arrows: [x, y, z, ox, oy, oz] per arrow */
13
+ readonly ARROWS: 6;
14
+ /** Line/Points: [x, y, z] per point */
15
+ readonly POSITIONS: 3;
16
+ /** Nurbs control points: [x, y, z, ox, oy, oz, theta] per point */
17
+ readonly NURBS_CONTROL_POINTS: 7;
18
+ /** Nurbs knots/weights: single float per element */
19
+ readonly NURBS_KNOTS: 1;
20
+ /** Colors: [r, g, b, a] per color (uint8) */
21
+ readonly COLORS_RGBA: 4;
22
+ };
23
+ /**
24
+ * Creates a Float32Array view over a Uint8Array without copying data.
25
+ * Falls back to a copy if the buffer is not 4-byte aligned (rare with protobuf).
26
+ *
27
+ * @param bytes - The raw bytes from a protobuf bytes field
28
+ * @returns A Float32Array view or copy of the data
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const positions = asFloat32Array(line.positions)
33
+ * geometry.setAttribute('position', new BufferAttribute(positions, 3))
34
+ * ```
35
+ */
36
+ export declare const asFloat32Array: (bytes: Uint8Array<ArrayBuffer>) => Float32Array<ArrayBuffer>;
37
+ /**
38
+ * Converts uint8 RGBA colors to normalized float32 colors (0-1 range).
39
+ * Three.js expects colors in 0-1 range for BufferAttributes.
40
+ *
41
+ * @param colors - Uint8Array of RGBA color data [r, g, b, a, ...]
42
+ * @returns Float32Array with normalized color values
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const colors = normalizeColorsRGBA(metadata.colors)
47
+ * geometry.setAttribute('color', new BufferAttribute(colors, 4))
48
+ * ```
49
+ */
50
+ export declare const normalizeColorsRGBA: (colors: Float32Array) => Float32Array;
package/dist/buffer.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Zero-copy buffer utilities for converting protobuf bytes to Three.js typed arrays.
3
+ *
4
+ * Proto messages pack float32 data as `Uint8Array` (bytes fields). These utilities
5
+ * provide efficient conversion to `Float32Array` for Three.js BufferAttributes.
6
+ */
7
+ /**
8
+ * Stride constants for proto binary data formats.
9
+ * Each value represents the number of float32 elements per item.
10
+ */
11
+ export const STRIDE = {
12
+ /** Arrows: [x, y, z, ox, oy, oz] per arrow */
13
+ ARROWS: 6,
14
+ /** Line/Points: [x, y, z] per point */
15
+ POSITIONS: 3,
16
+ /** Nurbs control points: [x, y, z, ox, oy, oz, theta] per point */
17
+ NURBS_CONTROL_POINTS: 7,
18
+ /** Nurbs knots/weights: single float per element */
19
+ NURBS_KNOTS: 1,
20
+ /** Colors: [r, g, b, a] per color (uint8) */
21
+ COLORS_RGBA: 4,
22
+ };
23
+ /**
24
+ * Creates a Float32Array view over a Uint8Array without copying data.
25
+ * Falls back to a copy if the buffer is not 4-byte aligned (rare with protobuf).
26
+ *
27
+ * @param bytes - The raw bytes from a protobuf bytes field
28
+ * @returns A Float32Array view or copy of the data
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const positions = asFloat32Array(line.positions)
33
+ * geometry.setAttribute('position', new BufferAttribute(positions, 3))
34
+ * ```
35
+ */
36
+ export const asFloat32Array = (bytes) => {
37
+ if (bytes.length === 0) {
38
+ return new Float32Array(0);
39
+ }
40
+ if (bytes.byteOffset % 4 === 0 && bytes.byteLength % 4 === 0) {
41
+ return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4);
42
+ }
43
+ const aligned = new Float32Array(bytes.byteLength / 4);
44
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
45
+ for (let i = 0; i < aligned.length; i++) {
46
+ aligned[i] = view.getFloat32(i * 4, true); // little-endian
47
+ }
48
+ return aligned;
49
+ };
50
+ /**
51
+ * Converts uint8 RGBA colors to normalized float32 colors (0-1 range).
52
+ * Three.js expects colors in 0-1 range for BufferAttributes.
53
+ *
54
+ * @param colors - Uint8Array of RGBA color data [r, g, b, a, ...]
55
+ * @returns Float32Array with normalized color values
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const colors = normalizeColorsRGBA(metadata.colors)
60
+ * geometry.setAttribute('color', new BufferAttribute(colors, 4))
61
+ * ```
62
+ */
63
+ export const normalizeColorsRGBA = (colors) => {
64
+ const normalized = new Float32Array(colors.length);
65
+ for (let i = 0; i < colors.length; i++) {
66
+ normalized[i] = colors[i] / 255;
67
+ }
68
+ return normalized;
69
+ };
package/dist/color.d.ts CHANGED
@@ -34,3 +34,5 @@ export declare const parseColor: (color: unknown, defaultColor?: ColorRepresenta
34
34
  export declare const isRGB: (color: unknown) => color is RGB;
35
35
  export declare const parseRGB: (color: unknown, defaultColor?: RGB) => Color;
36
36
  export declare const parseOpacity: (opacity: unknown, defaultOpacity?: number) => number;
37
+ export declare const rgbaToHex: (rgba: Uint8Array) => string;
38
+ export declare const rgbaBytesToFloat32: (bytes: Uint8Array<ArrayBuffer>) => Float32Array<ArrayBuffer>;
package/dist/color.js CHANGED
@@ -40,6 +40,8 @@ const oklchToHex = (raw) => {
40
40
  const b = Math.max(0, Math.min(1, linearToSrgb(b_linear)));
41
41
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
42
42
  };
43
+ const original = new Color();
44
+ const hsl = { h: 0, s: 0, l: 0 };
43
45
  /**
44
46
  * Darkens a THREE.Color by a given percentage while preserving hue.
45
47
  * @param color The original THREE.Color instance.
@@ -47,17 +49,17 @@ const oklchToHex = (raw) => {
47
49
  * @returns A new THREE.Color instance with the darkened color.
48
50
  */
49
51
  export const darkenColor = (value, percent) => {
50
- const original = new Color(value);
51
- const hsl = original.getHSL({ h: 0, s: 0, l: 0 });
52
+ original.set(value);
53
+ original.getHSL(hsl);
52
54
  hsl.l = Math.max(0, hsl.l * (1 - percent / 100));
53
55
  return new Color().setHSL(hsl.h, hsl.s, hsl.l);
54
56
  };
55
- const darkness = '600';
56
57
  export const resourceNameToColor = (resourceName) => {
57
58
  return resourceName
58
59
  ? new Color(resourceColors[resourceName.subtype])
59
60
  : undefined;
60
61
  };
62
+ const darkness = '600';
61
63
  export const colors = {
62
64
  default: oklchToHex(twColors.red[darkness]),
63
65
  };
@@ -139,3 +141,18 @@ const isColorHex = (color) => {
139
141
  }
140
142
  return false;
141
143
  };
144
+ export const rgbaToHex = (rgba) => {
145
+ if (rgba.length < 3)
146
+ return '#333333';
147
+ const r = rgba[0].toString(16).padStart(2, '0');
148
+ const g = rgba[1].toString(16).padStart(2, '0');
149
+ const b = rgba[2].toString(16).padStart(2, '0');
150
+ return `#${r}${g}${b}`;
151
+ };
152
+ export const rgbaBytesToFloat32 = (bytes) => {
153
+ const out = new Float32Array(bytes.length);
154
+ for (let i = 0; i < bytes.length; i++) {
155
+ out[i] = bytes[i] / 255;
156
+ }
157
+ return out;
158
+ };
@@ -28,7 +28,7 @@
28
28
 
29
29
  const instanceID = batched.addArrow(
30
30
  direction.set(pose?.oX ?? 0, pose?.oY ?? 0, pose?.oZ ?? 0),
31
- origin.set(pose?.x ?? 0, pose?.y ?? 0, pose?.z ?? 0),
31
+ origin.set(pose?.x ?? 0, pose?.y ?? 0, pose?.z ?? 0).multiplyScalar(0.001),
32
32
  colorRGB ? color.set(colorRGB.r, colorRGB.g, colorRGB.b) : color.set('yellow')
33
33
  )
34
34
 
@@ -47,7 +47,7 @@
47
47
  batch?.updateArrow(
48
48
  instanceID,
49
49
  direction.set(pose.oX, pose.oY, pose.oZ),
50
- origin.set(pose.x, pose.y, pose.z)
50
+ origin.set(pose.x, pose.y, pose.z).multiplyScalar(0.001)
51
51
  )
52
52
  }
53
53
  }
@@ -1,70 +1,85 @@
1
1
  <script lang="ts">
2
2
  import Pose from './Pose.svelte'
3
3
  import Frame from './Frame.svelte'
4
- import Line from './Line.svelte'
5
- import Pointcloud from './Pointcloud.svelte'
6
4
  import GLTF from './GLTF.svelte'
7
5
  import Label from './Label.svelte'
6
+ import Line from './Line.svelte'
7
+ import Points from './Points.svelte'
8
8
  import { traits, useQuery } from '../ecs'
9
- import { Or } from 'koota'
9
+ import { Not, Or } from 'koota'
10
10
 
11
- const frames = useQuery(traits.FramesAPI)
12
- const geometries = useQuery(traits.GeometriesAPI)
13
- const points = useQuery(traits.PointsGeometry)
14
- const lines = useQuery(traits.LineGeometry)
15
- const gltfs = useQuery(traits.GLTF)
16
- const droppedMeshes = useQuery(traits.DroppedFile, traits.BufferGeometry)
17
- const drawnMeshes = useQuery(
18
- traits.DrawAPI,
11
+ /**
12
+ * Frames from a live machine are bucketed into their own query
13
+ * due to needing to call `getPose` on each one
14
+ */
15
+ const machineFramesEntities = useQuery(traits.FramesAPI)
16
+
17
+ /**
18
+ * Geometries from a live machine are bucketed into their own query
19
+ * to avoid thrashing other query results due to them being
20
+ * potentially being polled at 30/60fps.
21
+ */
22
+ const resourceGeometriesEntities = useQuery(traits.GeometriesAPI)
23
+
24
+ /**
25
+ * Geometries from the world state API are bucketed into their own query
26
+ * to avoid thrashing other query results due to them being potentially polled at 60fps.
27
+ */
28
+ const worldStateEntities = useQuery(
29
+ traits.WorldStateStoreAPI,
19
30
  Or(traits.Box, traits.Capsule, traits.Sphere, traits.BufferGeometry, traits.ReferenceFrame)
20
31
  )
21
- const worldStateMeshes = useQuery(
22
- traits.WorldStateStoreAPI,
32
+
33
+ /**
34
+ * All remaining meshes can be bucketed into a query due to lower frequency updates.
35
+ */
36
+ const meshEntities = useQuery(
37
+ Not(traits.FramesAPI),
38
+ Not(traits.GeometriesAPI),
39
+ Not(traits.WorldStateStoreAPI),
23
40
  Or(traits.Box, traits.Capsule, traits.Sphere, traits.BufferGeometry, traits.ReferenceFrame)
24
41
  )
42
+
43
+ const points = useQuery(traits.PointsPositions)
44
+ const lines = useQuery(traits.LinePositions)
45
+ const gltfs = useQuery(traits.GLTF)
25
46
  </script>
26
47
 
27
- {#each drawnMeshes.current as entity (entity)}
48
+ {#each machineFramesEntities.current as entity (entity)}
49
+ <Pose {entity}>
50
+ {#snippet children({ pose })}
51
+ <Frame
52
+ {pose}
53
+ {entity}
54
+ >
55
+ <Label text={entity.get(traits.Name)} />
56
+ </Frame>
57
+ {/snippet}
58
+ </Pose>
59
+ {/each}
60
+
61
+ {#each resourceGeometriesEntities.current as entity (entity)}
28
62
  <Frame {entity}>
29
63
  <Label text={entity.get(traits.Name)} />
30
64
  </Frame>
31
65
  {/each}
32
66
 
33
- {#each worldStateMeshes.current as entity (entity)}
67
+ {#each worldStateEntities.current as entity (entity)}
34
68
  <Frame {entity}>
35
69
  <Label text={entity.get(traits.Name)} />
36
70
  </Frame>
37
71
  {/each}
38
72
 
39
- {#each droppedMeshes.current as entity (entity)}
73
+ {#each meshEntities.current as entity (entity)}
40
74
  <Frame {entity}>
41
75
  <Label text={entity.get(traits.Name)} />
42
76
  </Frame>
43
77
  {/each}
44
78
 
45
79
  {#each points.current as entity (entity)}
46
- <Pointcloud {entity}>
80
+ <Points {entity}>
47
81
  <Label text={entity.get(traits.Name)} />
48
- </Pointcloud>
49
- {/each}
50
-
51
- {#each frames.current as entity (entity)}
52
- <Pose {entity}>
53
- {#snippet children({ pose })}
54
- <Frame
55
- {pose}
56
- {entity}
57
- >
58
- <Label text={entity.get(traits.Name)} />
59
- </Frame>
60
- {/snippet}
61
- </Pose>
62
- {/each}
63
-
64
- {#each geometries.current as entity (entity)}
65
- <Frame {entity}>
66
- <Label text={entity.get(traits.Name)} />
67
- </Frame>
82
+ </Points>
68
83
  {/each}
69
84
 
70
85
  {#each lines.current as entity (entity)}
@@ -5,59 +5,39 @@
5
5
  import { useWorld } from '../../ecs/useWorld'
6
6
  import type { FileDropperSuccess } from './file-dropper'
7
7
  import { traits } from '../../ecs'
8
- import { parseMetadata } from '../../WorldObject.svelte'
9
- import type { Snapshot } from '../../draw/v1/snapshot_pb'
8
+ import { spawnSnapshotEntities } from '../../snapshot'
9
+ import { useCameraControls } from '../../hooks/useControls.svelte'
10
10
 
11
11
  const props: HTMLAttributes<HTMLDivElement> = $props()
12
12
 
13
13
  const world = useWorld()
14
14
  const toast = useToast()
15
-
16
- const addSnapshotToWorld = (snapshot: Snapshot) => {
17
- for (const transform of snapshot.transforms) {
18
- const entity = world.spawn(
19
- traits.Name(transform.referenceFrame),
20
- traits.Pose(transform.poseInObserverFrame?.pose),
21
- traits.Parent(transform.poseInObserverFrame?.referenceFrame)
22
- )
23
-
24
- if (transform.physicalObject) {
25
- entity.add(traits.Geometry(transform.physicalObject))
26
- }
27
-
28
- if (transform.metadata) {
29
- const metadata = parseMetadata(transform.metadata.fields)
30
- if (metadata.color) {
31
- entity.add(traits.Color(metadata.color))
32
- }
33
- }
34
- }
35
-
36
- for (const drawing of snapshot.drawings) {
37
- world.spawn(
38
- traits.Name(drawing.referenceFrame),
39
- traits.Pose(drawing.poseInObserverFrame?.pose),
40
- traits.Parent(drawing.poseInObserverFrame?.referenceFrame)
41
- // TODO: Add shape
42
- )
43
-
44
- if (drawing.metadata) {
45
- // add shape colors
46
- }
47
- }
48
- }
15
+ const cameraControls = useCameraControls()
49
16
 
50
17
  const fileDrop = useFileDrop(
51
18
  (result: FileDropperSuccess) => {
52
19
  switch (result.type) {
53
20
  case 'snapshot': {
54
- addSnapshotToWorld(result.snapshot)
21
+ spawnSnapshotEntities(world, result.snapshot)
22
+
23
+ const { sceneCamera } = result.snapshot.sceneMetadata ?? {}
24
+
25
+ if (sceneCamera) {
26
+ const { x = 0, y = 0, z = 0 } = sceneCamera.position ?? {}
27
+ const { x: lx = 0, y: ly = 0, z: lz = 0 } = sceneCamera.lookAt ?? {}
28
+
29
+ cameraControls.setPose({
30
+ position: [x * 0.001, y * 0.001, z * 0.001],
31
+ lookAt: [lx * 0.001, ly * 0.001, lz * 0.001],
32
+ })
33
+ }
34
+
55
35
  break
56
36
  }
57
37
  case 'pcd':
58
38
  world.spawn(
59
39
  traits.Name(result.name),
60
- traits.PointsGeometry(result.pcd.positions),
40
+ traits.PointsPositions(result.pcd.positions),
61
41
  result.pcd.colors ? traits.VertexColors(result.pcd.colors) : traits.Color,
62
42
  traits.DroppedFile
63
43
  )
@@ -1,11 +1,25 @@
1
+ <script
2
+ module
3
+ lang="ts"
4
+ >
5
+ import { GLTFLoader, DRACOLoader } from 'three/examples/jsm/Addons.js'
6
+
7
+ const dracoLoader = new DRACOLoader()
8
+ const gltfLoader = new GLTFLoader()
9
+
10
+ dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/')
11
+ gltfLoader.setDRACOLoader(dracoLoader)
12
+ </script>
13
+
1
14
  <script lang="ts">
2
15
  import { T, type Props as ThrelteProps } from '@threlte/core'
3
- import { Portal, PortalTarget } from '@threlte/extras'
16
+ import { Portal, PortalTarget, useGltfAnimations, type ThrelteGltf } from '@threlte/extras'
4
17
  import type { Snippet } from 'svelte'
5
- import type { Object3D } from 'three'
18
+ import { Group, type Object3D } from 'three'
6
19
  import { useObjectEvents } from '../hooks/useObjectEvents.svelte'
7
20
  import type { Entity } from 'koota'
8
21
  import { traits, useTrait } from '../ecs'
22
+ import { poseToObject3d } from '../transform'
9
23
 
10
24
  interface Props extends ThrelteProps<Object3D> {
11
25
  entity: Entity
@@ -14,23 +28,70 @@
14
28
 
15
29
  let { entity, children, ...rest }: Props = $props()
16
30
 
31
+ const { gltf, actions } = useGltfAnimations()
32
+
17
33
  const name = useTrait(() => entity, traits.Name)
18
34
  const parent = useTrait(() => entity, traits.Parent)
19
- const gltf = useTrait(() => entity, traits.GLTF)
35
+ const pose = useTrait(() => entity, traits.Pose)
36
+ const gltfTrait = useTrait(() => entity, traits.GLTF)
37
+ const scale = useTrait(() => entity, traits.Scale)
20
38
  const objectProps = useObjectEvents(() => entity)
39
+
40
+ const animationName = $derived(gltfTrait.current?.animationName)
41
+
42
+ const group = new Group()
43
+
44
+ $effect.pre(() => {
45
+ if (pose.current) {
46
+ poseToObject3d(pose.current, group)
47
+ }
48
+ })
49
+
50
+ $effect.pre(() => {
51
+ if (!gltfTrait.current) {
52
+ return
53
+ }
54
+
55
+ const { source } = gltfTrait.current
56
+
57
+ const load = async () => {
58
+ if ('url' in source) {
59
+ $gltf = (await gltfLoader.loadAsync(source.url)) as ThrelteGltf
60
+ } else if ('glb' in source) {
61
+ const buffer = source.glb.buffer.slice(
62
+ source.glb.byteOffset,
63
+ source.glb.byteOffset + source.glb.byteLength
64
+ )
65
+ $gltf = (await gltfLoader.parseAsync(buffer, '')) as ThrelteGltf
66
+ } else if ('gltf' in source) {
67
+ $gltf = source.gltf as ThrelteGltf
68
+ }
69
+ }
70
+
71
+ load()
72
+ })
73
+
74
+ $effect.pre(() => {
75
+ if (animationName) {
76
+ $actions[animationName]?.play()
77
+ }
78
+ })
21
79
  </script>
22
80
 
23
81
  <Portal id={parent.current}>
24
- {#if gltf.current?.scene}
25
- <T
26
- is={gltf.current.scene as Object3D}
27
- name={name.current}
28
- {...objectProps}
29
- {...rest}
30
- >
31
- {@render children?.()}
32
-
33
- <PortalTarget id={name.current} />
34
- </T>
35
- {/if}
82
+ <T is={group}>
83
+ {#if $gltf}
84
+ <T
85
+ is={$gltf.scene as Object3D}
86
+ scale={[scale.current?.x ?? 1, scale.current?.y ?? 1, scale.current?.z ?? 1]}
87
+ name={name.current}
88
+ {...objectProps}
89
+ {...rest}
90
+ >
91
+ {@render children?.()}
92
+
93
+ <PortalTarget id={name.current} />
94
+ </T>
95
+ {/if}
96
+ </T>
36
97
  </Portal>
@@ -1,6 +1,6 @@
1
1
  import { type Props as ThrelteProps } from '@threlte/core';
2
2
  import type { Snippet } from 'svelte';
3
- import type { Object3D } from 'three';
3
+ import { type Object3D } from 'three';
4
4
  import type { Entity } from 'koota';
5
5
  interface Props extends ThrelteProps<Object3D> {
6
6
  entity: Entity;
@@ -1,8 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { T, useThrelte, type Props as ThrelteProps } from '@threlte/core'
3
3
  import { type Snippet } from 'svelte'
4
- import { meshBounds, MeshLineMaterial, MeshLineGeometry } from '@threlte/extras'
4
+ import { meshBounds } from '@threlte/extras'
5
5
  import { BufferGeometry, Color, DoubleSide, FrontSide, Group, Mesh } from 'three'
6
+ import { Line2, LineGeometry, LineMaterial } from 'three/examples/jsm/Addons.js'
6
7
  import { CapsuleGeometry } from '../three/CapsuleGeometry'
7
8
  import { colors, darkenColor } from '../color'
8
9
  import AxesHelper from './AxesHelper.svelte'
@@ -42,7 +43,8 @@
42
43
  const capsule = useTrait(() => entity, traits.Capsule)
43
44
  const sphere = useTrait(() => entity, traits.Sphere)
44
45
  const bufferGeometry = useTrait(() => entity, traits.BufferGeometry)
45
- const lineGeometry = useTrait(() => entity, traits.LineGeometry)
46
+ const linePositions = useTrait(() => entity, traits.LinePositions)
47
+ const lineWidth = useTrait(() => entity, traits.LineWidth)
46
48
  const center = useTrait(() => entity, traits.Center)
47
49
 
48
50
  const geometryType = $derived.by(() => {
@@ -50,7 +52,7 @@
50
52
  if (capsule.current) return 'capsule'
51
53
  if (sphere.current) return 'sphere'
52
54
  if (bufferGeometry.current) return 'buffer'
53
- if (lineGeometry.current) return 'line'
55
+ if (linePositions.current) return 'line'
54
56
  })
55
57
 
56
58
  const color = $derived.by(() => {
@@ -73,7 +75,7 @@
73
75
  return
74
76
  }
75
77
 
76
- const result = new Mesh()
78
+ const result = geometryType === 'line' ? new Line2() : new Mesh()
77
79
 
78
80
  if (geometryType === 'line') {
79
81
  result.raycast = meshBounds
@@ -111,7 +113,7 @@
111
113
 
112
114
  return () => {
113
115
  geo = undefined
114
- mesh.geometry.dispose()
116
+ mesh?.geometry?.dispose()
115
117
  }
116
118
  }
117
119
  })
@@ -137,8 +139,15 @@
137
139
  {/if}
138
140
 
139
141
  {#if !model || renderMode.includes('colliders')}
140
- {#if lineGeometry.current}
141
- <MeshLineGeometry points={lineGeometry.current} />
142
+ {#if linePositions.current}
143
+ <T
144
+ is={LineGeometry}
145
+ oncreate={(ref) => {
146
+ if (linePositions.current) {
147
+ ref.setPositions(linePositions.current)
148
+ }
149
+ }}
150
+ />
142
151
  {:else if box.current}
143
152
  {@const { x, y, z } = box.current ?? { x: 0, y: 0, z: 0 }}
144
153
  <T.BoxGeometry
@@ -161,16 +170,17 @@
161
170
  {/if}
162
171
  {/if}
163
172
 
164
- {#if lineGeometry.current}
165
- <MeshLineMaterial
173
+ {#if linePositions.current}
174
+ <T
175
+ is={LineMaterial}
166
176
  {color}
167
- width={0.005}
177
+ width={lineWidth.current ? lineWidth.current * 0.001 : 0.5}
168
178
  />
169
179
  {:else}
170
180
  <T.MeshToonMaterial
171
181
  {color}
172
182
  side={geometryType === 'buffer' ? DoubleSide : FrontSide}
173
- transparent
183
+ transparent={(opacity.current ?? 0.7) < 1}
174
184
  opacity={opacity.current ?? 0.7}
175
185
  />
176
186