@viamrobotics/motion-tools 1.1.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/WorldObject.svelte.js +23 -0
  2. package/dist/buffer.d.ts +50 -0
  3. package/dist/buffer.js +69 -0
  4. package/dist/color.d.ts +2 -0
  5. package/dist/color.js +20 -3
  6. package/dist/components/BatchedArrows.svelte +2 -2
  7. package/dist/components/BatchedGeometry.svelte +0 -0
  8. package/dist/components/BatchedGeometry.svelte.d.ts +26 -0
  9. package/dist/components/Entities.svelte +52 -37
  10. package/dist/components/FileDrop/FileDrop.svelte +18 -38
  11. package/dist/components/Frame.svelte +29 -12
  12. package/dist/components/GLTF.svelte +76 -15
  13. package/dist/components/GLTF.svelte.d.ts +1 -1
  14. package/dist/components/Geometry2.svelte +95 -94
  15. package/dist/components/Geometry2.svelte.d.ts +2 -1
  16. package/dist/components/Line.svelte +27 -28
  17. package/dist/components/LineDots.svelte +45 -0
  18. package/dist/components/LineDots.svelte.d.ts +9 -0
  19. package/dist/components/{Pointcloud.svelte → Points.svelte} +41 -6
  20. package/dist/components/Points.svelte.d.ts +10 -0
  21. package/dist/components/Scene.svelte +0 -1
  22. package/dist/components/Snapshot.svelte +60 -0
  23. package/dist/components/Snapshot.svelte.d.ts +21 -0
  24. package/dist/ecs/traits.d.ts +30 -12
  25. package/dist/ecs/traits.js +22 -13
  26. package/dist/hooks/useDrawAPI.svelte.js +23 -11
  27. package/dist/hooks/usePointclouds.svelte.js +2 -2
  28. package/dist/hooks/useSettings.svelte.d.ts +1 -1
  29. package/dist/hooks/useSettings.svelte.js +3 -0
  30. package/dist/hooks/useWorldState.svelte.js +9 -2
  31. package/dist/lib.d.ts +2 -1
  32. package/dist/lib.js +3 -2
  33. package/dist/snapshot.d.ts +7 -0
  34. package/dist/snapshot.js +255 -0
  35. package/package.json +1 -1
  36. package/dist/components/Pointcloud.svelte.d.ts +0 -9
@@ -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
  }
File without changes
@@ -0,0 +1,26 @@
1
+ export default BatchedGeometry;
2
+ type BatchedGeometry = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ declare const BatchedGeometry: $$__sveltets_2_IsomorphicComponent<{
10
+ [x: string]: never;
11
+ }, {
12
+ [evt: string]: CustomEvent<any>;
13
+ }, {}, {}, string>;
14
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
+ $$bindings?: Bindings;
17
+ } & Exports;
18
+ (internal: unknown, props: {
19
+ $$events?: Events;
20
+ $$slots?: Slots;
21
+ }): Exports & {
22
+ $set?: any;
23
+ $on?: any;
24
+ };
25
+ z_$$bindings?: Bindings;
26
+ }
@@ -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,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
3
  import { useObjectEvents } from '../hooks/useObjectEvents.svelte'
4
- import { Color, type Object3D } from 'three'
4
+ import { Color, Group, type Object3D } from 'three'
5
5
  import Geometry from './Geometry2.svelte'
6
6
  import { useWeblabs } from '../hooks/useWeblabs.svelte'
7
7
  import { useSelectedEntity } from '../hooks/useSelection.svelte'
@@ -13,6 +13,7 @@
13
13
  import { traits, useTrait } from '../ecs'
14
14
  import type { Pose } from '@viamrobotics/sdk'
15
15
  import { useResourceByName } from '../hooks/useResourceByName.svelte'
16
+ import { Portal, PortalTarget } from '@threlte/extras'
16
17
 
17
18
  interface Props {
18
19
  entity: Entity
@@ -22,14 +23,20 @@
22
23
 
23
24
  let { entity, pose, children }: Props = $props()
24
25
 
26
+ let ref = $state<Group>()
27
+
25
28
  const colorUtil = new Color()
29
+
26
30
  const settings = useSettings()
27
31
  const componentModels = use3DModels()
28
32
  const selectedEntity = useSelectedEntity()
29
33
  const resourceByName = useResourceByName()
30
34
  const weblabs = useWeblabs()
35
+
31
36
  const name = useTrait(() => entity, traits.Name)
37
+ const parent = useTrait(() => entity, traits.Parent)
32
38
  const entityColor = useTrait(() => entity, traits.Color)
39
+
33
40
  const events = useObjectEvents(() => entity)
34
41
  const resourceColor = $derived.by(() => {
35
42
  if (!name.current) {
@@ -66,14 +73,24 @@
66
73
  })
67
74
  </script>
68
75
 
69
- <Geometry
70
- {entity}
71
- {model}
72
- {pose}
73
- {children}
74
- renderMode={settings.current.renderArmModels}
75
- color={selectedEntity.current === entity
76
- ? `#${darkenColor(color, 75).getHexString()}`
77
- : `#${colorUtil.set(color).getHexString()}`}
78
- {...events}
79
- />
76
+ <Portal id={parent.current}>
77
+ <Geometry
78
+ bind:ref
79
+ {entity}
80
+ {model}
81
+ {pose}
82
+ renderMode={settings.current.renderArmModels}
83
+ color={selectedEntity.current === entity
84
+ ? `#${darkenColor(color, 75).getHexString()}`
85
+ : `#${colorUtil.set(color).getHexString()}`}
86
+ {...events}
87
+ >
88
+ {#if name.current}
89
+ <PortalTarget id={name.current} />
90
+ {/if}
91
+
92
+ {#if ref}
93
+ {@render children?.({ ref })}
94
+ {/if}
95
+ </Geometry>
96
+ </Portal>
@@ -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;