@viamrobotics/motion-tools 1.0.2 → 1.1.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.
- package/dist/components/App.svelte +1 -1
- package/dist/components/Entities.svelte +7 -0
- package/dist/components/FileDrop/FileDrop.svelte +96 -0
- package/dist/components/FileDrop/FileDrop.svelte.d.ts +4 -0
- package/dist/components/FileDrop/file-dropper.d.ts +36 -0
- package/dist/components/FileDrop/file-dropper.js +6 -0
- package/dist/components/FileDrop/file-names.d.ts +27 -0
- package/dist/components/FileDrop/file-names.js +85 -0
- package/dist/components/FileDrop/pcd-dropper.d.ts +2 -0
- package/dist/components/FileDrop/pcd-dropper.js +27 -0
- package/dist/components/FileDrop/ply-dropper.d.ts +2 -0
- package/dist/components/FileDrop/ply-dropper.js +27 -0
- package/dist/components/FileDrop/snapshot-dropper.d.ts +2 -0
- package/dist/components/FileDrop/snapshot-dropper.js +96 -0
- package/dist/components/FileDrop/useFileDrop.svelte.d.ts +9 -0
- package/dist/components/FileDrop/useFileDrop.svelte.js +97 -0
- package/dist/components/GLTF.svelte +4 -4
- package/dist/components/Geometry2.svelte +15 -10
- package/dist/components/Line.svelte +5 -1
- package/dist/components/Tree/Tree.svelte +33 -26
- package/dist/components/Tree/Tree.svelte.d.ts +1 -1
- package/dist/components/Tree/TreeContainer.svelte +6 -3
- package/dist/components/Tree/buildTree.d.ts +5 -1
- package/dist/components/Tree/buildTree.js +5 -4
- package/dist/ecs/traits.d.ts +4 -0
- package/dist/ecs/traits.js +4 -0
- package/package.json +2 -1
- package/dist/components/FileDrop.svelte +0 -144
- package/dist/components/FileDrop.svelte.d.ts +0 -3
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import Dashboard from './dashboard/Dashboard.svelte'
|
|
14
14
|
import { domPortal } from '../portal'
|
|
15
15
|
import { provideSettings } from '../hooks/useSettings.svelte'
|
|
16
|
-
import FileDrop from './FileDrop.svelte'
|
|
16
|
+
import FileDrop from './FileDrop/FileDrop.svelte'
|
|
17
17
|
import { provideWeblabs } from '../hooks/useWeblabs.svelte'
|
|
18
18
|
import { providePartConfig } from '../hooks/usePartConfig.svelte'
|
|
19
19
|
import { useViamClient } from '@viamrobotics/svelte-sdk'
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const points = useQuery(traits.PointsGeometry)
|
|
14
14
|
const lines = useQuery(traits.LineGeometry)
|
|
15
15
|
const gltfs = useQuery(traits.GLTF)
|
|
16
|
+
const droppedMeshes = useQuery(traits.DroppedFile, traits.BufferGeometry)
|
|
16
17
|
const drawnMeshes = useQuery(
|
|
17
18
|
traits.DrawAPI,
|
|
18
19
|
Or(traits.Box, traits.Capsule, traits.Sphere, traits.BufferGeometry, traits.ReferenceFrame)
|
|
@@ -35,6 +36,12 @@
|
|
|
35
36
|
</Frame>
|
|
36
37
|
{/each}
|
|
37
38
|
|
|
39
|
+
{#each droppedMeshes.current as entity (entity)}
|
|
40
|
+
<Frame {entity}>
|
|
41
|
+
<Label text={entity.get(traits.Name)} />
|
|
42
|
+
</Frame>
|
|
43
|
+
{/each}
|
|
44
|
+
|
|
38
45
|
{#each points.current as entity (entity)}
|
|
39
46
|
<Pointcloud {entity}>
|
|
40
47
|
<Label text={entity.get(traits.Name)} />
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
3
|
+
import { useToast, ToastVariant } from '@viamrobotics/prime-core'
|
|
4
|
+
import { useFileDrop } from './useFileDrop.svelte'
|
|
5
|
+
import { useWorld } from '../../ecs/useWorld'
|
|
6
|
+
import type { FileDropperSuccess } from './file-dropper'
|
|
7
|
+
import { traits } from '../../ecs'
|
|
8
|
+
import { parseMetadata } from '../../WorldObject.svelte'
|
|
9
|
+
import type { Snapshot } from '../../draw/v1/snapshot_pb'
|
|
10
|
+
|
|
11
|
+
const props: HTMLAttributes<HTMLDivElement> = $props()
|
|
12
|
+
|
|
13
|
+
const world = useWorld()
|
|
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
|
+
}
|
|
49
|
+
|
|
50
|
+
const fileDrop = useFileDrop(
|
|
51
|
+
(result: FileDropperSuccess) => {
|
|
52
|
+
switch (result.type) {
|
|
53
|
+
case 'snapshot': {
|
|
54
|
+
addSnapshotToWorld(result.snapshot)
|
|
55
|
+
break
|
|
56
|
+
}
|
|
57
|
+
case 'pcd':
|
|
58
|
+
world.spawn(
|
|
59
|
+
traits.Name(result.name),
|
|
60
|
+
traits.PointsGeometry(result.pcd.positions),
|
|
61
|
+
result.pcd.colors ? traits.VertexColors(result.pcd.colors) : traits.Color,
|
|
62
|
+
traits.DroppedFile
|
|
63
|
+
)
|
|
64
|
+
break
|
|
65
|
+
case 'ply':
|
|
66
|
+
world.spawn(
|
|
67
|
+
traits.Name(result.name),
|
|
68
|
+
traits.BufferGeometry(result.ply),
|
|
69
|
+
traits.DroppedFile
|
|
70
|
+
)
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
toast({ message: `${result.name} loaded.`, variant: ToastVariant.Success })
|
|
75
|
+
},
|
|
76
|
+
(message) => toast({ message, variant: ToastVariant.Danger })
|
|
77
|
+
)
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<svelte:window
|
|
81
|
+
ondragenter={fileDrop.ondragenter}
|
|
82
|
+
ondragleave={fileDrop.ondragleave}
|
|
83
|
+
ondragover={fileDrop.ondragover}
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
<div
|
|
87
|
+
class={{
|
|
88
|
+
'fixed inset-0 z-9999': true,
|
|
89
|
+
'pointer-events-none': fileDrop.dropState === 'inactive',
|
|
90
|
+
'bg-black/10': fileDrop.dropState !== 'inactive',
|
|
91
|
+
}}
|
|
92
|
+
role="region"
|
|
93
|
+
aria-label="File drop zone"
|
|
94
|
+
ondrop={fileDrop.ondrop}
|
|
95
|
+
{...props}
|
|
96
|
+
></div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Snapshot } from '../../draw/v1/snapshot_pb';
|
|
2
|
+
import type { SuccessMessage } from '../../loaders/pcd/worker';
|
|
3
|
+
import type { BufferGeometry } from 'three';
|
|
4
|
+
interface FileDropSuccess {
|
|
5
|
+
success: true;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SnapshotFileDropSuccess extends FileDropSuccess {
|
|
9
|
+
type: 'snapshot';
|
|
10
|
+
snapshot: Snapshot;
|
|
11
|
+
}
|
|
12
|
+
export interface PointcloudFileDropSuccess extends FileDropSuccess {
|
|
13
|
+
type: 'pcd';
|
|
14
|
+
pcd: SuccessMessage;
|
|
15
|
+
}
|
|
16
|
+
export interface PlyFileDropSuccess extends FileDropSuccess {
|
|
17
|
+
type: 'ply';
|
|
18
|
+
ply: BufferGeometry;
|
|
19
|
+
}
|
|
20
|
+
export declare class FileDropperError extends Error {
|
|
21
|
+
constructor(message: string, options?: ErrorOptions);
|
|
22
|
+
}
|
|
23
|
+
export type FileDropperSuccess = SnapshotFileDropSuccess | PointcloudFileDropSuccess | PlyFileDropSuccess;
|
|
24
|
+
export interface FileDropperFailure {
|
|
25
|
+
success: false;
|
|
26
|
+
error: FileDropperError;
|
|
27
|
+
}
|
|
28
|
+
export type FileDropperResult = FileDropperSuccess | FileDropperFailure;
|
|
29
|
+
export type FileDropperParams = {
|
|
30
|
+
name: string;
|
|
31
|
+
extension: string;
|
|
32
|
+
prefix: string | undefined;
|
|
33
|
+
content: string | ArrayBuffer | null | undefined;
|
|
34
|
+
};
|
|
35
|
+
export type FileDropper = (params: FileDropperParams) => Promise<FileDropperResult>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ValueOf } from 'type-fest';
|
|
2
|
+
export declare const Extensions: {
|
|
3
|
+
readonly JSON: "json";
|
|
4
|
+
readonly PCD: "pcd";
|
|
5
|
+
readonly PLY: "ply";
|
|
6
|
+
readonly PB: "pb";
|
|
7
|
+
readonly PB_GZ: "pb.gz";
|
|
8
|
+
};
|
|
9
|
+
export declare const Prefixes: {
|
|
10
|
+
readonly Snapshot: "snapshot";
|
|
11
|
+
};
|
|
12
|
+
declare class FileNameError extends Error {
|
|
13
|
+
constructor(message: string, options?: ErrorOptions);
|
|
14
|
+
}
|
|
15
|
+
interface ParseFileSuccess {
|
|
16
|
+
success: true;
|
|
17
|
+
extension: ValueOf<typeof Extensions>;
|
|
18
|
+
prefix: ValueOf<typeof Prefixes> | undefined;
|
|
19
|
+
}
|
|
20
|
+
interface ParseFileError {
|
|
21
|
+
success: false;
|
|
22
|
+
error: FileNameError;
|
|
23
|
+
}
|
|
24
|
+
type ParseFileResult = ParseFileSuccess | ParseFileError;
|
|
25
|
+
export declare const parseFileName: (filename: string) => ParseFileResult;
|
|
26
|
+
export declare const readFile: (file: File, reader: FileReader, extension: ValueOf<typeof Extensions> | undefined) => void;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const Extensions = {
|
|
2
|
+
JSON: 'json',
|
|
3
|
+
PCD: 'pcd',
|
|
4
|
+
PLY: 'ply',
|
|
5
|
+
PB: 'pb',
|
|
6
|
+
PB_GZ: 'pb.gz',
|
|
7
|
+
};
|
|
8
|
+
export const Prefixes = {
|
|
9
|
+
Snapshot: 'snapshot',
|
|
10
|
+
};
|
|
11
|
+
class FileNameError extends Error {
|
|
12
|
+
constructor(message, options) {
|
|
13
|
+
super(message, options);
|
|
14
|
+
this.name = 'FileNameError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const isExtension = (extension) => {
|
|
18
|
+
return Object.values(Extensions).includes(extension);
|
|
19
|
+
};
|
|
20
|
+
const isPrefix = (prefix) => {
|
|
21
|
+
if (!prefix)
|
|
22
|
+
return false;
|
|
23
|
+
return Object.values(Prefixes).includes(prefix);
|
|
24
|
+
};
|
|
25
|
+
const validatePrefix = (extension, prefix) => {
|
|
26
|
+
switch (prefix) {
|
|
27
|
+
case Prefixes.Snapshot:
|
|
28
|
+
if (extension !== Extensions.JSON &&
|
|
29
|
+
extension !== Extensions.PB &&
|
|
30
|
+
extension !== Extensions.PB_GZ) {
|
|
31
|
+
return new FileNameError(`Only ${Extensions.JSON}, ${Extensions.PB} and ${Extensions.PB_GZ} snapshot files are supported.`);
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
};
|
|
37
|
+
export const parseFileName = (filename) => {
|
|
38
|
+
const [name, ...extensions] = filename.split('.');
|
|
39
|
+
const suffix = extensions.at(-1);
|
|
40
|
+
if (!suffix) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
error: new FileNameError('Could not determine file extension.'),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const nested = extensions.at(-2);
|
|
47
|
+
let extension = suffix;
|
|
48
|
+
if (nested) {
|
|
49
|
+
const nestedExtension = `${nested}.${suffix}`;
|
|
50
|
+
if (isExtension(nestedExtension)) {
|
|
51
|
+
extension = nestedExtension;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!isExtension(extension)) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: new FileNameError(`Only ${Object.values(Extensions).join(', ')} files are supported.`),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const prefix = name.split('_').at(0);
|
|
61
|
+
if (isPrefix(prefix)) {
|
|
62
|
+
const error = validatePrefix(extension, prefix);
|
|
63
|
+
if (error) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
error,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { success: true, extension, prefix };
|
|
70
|
+
}
|
|
71
|
+
return { success: true, extension, prefix: undefined };
|
|
72
|
+
};
|
|
73
|
+
export const readFile = (file, reader, extension) => {
|
|
74
|
+
if (!extension)
|
|
75
|
+
return;
|
|
76
|
+
switch (extension) {
|
|
77
|
+
case Extensions.JSON:
|
|
78
|
+
return reader.readAsText(file);
|
|
79
|
+
case Extensions.PCD:
|
|
80
|
+
case Extensions.PLY:
|
|
81
|
+
case Extensions.PB:
|
|
82
|
+
case Extensions.PB_GZ:
|
|
83
|
+
return reader.readAsArrayBuffer(file);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isArrayBuffer } from 'lodash-es';
|
|
2
|
+
import { FileDropperError } from './file-dropper';
|
|
3
|
+
import { parsePcdInWorker } from '../../loaders/pcd';
|
|
4
|
+
export const pcdDropper = async (params) => {
|
|
5
|
+
const { name, content } = params;
|
|
6
|
+
if (!isArrayBuffer(content)) {
|
|
7
|
+
return {
|
|
8
|
+
success: false,
|
|
9
|
+
error: new FileDropperError(`${name} failed to load.`),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const result = await parsePcdInWorker(new Uint8Array(content));
|
|
14
|
+
return {
|
|
15
|
+
success: true,
|
|
16
|
+
name,
|
|
17
|
+
type: 'pcd',
|
|
18
|
+
pcd: result,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
error: new FileDropperError(`${name} failed to parse.`, { cause: error }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isArrayBuffer } from 'lodash-es';
|
|
2
|
+
import { FileDropperError } from './file-dropper';
|
|
3
|
+
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js';
|
|
4
|
+
export const plyDropper = async (params) => {
|
|
5
|
+
const { name, content } = params;
|
|
6
|
+
if (!isArrayBuffer(content)) {
|
|
7
|
+
return {
|
|
8
|
+
success: false,
|
|
9
|
+
error: new FileDropperError(`${name} failed to load.`),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const geometry = new PLYLoader().parse(new TextDecoder().decode(content));
|
|
14
|
+
return {
|
|
15
|
+
success: true,
|
|
16
|
+
name,
|
|
17
|
+
type: 'ply',
|
|
18
|
+
ply: geometry,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
error: new FileDropperError(`${name} failed to parse.`, { cause: error }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Snapshot } from '../../draw/v1/snapshot_pb';
|
|
2
|
+
import { isArrayBuffer, isString } from 'lodash-es';
|
|
3
|
+
import { FileDropperError, } from './file-dropper';
|
|
4
|
+
import { Extensions } from './file-names';
|
|
5
|
+
const decodeJson = (params) => {
|
|
6
|
+
const { name, content } = params;
|
|
7
|
+
if (!isString(content)) {
|
|
8
|
+
return {
|
|
9
|
+
success: false,
|
|
10
|
+
error: new FileDropperError(`${name} failed to load.`),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const snapshot = Snapshot.fromJsonString(content);
|
|
15
|
+
return {
|
|
16
|
+
success: true,
|
|
17
|
+
name,
|
|
18
|
+
type: 'snapshot',
|
|
19
|
+
snapshot,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
error: new FileDropperError(`${name} failed to parse.`, { cause: error }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const decodeBinary = (params) => {
|
|
30
|
+
const { name, content } = params;
|
|
31
|
+
if (!isArrayBuffer(content)) {
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
error: new FileDropperError(`${name} failed to load.`),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const snapshot = Snapshot.fromBinary(new Uint8Array(content));
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
name,
|
|
42
|
+
type: 'snapshot',
|
|
43
|
+
snapshot,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: new FileDropperError(`${name} failed to parse.`, { cause: error }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const decodeGzip = async (params) => {
|
|
54
|
+
const { name, content } = params;
|
|
55
|
+
if (!isArrayBuffer(content)) {
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
error: new FileDropperError(`${name} failed to load.`),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const decompressor = new DecompressionStream('gzip');
|
|
63
|
+
const blob = new Blob([content]);
|
|
64
|
+
const stream = blob.stream().pipeThrough(decompressor);
|
|
65
|
+
const response = await new Response(stream).blob();
|
|
66
|
+
const buffer = await response.arrayBuffer();
|
|
67
|
+
const snapshot = Snapshot.fromBinary(new Uint8Array(buffer));
|
|
68
|
+
return {
|
|
69
|
+
success: true,
|
|
70
|
+
name,
|
|
71
|
+
type: 'snapshot',
|
|
72
|
+
snapshot,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: new FileDropperError(`${name} failed to parse.`, { cause: error }),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
export const snapshotDropper = async (params) => {
|
|
83
|
+
switch (params.extension) {
|
|
84
|
+
case 'json':
|
|
85
|
+
return decodeJson(params);
|
|
86
|
+
case 'pb':
|
|
87
|
+
return decodeBinary(params);
|
|
88
|
+
case 'pb.gz':
|
|
89
|
+
return decodeGzip(params);
|
|
90
|
+
default:
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: new FileDropperError(`Only ${Extensions.JSON}, ${Extensions.PB} and ${Extensions.PB_GZ} snapshot files are supported.`),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FileDropperSuccess } from './file-dropper';
|
|
2
|
+
export type DropStates = 'inactive' | 'hovering' | 'loading';
|
|
3
|
+
export declare const useFileDrop: (onSuccess: (result: FileDropperSuccess) => void, onError: (message: string) => void) => {
|
|
4
|
+
readonly dropState: DropStates;
|
|
5
|
+
ondrop: (event: DragEvent) => void;
|
|
6
|
+
ondragenter: (event: DragEvent) => void;
|
|
7
|
+
ondragover: (event: DragEvent) => void;
|
|
8
|
+
ondragleave: (event: DragEvent) => void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Extensions, parseFileName, Prefixes, readFile } from './file-names';
|
|
2
|
+
import { pcdDropper } from './pcd-dropper';
|
|
3
|
+
import { plyDropper } from './ply-dropper';
|
|
4
|
+
import { snapshotDropper } from './snapshot-dropper';
|
|
5
|
+
const createFileDropper = (extension, prefix) => {
|
|
6
|
+
switch (prefix) {
|
|
7
|
+
case Prefixes.Snapshot:
|
|
8
|
+
return snapshotDropper;
|
|
9
|
+
}
|
|
10
|
+
switch (extension) {
|
|
11
|
+
case Extensions.PCD:
|
|
12
|
+
return pcdDropper;
|
|
13
|
+
case Extensions.PLY:
|
|
14
|
+
return plyDropper;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
};
|
|
18
|
+
export const useFileDrop = (onSuccess, onError) => {
|
|
19
|
+
let dropState = $state('inactive');
|
|
20
|
+
// prevent default to allow drop
|
|
21
|
+
const ondragenter = (event) => {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
dropState = 'hovering';
|
|
24
|
+
};
|
|
25
|
+
// prevent default to allow drop
|
|
26
|
+
const ondragover = (event) => {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
};
|
|
29
|
+
const ondragleave = (event) => {
|
|
30
|
+
// only deactivate if really leaving the window
|
|
31
|
+
if (event.relatedTarget === null) {
|
|
32
|
+
dropState = 'inactive';
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const handleError = (error) => {
|
|
36
|
+
onError(error);
|
|
37
|
+
dropState = 'inactive';
|
|
38
|
+
};
|
|
39
|
+
const ondrop = (event) => {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
if (event.dataTransfer === null)
|
|
42
|
+
return;
|
|
43
|
+
const { files } = event.dataTransfer;
|
|
44
|
+
let completed = 0;
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const fileName = parseFileName(file.name);
|
|
47
|
+
if (!fileName.success) {
|
|
48
|
+
handleError(fileName.error.message);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const { extension, prefix } = fileName;
|
|
52
|
+
const reader = new FileReader();
|
|
53
|
+
reader.addEventListener('loadend', () => {
|
|
54
|
+
completed += 1;
|
|
55
|
+
if (completed === files.length) {
|
|
56
|
+
dropState = 'inactive';
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
reader.addEventListener('error', (event) => {
|
|
60
|
+
const error = event.target?.error?.message;
|
|
61
|
+
console.error(`${file.name} failed to load.`, error);
|
|
62
|
+
handleError(`${file.name} failed to load.`);
|
|
63
|
+
});
|
|
64
|
+
reader.addEventListener('load', async (event) => {
|
|
65
|
+
const content = event.target?.result;
|
|
66
|
+
const dropper = createFileDropper(extension, prefix);
|
|
67
|
+
if (!dropper) {
|
|
68
|
+
handleError(`${file.name} is not a supported file type.`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const result = await dropper({
|
|
72
|
+
name: file.name,
|
|
73
|
+
extension,
|
|
74
|
+
prefix,
|
|
75
|
+
content,
|
|
76
|
+
});
|
|
77
|
+
if (!result.success) {
|
|
78
|
+
handleError(result.error.message);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
onSuccess(result);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
readFile(file, reader, extension);
|
|
85
|
+
dropState = 'loading';
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
get dropState() {
|
|
90
|
+
return dropState;
|
|
91
|
+
},
|
|
92
|
+
ondrop,
|
|
93
|
+
ondragenter,
|
|
94
|
+
ondragover,
|
|
95
|
+
ondragleave,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
const objectProps = useObjectEvents(() => entity)
|
|
21
21
|
</script>
|
|
22
22
|
|
|
23
|
-
{
|
|
24
|
-
|
|
23
|
+
<Portal id={parent.current}>
|
|
24
|
+
{#if gltf.current?.scene}
|
|
25
25
|
<T
|
|
26
26
|
is={gltf.current.scene as Object3D}
|
|
27
27
|
name={name.current}
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
|
|
33
33
|
<PortalTarget id={name.current} />
|
|
34
34
|
</T>
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
{/if}
|
|
36
|
+
</Portal>
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
const sphere = useTrait(() => entity, traits.Sphere)
|
|
49
49
|
const bufferGeometry = useTrait(() => entity, traits.BufferGeometry)
|
|
50
50
|
const lineGeometry = useTrait(() => entity, traits.LineGeometry)
|
|
51
|
-
const pointsGeometry = useTrait(() => entity, traits.PointsGeometry)
|
|
52
51
|
const center = useTrait(() => entity, traits.Center)
|
|
53
52
|
|
|
54
53
|
const geometryType = $derived.by(() => {
|
|
@@ -57,7 +56,6 @@
|
|
|
57
56
|
if (sphere.current) return 'sphere'
|
|
58
57
|
if (bufferGeometry.current) return 'buffer'
|
|
59
58
|
if (lineGeometry.current) return 'line'
|
|
60
|
-
if (pointsGeometry.current) return 'points'
|
|
61
59
|
})
|
|
62
60
|
|
|
63
61
|
const color = $derived.by(() => {
|
|
@@ -80,7 +78,7 @@
|
|
|
80
78
|
|
|
81
79
|
const result = new Mesh()
|
|
82
80
|
|
|
83
|
-
if (geometryType === '
|
|
81
|
+
if (geometryType === 'line') {
|
|
84
82
|
result.raycast = meshBounds
|
|
85
83
|
}
|
|
86
84
|
|
|
@@ -108,6 +106,18 @@
|
|
|
108
106
|
const oncreate = (ref: BufferGeometry) => {
|
|
109
107
|
geo = ref
|
|
110
108
|
}
|
|
109
|
+
|
|
110
|
+
$effect.pre(() => {
|
|
111
|
+
if (mesh && bufferGeometry.current) {
|
|
112
|
+
mesh.geometry = bufferGeometry.current
|
|
113
|
+
oncreate(bufferGeometry.current)
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
geo = undefined
|
|
117
|
+
mesh.geometry.dispose()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
})
|
|
111
121
|
</script>
|
|
112
122
|
|
|
113
123
|
<Portal id={parent.current}>
|
|
@@ -124,19 +134,14 @@
|
|
|
124
134
|
<T
|
|
125
135
|
is={mesh}
|
|
126
136
|
name={name.current}
|
|
127
|
-
bvh={{ enabled:
|
|
137
|
+
bvh={{ enabled: geometryType === 'buffer' }}
|
|
128
138
|
>
|
|
129
139
|
{#if model && renderMode.includes('model')}
|
|
130
140
|
<T is={model} />
|
|
131
141
|
{/if}
|
|
132
142
|
|
|
133
143
|
{#if !model || renderMode.includes('colliders')}
|
|
134
|
-
{#if
|
|
135
|
-
<T
|
|
136
|
-
is={bufferGeometry.current}
|
|
137
|
-
{oncreate}
|
|
138
|
-
/>
|
|
139
|
-
{:else if lineGeometry.current}
|
|
144
|
+
{#if lineGeometry.current}
|
|
140
145
|
<MeshLineGeometry points={lineGeometry.current} />
|
|
141
146
|
{:else if box.current}
|
|
142
147
|
{@const { x, y, z } = box.current ?? { x: 0, y: 0, z: 0 }}
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
<Frame {entity} />
|
|
23
23
|
|
|
24
24
|
{#if dotColor.current && points.current}
|
|
25
|
-
<InstancedMesh
|
|
25
|
+
<InstancedMesh
|
|
26
|
+
frustumCulled={false}
|
|
27
|
+
bvh={{ enabled: false }}
|
|
28
|
+
raycast={() => null}
|
|
29
|
+
>
|
|
26
30
|
<T.SphereGeometry />
|
|
27
31
|
<T.MeshBasicMaterial color={[dotColor.current.r, dotColor.current.g, dotColor.current.b]} />
|
|
28
32
|
|
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import * as tree from '@zag-js/tree-view'
|
|
3
3
|
import { useMachine, normalizeProps } from '@zag-js/svelte'
|
|
4
|
-
import { untrack } from 'svelte'
|
|
5
4
|
import { ChevronRight, Eye, EyeOff } from 'lucide-svelte'
|
|
6
5
|
import { useVisibility } from '../../hooks/useVisibility.svelte'
|
|
7
6
|
import type { TreeNode } from './buildTree'
|
|
8
|
-
import { useExpanded } from './useExpanded.svelte'
|
|
9
7
|
import { VirtualList } from 'svelte-virtuallists'
|
|
10
|
-
import { observe } from '@threlte/core'
|
|
11
8
|
import { Icon } from '@viamrobotics/prime-core'
|
|
12
9
|
import { traits } from '../../ecs'
|
|
10
|
+
import { useSelectedEntity } from '../../hooks/useSelection.svelte'
|
|
11
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
13
12
|
|
|
13
|
+
const selected = useSelectedEntity()
|
|
14
14
|
const visibility = useVisibility()
|
|
15
|
-
const expanded = useExpanded()
|
|
16
15
|
|
|
17
16
|
interface Props {
|
|
18
17
|
rootNode: TreeNode
|
|
19
|
-
|
|
18
|
+
nodeMap: Record<string, TreeNode | undefined>
|
|
20
19
|
dragElement?: HTMLElement
|
|
21
20
|
onSelectionChange?: (event: tree.SelectionChangeDetails) => void
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
let { rootNode,
|
|
23
|
+
let { rootNode, nodeMap, onSelectionChange, dragElement = $bindable() }: Props = $props()
|
|
25
24
|
|
|
26
25
|
const collection = $derived(
|
|
27
26
|
tree.collection<TreeNode>({
|
|
@@ -31,39 +30,47 @@
|
|
|
31
30
|
})
|
|
32
31
|
)
|
|
33
32
|
|
|
33
|
+
const selectedValue = $derived(selected.current ? [`${selected.current}`] : [])
|
|
34
|
+
const expandedValues = new SvelteSet<string>()
|
|
35
|
+
|
|
36
|
+
$effect(() => {
|
|
37
|
+
let name = selected.current?.get(traits.Name)
|
|
38
|
+
let node = nodeMap[name ?? '']
|
|
39
|
+
while (node) {
|
|
40
|
+
expandedValues.add(`${node.entity}`)
|
|
41
|
+
node = node.parent
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
34
45
|
const id = $props.id()
|
|
35
|
-
const service = useMachine(tree.machine, {
|
|
46
|
+
const service = useMachine(tree.machine, () => ({
|
|
36
47
|
id,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
collection,
|
|
49
|
+
selectedValue,
|
|
50
|
+
expandedValue: [...expandedValues],
|
|
40
51
|
onSelectionChange(details) {
|
|
41
52
|
onSelectionChange?.(details)
|
|
42
53
|
},
|
|
43
54
|
onExpandedChange(details) {
|
|
44
|
-
|
|
55
|
+
expandedValues.clear()
|
|
45
56
|
for (const value of details.expandedValue) {
|
|
46
|
-
|
|
57
|
+
expandedValues.add(value)
|
|
47
58
|
}
|
|
48
59
|
},
|
|
49
|
-
})
|
|
60
|
+
}))
|
|
50
61
|
|
|
51
62
|
const api = $derived(tree.connect(service, normalizeProps))
|
|
63
|
+
const rootChildren = $derived(collection.rootNode.children ?? [])
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
api.setSelectedValue(selections)
|
|
58
|
-
})
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
observe(
|
|
62
|
-
() => [expanded],
|
|
63
|
-
() => untrack(() => api.setExpandedValue([...expanded]))
|
|
64
|
-
)
|
|
65
|
+
$effect(() => {
|
|
66
|
+
const element = document.querySelector(
|
|
67
|
+
`[data-scope="tree-view"][data-value="${selected.current}"]`
|
|
68
|
+
)
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
requestAnimationFrame(() => {
|
|
71
|
+
element?.scrollIntoView({ block: 'nearest' })
|
|
72
|
+
})
|
|
73
|
+
})
|
|
67
74
|
</script>
|
|
68
75
|
|
|
69
76
|
{#snippet treeNode({
|
|
@@ -2,7 +2,7 @@ import * as tree from '@zag-js/tree-view';
|
|
|
2
2
|
import type { TreeNode } from './buildTree';
|
|
3
3
|
interface Props {
|
|
4
4
|
rootNode: TreeNode;
|
|
5
|
-
|
|
5
|
+
nodeMap: Record<string, TreeNode | undefined>;
|
|
6
6
|
dragElement?: HTMLElement;
|
|
7
7
|
onSelectionChange?: (event: tree.SelectionChangeDetails) => void;
|
|
8
8
|
}
|
|
@@ -35,14 +35,17 @@
|
|
|
35
35
|
|
|
36
36
|
const worldEntity = world.spawn(IsExcluded, traits.Name('World'))
|
|
37
37
|
|
|
38
|
-
let children = $state<TreeNode[]>([])
|
|
38
|
+
let children = $state.raw<TreeNode[]>([])
|
|
39
|
+
let nodeMap = $state.raw<Record<string, TreeNode | undefined>>({})
|
|
39
40
|
|
|
40
41
|
let pending = false
|
|
41
42
|
const flush = () => {
|
|
42
43
|
if (pending) return
|
|
43
44
|
pending = true
|
|
44
45
|
window.setTimeout(() => {
|
|
45
|
-
|
|
46
|
+
const results = buildTreeNodes(world.query(traits.Name))
|
|
47
|
+
children = results.rootNodes
|
|
48
|
+
nodeMap = results.nodeMap
|
|
46
49
|
pending = false
|
|
47
50
|
})
|
|
48
51
|
}
|
|
@@ -86,8 +89,8 @@
|
|
|
86
89
|
>
|
|
87
90
|
<Tree
|
|
88
91
|
{rootNode}
|
|
92
|
+
{nodeMap}
|
|
89
93
|
bind:dragElement
|
|
90
|
-
selections={selectedEntity.current ? [`${selectedEntity.current}`] : []}
|
|
91
94
|
onSelectionChange={(event) => {
|
|
92
95
|
const value = event.selectedValue[0]
|
|
93
96
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { Entity, QueryResult, Trait } from 'koota';
|
|
2
2
|
export interface TreeNode {
|
|
3
3
|
entity: Entity;
|
|
4
|
+
parent?: TreeNode;
|
|
4
5
|
children?: TreeNode[];
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
7
8
|
* Creates a tree representing parent child / relationships from a set of frames.
|
|
8
9
|
*/
|
|
9
|
-
export declare const buildTreeNodes: (entities: QueryResult<[Trait]>) =>
|
|
10
|
+
export declare const buildTreeNodes: (entities: QueryResult<[Trait]>) => {
|
|
11
|
+
rootNodes: TreeNode[];
|
|
12
|
+
nodeMap: Record<string, TreeNode | undefined>;
|
|
13
|
+
};
|
|
@@ -3,14 +3,14 @@ import { traits } from '../../ecs';
|
|
|
3
3
|
* Creates a tree representing parent child / relationships from a set of frames.
|
|
4
4
|
*/
|
|
5
5
|
export const buildTreeNodes = (entities) => {
|
|
6
|
-
const nodeMap =
|
|
6
|
+
const nodeMap = {};
|
|
7
7
|
const rootNodes = [];
|
|
8
8
|
const childNodes = [];
|
|
9
9
|
for (const entity of entities) {
|
|
10
10
|
const parent = entity.get(traits.Parent);
|
|
11
11
|
const name = entity.get(traits.Name) ?? '';
|
|
12
12
|
const node = { entity };
|
|
13
|
-
nodeMap
|
|
13
|
+
nodeMap[name] = node;
|
|
14
14
|
if (!parent || parent === 'world') {
|
|
15
15
|
rootNodes.push(node);
|
|
16
16
|
}
|
|
@@ -21,12 +21,13 @@ export const buildTreeNodes = (entities) => {
|
|
|
21
21
|
for (const node of childNodes) {
|
|
22
22
|
const parent = node.entity.get(traits.Parent);
|
|
23
23
|
if (parent) {
|
|
24
|
-
const parentNode = nodeMap
|
|
24
|
+
const parentNode = nodeMap[parent];
|
|
25
|
+
node.parent = parentNode;
|
|
25
26
|
if (parentNode) {
|
|
26
27
|
parentNode.children ??= [];
|
|
27
28
|
parentNode.children?.push(node);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
|
-
return rootNodes;
|
|
32
|
+
return { rootNodes, nodeMap };
|
|
32
33
|
};
|
package/dist/ecs/traits.d.ts
CHANGED
|
@@ -83,6 +83,10 @@ export declare const GLTF: import("koota").Trait<() => ThreeGltf>;
|
|
|
83
83
|
export declare const DrawAPI: import("koota").TagTrait;
|
|
84
84
|
export declare const GeometriesAPI: import("koota").TagTrait;
|
|
85
85
|
export declare const WorldStateStoreAPI: import("koota").TagTrait;
|
|
86
|
+
/**
|
|
87
|
+
* Marker trait for entities created from user-dropped files (PLY, PCD, etc.)
|
|
88
|
+
*/
|
|
89
|
+
export declare const DroppedFile: import("koota").TagTrait;
|
|
86
90
|
/**
|
|
87
91
|
* An entity with data from the FrameSystemConfig() API
|
|
88
92
|
*/
|
package/dist/ecs/traits.js
CHANGED
|
@@ -44,6 +44,10 @@ export const GLTF = trait(() => ({}));
|
|
|
44
44
|
export const DrawAPI = trait();
|
|
45
45
|
export const GeometriesAPI = trait();
|
|
46
46
|
export const WorldStateStoreAPI = trait();
|
|
47
|
+
/**
|
|
48
|
+
* Marker trait for entities created from user-dropped files (PLY, PCD, etc.)
|
|
49
|
+
*/
|
|
50
|
+
export const DroppedFile = trait();
|
|
47
51
|
/**
|
|
48
52
|
* An entity with data from the FrameSystemConfig() API
|
|
49
53
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viamrobotics/motion-tools",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Motion visualization with Viam",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
"test": "pnpm test:unit -- --run",
|
|
139
139
|
"test:coverage": "npx vitest run --coverage",
|
|
140
140
|
"test:e2e": "playwright test",
|
|
141
|
+
"test:e2e-ui": "playwright test --ui",
|
|
141
142
|
"model-pipeline:run": "node scripts/model-pipeline.js",
|
|
142
143
|
"release": "changeset publish"
|
|
143
144
|
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { traits, useWorld } from '../ecs'
|
|
3
|
-
import { parsePcdInWorker } from '../lib'
|
|
4
|
-
import { useToast, ToastVariant } from '@viamrobotics/prime-core'
|
|
5
|
-
import { PLYLoader } from 'three/examples/jsm/Addons.js'
|
|
6
|
-
|
|
7
|
-
let { ...rest } = $props()
|
|
8
|
-
|
|
9
|
-
const world = useWorld()
|
|
10
|
-
|
|
11
|
-
type DropStates = 'inactive' | 'hovering' | 'loading'
|
|
12
|
-
|
|
13
|
-
let dropState = $state<DropStates>('inactive')
|
|
14
|
-
|
|
15
|
-
// prevent default to allow drop
|
|
16
|
-
const ondragenter = (event: DragEvent) => {
|
|
17
|
-
event.preventDefault()
|
|
18
|
-
dropState = 'hovering'
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// prevent default to allow drop
|
|
22
|
-
const ondragover = (event: DragEvent) => {
|
|
23
|
-
event.preventDefault()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const ondragleave = (event: DragEvent) => {
|
|
27
|
-
// only deactivate if really leaving the window
|
|
28
|
-
if (event.relatedTarget === null) {
|
|
29
|
-
dropState = 'inactive'
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const toast = useToast()
|
|
34
|
-
|
|
35
|
-
const extensions = {
|
|
36
|
-
PCD: 'pcd',
|
|
37
|
-
PLY: 'ply',
|
|
38
|
-
}
|
|
39
|
-
const supportedFiles = [extensions.PCD, extensions.PLY]
|
|
40
|
-
|
|
41
|
-
const ondrop = (event: DragEvent) => {
|
|
42
|
-
event.preventDefault()
|
|
43
|
-
|
|
44
|
-
if (event.dataTransfer === null) {
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let completed = 0
|
|
49
|
-
|
|
50
|
-
const { files } = event.dataTransfer
|
|
51
|
-
|
|
52
|
-
for (const file of files) {
|
|
53
|
-
const ext = file.name.split('.').at(-1)
|
|
54
|
-
|
|
55
|
-
if (!ext) {
|
|
56
|
-
toast({
|
|
57
|
-
message: `Could not determine file extension.`,
|
|
58
|
-
variant: ToastVariant.Danger,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
continue
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!supportedFiles.includes(ext)) {
|
|
65
|
-
toast({
|
|
66
|
-
message: `Only ${supportedFiles.map((file) => `.${file}`).join(', ')} files are supported.`,
|
|
67
|
-
variant: ToastVariant.Danger,
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
continue
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const reader = new FileReader()
|
|
74
|
-
|
|
75
|
-
reader.addEventListener('loadend', () => {
|
|
76
|
-
completed += 1
|
|
77
|
-
|
|
78
|
-
if (completed === files.length) {
|
|
79
|
-
dropState = 'inactive'
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
reader.addEventListener('error', () => {
|
|
84
|
-
toast({
|
|
85
|
-
message: `${file.name} failed to load.`,
|
|
86
|
-
variant: ToastVariant.Danger,
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
reader.addEventListener('load', async (event) => {
|
|
91
|
-
const arrayBuffer = event.target?.result
|
|
92
|
-
|
|
93
|
-
if (!arrayBuffer || typeof arrayBuffer === 'string') {
|
|
94
|
-
toast({
|
|
95
|
-
message: `${file.name} failed to load.`,
|
|
96
|
-
variant: ToastVariant.Danger,
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (ext === extensions.PCD) {
|
|
103
|
-
const result = await parsePcdInWorker(new Uint8Array(arrayBuffer))
|
|
104
|
-
|
|
105
|
-
world.spawn(
|
|
106
|
-
traits.Name(file.name),
|
|
107
|
-
traits.PointsGeometry(result.positions),
|
|
108
|
-
result.colors ? traits.VertexColors(result.colors) : traits.Color
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
toast({ message: `Loaded ${file.name}`, variant: ToastVariant.Success })
|
|
112
|
-
} else if (ext === extensions.PLY) {
|
|
113
|
-
const bufferGeometry = new PLYLoader().parse(arrayBuffer)
|
|
114
|
-
|
|
115
|
-
world.spawn(traits.Name(file.name), traits.BufferGeometry(bufferGeometry))
|
|
116
|
-
|
|
117
|
-
toast({ message: `Loaded ${file.name}`, variant: ToastVariant.Success })
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
reader.readAsArrayBuffer(file)
|
|
122
|
-
|
|
123
|
-
dropState = 'loading'
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
</script>
|
|
127
|
-
|
|
128
|
-
<svelte:window
|
|
129
|
-
{ondragenter}
|
|
130
|
-
{ondragleave}
|
|
131
|
-
{ondragover}
|
|
132
|
-
/>
|
|
133
|
-
|
|
134
|
-
<div
|
|
135
|
-
class={{
|
|
136
|
-
'fixed inset-0 z-9999 ': true,
|
|
137
|
-
'pointer-events-none': dropState === 'inactive',
|
|
138
|
-
'bg-black/10': dropState !== 'inactive',
|
|
139
|
-
}}
|
|
140
|
-
role="region"
|
|
141
|
-
aria-label="File drop zone"
|
|
142
|
-
{ondrop}
|
|
143
|
-
{...rest}
|
|
144
|
-
></div>
|