From ec4d602627723b2dba6cb96dcfa04da0c4b547c9 Mon Sep 17 00:00:00 2001 From: Matthew MacFarquhar Date: Tue, 14 Jul 2026 17:18:57 -0400 Subject: [PATCH 1/2] support stl load --- src/lib/ecs/traits.ts | 12 +++++++++--- src/lib/hooks/useGeometries.svelte.ts | 4 ++++ src/lib/stl.ts | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 src/lib/stl.ts diff --git a/src/lib/ecs/traits.ts b/src/lib/ecs/traits.ts index 444a8d2d1..02079438e 100644 --- a/src/lib/ecs/traits.ts +++ b/src/lib/ecs/traits.ts @@ -9,6 +9,7 @@ import { ColorFormat } from '$lib/buf/draw/v1/metadata_pb' import { createBox, createCapsule, createSphere } from '$lib/geometry' import { parsePcdInWorker } from '$lib/loaders/pcd' import { parsePlyInput } from '$lib/ply' +import { parseStlInput } from '$lib/stl' import { createPose, matrixToPose, poseToMatrix } from '$lib/transform' export const Name = trait(() => '') @@ -260,7 +261,12 @@ export const Geometry = (geometry: ViamGeometry) => { } else if (geometry.geometryType.case === 'sphere') { return Sphere(createSphere(geometry.geometryType.value)) } else if (geometry.geometryType.case === 'mesh') { - return BufferGeometry(parsePlyInput(geometry.geometryType.value.mesh)) + console.log(geometry.geometryType.value.contentType) + if (geometry.geometryType.value.contentType === 'stl') { + return BufferGeometry(parseStlInput(geometry.geometryType.value.mesh)) + } else if (geometry.geometryType.value.contentType === 'ply') { + return BufferGeometry(parsePlyInput(geometry.geometryType.value.mesh)) + } } return ReferenceFrame @@ -302,11 +308,11 @@ export const updateGeometryTrait = (entity: Entity, geometry?: ViamGeometry) => } else if (geometry.geometryType.case === 'mesh') { if (entity.has(BufferGeometry)) { const old = entity.get(BufferGeometry) - entity.set(BufferGeometry, parsePlyInput(geometry.geometryType.value.mesh)) + entity.set(BufferGeometry, parseStlInput(geometry.geometryType.value.mesh)) old?.dispose() } else { entity.remove(Box, Sphere, Capsule) - entity.add(BufferGeometry(parsePlyInput(geometry.geometryType.value.mesh))) + entity.add(BufferGeometry(parseStlInput(geometry.geometryType.value.mesh))) } } else if (geometry.geometryType.case === 'pointcloud') { updatePointCloud(entity, geometry.geometryType.value.pointCloud) diff --git a/src/lib/hooks/useGeometries.svelte.ts b/src/lib/hooks/useGeometries.svelte.ts index 2644d485c..9720d4372 100644 --- a/src/lib/hooks/useGeometries.svelte.ts +++ b/src/lib/hooks/useGeometries.svelte.ts @@ -189,6 +189,10 @@ export const provideGeometries = (partID: () => string) => { continue } + if (geometry.geometryType.case === 'mesh') { + geometry.geometryType.value.contentType = "stl"; + } + const entityTraits: ConfigurableTrait[] = [ ...hierarchy.parentTraits(name), traits.Name(label), diff --git a/src/lib/stl.ts b/src/lib/stl.ts new file mode 100644 index 000000000..2e850dc79 --- /dev/null +++ b/src/lib/stl.ts @@ -0,0 +1,18 @@ +import { BufferGeometry } from 'three' +import { STLLoader } from 'three/addons/loaders/STLLoader.js' + +const stlLoader = new STLLoader() + +export const parseStlInput = (mesh: string | Uint8Array): BufferGeometry => { + if (typeof mesh === 'string') { + return stlLoader.parse(atob(mesh)) + } + + if (mesh.length === 0) { + return new BufferGeometry() + } + + return stlLoader.parse( + mesh.buffer.slice(mesh.byteOffset, mesh.byteOffset + mesh.byteLength) as ArrayBuffer + ) +} \ No newline at end of file From 6cc83c436cc624766ee3c78a3c487d43f3f22713 Mon Sep 17 00:00:00 2001 From: Matthew MacFarquhar Date: Tue, 21 Jul 2026 12:13:22 -0400 Subject: [PATCH 2/2] add demo latency and throughput metrics --- src/lib/hooks/useGeometries.svelte.ts | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/lib/hooks/useGeometries.svelte.ts b/src/lib/hooks/useGeometries.svelte.ts index 9720d4372..b88bf567f 100644 --- a/src/lib/hooks/useGeometries.svelte.ts +++ b/src/lib/hooks/useGeometries.svelte.ts @@ -86,6 +86,60 @@ export const provideGeometries = (partID: () => string) => { [client.current?.name, createResourceQuery(client, 'getGeometries', () => options)] as const ) ) + + // Instrumentation: log per-fetch latency and rolling payload throughput + // for arm getGeometries calls, since arms are typically polled at high frequency. + const ARM_STATS_LOG_INTERVAL = 30 + const armQueryStats = new Map< + string, + { fetchStart?: number; count: number; totalLatency: number; totalBytes: number; windowStart: number } + >() + + $effect(() => { + for (const [name, query] of armQueries) { + if (!name) { + continue + } + + untrack(() => { + $effect(() => { + const stats = armQueryStats.get(name) ?? { + count: 0, + totalLatency: 0, + totalBytes: 0, + windowStart: performance.now(), + } + armQueryStats.set(name, stats) + + if (query.isFetching) { + stats.fetchStart = performance.now() + return + } + + if (stats.fetchStart === undefined) { + return + } + + const latency = performance.now() - stats.fetchStart + stats.fetchStart = undefined + stats.count += 1 + stats.totalLatency += latency + stats.totalBytes += + query.data?.reduce((sum, geometry) => sum + geometry.toBinary().byteLength, 0) ?? 0 + + if (stats.count % ARM_STATS_LOG_INTERVAL === 0) { + const elapsedSeconds = (performance.now() - stats.windowStart) / 1000 + const avgLatency = stats.totalLatency / stats.count + const throughputKBps = stats.totalBytes / 1024 / elapsedSeconds + + console.log( + `[🦾 ${name}] getGeometries — avg latency ${avgLatency.toFixed(1)}ms, throughput ${throughputKBps.toFixed(2)} KB/s (n=${stats.count})` + ) + } + }) + }) + } + }) const baseQueries = $derived( baseClients.map( (client) =>