Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e8a46bd
use matrix traits
micheal-parks May 7, 2026
3526a22
rename
micheal-parks May 7, 2026
6373fa8
refactor
micheal-parks May 7, 2026
0d73e88
useTrait fixes
micheal-parks May 7, 2026
959f8d9
perf improvements
micheal-parks May 8, 2026
5fae23e
perf improvements
micheal-parks May 8, 2026
c22a68d
Merge branch 'main' of https://github.com/viamrobotics/acdc into matr…
micheal-parks May 8, 2026
5ec037f
Merge branch 'main' into matrix-trait
micheal-parks May 11, 2026
cc64d7b
better naming
micheal-parks May 11, 2026
fdf3a8e
Merge branch 'main' into matrix-trait
micheal-parks May 11, 2026
ef336f2
improve details panel
micheal-parks May 11, 2026
3f56b4e
Merge branch 'matrix-trait' of https://github.com/viamrobotics/acdc i…
micheal-parks May 11, 2026
a62bcd7
refactor
micheal-parks May 11, 2026
df698bb
cleanup
micheal-parks May 11, 2026
cd9a2c5
better name
micheal-parks May 11, 2026
b4da2e0
better naming
micheal-parks May 11, 2026
5473db0
better comments
micheal-parks May 11, 2026
49cf498
Create smooth-dots-search.md
micheal-parks May 11, 2026
7f8a0a6
Merge branch 'main' into matrix-trait
micheal-parks May 11, 2026
05251b2
better naming
micheal-parks May 11, 2026
80dc184
Merge branch 'matrix-trait' of https://github.com/viamrobotics/acdc i…
micheal-parks May 11, 2026
d1e9ee5
cleanup
micheal-parks May 11, 2026
0922c74
fix frame editing
micheal-parks May 11, 2026
a24288a
fix rendering bug
micheal-parks May 12, 2026
47dc95f
snap update
micheal-parks May 14, 2026
143f560
update snaps
micheal-parks May 14, 2026
256e06c
snapshot
micheal-parks May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/smooth-dots-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@viamrobotics/motion-tools": patch
---

Use local and world Matrix traits as object transform source-of-truth
4 changes: 2 additions & 2 deletions buf.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ deps:
commit: 62f35d8aed1149c291d606d958a7ce32
digest: b5:d66bf04adc77a0870bdc9328aaf887c7188a36fb02b83a480dc45ef9dc031b4d39fc6e9dc6435120ccf4fe5bfd5c6cb6592533c6c316595571f9a31420ab47fe
- name: buf.build/viamrobotics/api
commit: 41fbf2bec8b149d58e9048a8b28bcc10
digest: b5:1503808719cfe2323d1fcff7eda3d8c2fdb386a245619f239996ddba17636920dfac8c1c78b3bbc2264989c06fb5fa8088c64716cb34e04cb773946e5a09b47e
commit: e1e7b37b6b224400984f294513bb276c
digest: b5:e1f825a0a6210ef987b514e1b025fabcadc11ea20d33fe9be119339ab350ac8cb0875834f08e27efe326a0559f02d7740b322fcaf848ca64b326a4ffb642f732
Binary file modified e2e/draw-client.test.ts-snapshots/DRAW-GLTF-darwin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/draw-client.test.ts-snapshots/DRAW-POINT-CLOUDS-darwin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/draw-client.test.ts-snapshots/DRAW-POINTS-darwin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/draw-client.test.ts-snapshots/REMOVE-ALL-SETUP-darwin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/draw-client.test.ts-snapshots/REMOVE-ALL-darwin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/draw-client.test.ts-snapshots/REMOVE-DRAWINGS-darwin.png
Binary file modified e2e/draw-client.test.ts-snapshots/REMOVE-TRANSFORMS-darwin.png
Binary file modified e2e/draw-client.test.ts-snapshots/REPLAY-PLAYBACK-darwin.png
Binary file modified e2e/draw-client.test.ts-snapshots/REPLAY-RECORD-darwin.png
26 changes: 18 additions & 8 deletions e2e/edit-frame.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect, type Page } from '@playwright/test'
import { JsonValue, Struct, type ViamClient } from '@viamrobotics/sdk'

import {
activateConnectionConfigByHost,
applyMachineConfig,
connectOrgViamClient,
connectViamClient,
Expand Down Expand Up @@ -94,11 +95,18 @@ withRobot('basic edit frame', async ({ robotPage }) => {
await expect(page.getByText('Box', { exact: true })).toBeVisible()
await page.getByText('Box', { exact: true }).click()

await expect(page.getByLabel('mutable local position')).toBeVisible()
// The `mutable …` aria-label divs are invisible Svelte wrappers around
// Tweakpane widgets — their content gets portaled into Tweakpane's own
// pane DOM. Assert attachment (i.e. the conditional render flipped on)
// rather than visibility.
await expect(page.getByLabel('mutable local position')).toBeAttached()
await fillFrameInputs(page, 'mutable local position', ['100', '200', '300'])

await expect(page.getByLabel('mutable box dimensions')).toBeVisible()
await fillFrameInputs(page, 'mutable box dimensions', ['400', '500', '600'])
// `mutable box dimensions` attaches when the entity gets a Box trait, but
// the actual <input>s live in the outer Tweakpane pane DOM (under
// `mutable geometry`), not as descendants of the box-dimensions wrapper.
await expect(page.getByLabel('mutable box dimensions')).toBeAttached()
await fillFrameInputs(page, 'mutable geometry', ['400', '500', '600'])

await expect(page.getByText('Live updates paused', { exact: true })).toBeVisible()
try {
Expand Down Expand Up @@ -148,7 +156,7 @@ withRobot('basic edit frame', async ({ robotPage }) => {
}

// REPARENT THE OBJECT
await expect(page.getByLabel('mutable parent frame')).toBeVisible()
await expect(page.getByLabel('mutable parent frame')).toBeAttached()
await page.getByLabel('mutable parent frame').locator('select').selectOption('parent')

try {
Expand All @@ -173,7 +181,7 @@ withRobot('basic edit frame', async ({ robotPage }) => {
await expect(page.getByText('None', { exact: true }).first()).toBeVisible()
await page.getByText('None', { exact: true }).first().click()

await expect(page.getByLabel('mutable local position')).toBeVisible()
await expect(page.getByLabel('mutable local position')).toBeAttached()
await fillFrameInputs(page, 'mutable local position', ['0', '0', '0'])

// SAVE THE CHANGES
Expand Down Expand Up @@ -232,6 +240,7 @@ withRobot('create and delete frame', async ({ browser }) => {
await injectMachineConfig(page, config)
await page.reload()
await page.waitForLoadState('domcontentloaded')
await activateConnectionConfigByHost(page, config.host)

const machineConfigButton = page.getByRole('button', { name: 'Machine connection configs' })
await expect(machineConfigButton.getByText('live', { exact: true })).toBeVisible({
Expand Down Expand Up @@ -374,6 +383,7 @@ withRobot('fragment edit frame', async ({ browser }) => {
await injectMachineConfig(page, config)
await page.reload()
await page.waitForLoadState('domcontentloaded')
await activateConnectionConfigByHost(page, config.host)

const machineConfigButton = page.getByRole('button', { name: 'Machine connection configs' })
await expect(machineConfigButton.getByText('live', { exact: true })).toBeVisible({
Expand All @@ -396,11 +406,11 @@ withRobot('fragment edit frame', async ({ browser }) => {
await expect(page.getByText('Sphere', { exact: true })).toBeVisible()
await page.getByText('Sphere', { exact: true }).click()

await expect(page.getByLabel('mutable local position')).toBeVisible()
await expect(page.getByLabel('mutable local position')).toBeAttached()
await fillFrameInputs(page, 'mutable local position', ['100', '200', '300'])

await expect(page.getByLabel('mutable sphere dimensions')).toBeVisible()
await fillFrameInputs(page, 'mutable sphere dimensions', ['400'])
await expect(page.getByLabel('mutable sphere dimensions')).toBeAttached()
await fillFrameInputs(page, 'mutable geometry', ['400'])

// SAVE THE CHANGES
await expect(page.getByText('Live updates paused', { exact: true })).toBeVisible()
Expand Down
Binary file modified e2e/file-drop.test.ts-snapshots/FILE-DROP-PCD-darwin.png
Binary file modified e2e/file-drop.test.ts-snapshots/FILE-DROP-PLY-darwin.png
Binary file modified e2e/file-drop.test.ts-snapshots/FILE-DROP-UNSUPPORTED-darwin.png
33 changes: 32 additions & 1 deletion e2e/fixtures/with-robot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,42 @@ export const injectMachineConfig = async (page: Page, config: E2ETestConfig) =>
}
}

localStorage.setItem('active-connection-config', '0')
// Leave the active config unset (-1). The fixture activates the
// injected entry by host after reload, because the merged-list index
// depends on how many env configs the running dev server is serving —
// which the test can't know reliably (e.g. when `.env.local` defines
// VITE_CONFIGS and reuseExistingServer reuses that dev server).
localStorage.setItem('active-connection-config', '-1')
}),
config
)
}

export const activateConnectionConfigByHost = async (page: Page, host: string) => {
const configButton = page.getByRole('button', { name: 'Machine connection configs' })
await configButton.click()

await expect(async () => {
const rows = page.locator('form').filter({
has: page.locator('input[placeholder="Host"]'),
})
const count = await rows.count()
for (let index = 0; index < count; index += 1) {
const row = rows.nth(index)
if ((await row.locator('input[placeholder="Host"]').inputValue()) === host) {
const switchButton = row.getByRole('switch')
if ((await switchButton.getAttribute('aria-checked')) !== 'true') {
await switchButton.click()
}
return
}
}
throw new Error(`Connection config row for host "${host}" not found`)
}).toPass({ timeout: 10_000 })

await configButton.click()
}

export const connectViamClient = async (): Promise<ViamClient> => {
const config = getE2EConfig()
const opts: ViamClientOptions = {
Expand Down Expand Up @@ -160,6 +190,7 @@ export const withRobot = base.extend<{ robotPage: RobotTestPage }>({
await page.goto('/')
await injectMachineConfig(page, config)
await page.reload()
await activateConnectionConfigByHost(page, config.host)

const machineConfigButton = page.getByRole('button', { name: 'Machine connection configs' })

Expand Down
Binary file modified e2e/go-client.test.ts-snapshots/DRAW-FRAME-SYSTEM-darwin.png
Binary file modified e2e/go-client.test.ts-snapshots/DRAW-FRAMES-darwin.png
Binary file modified e2e/go-client.test.ts-snapshots/DRAW-GEOMETRIES-darwin.png
Binary file modified e2e/go-client.test.ts-snapshots/DRAW-GLTF-darwin.png
Binary file modified e2e/go-client.test.ts-snapshots/DRAW-LINES-darwin.png
Binary file modified e2e/go-client.test.ts-snapshots/DRAW-POINTS-darwin.png
Binary file modified e2e/go-client.test.ts-snapshots/DRAW-POSES-darwin.png
Binary file modified e2e/go-client.test.ts-snapshots/DRAW-WORLD-STATE-darwin.png
Binary file modified e2e/go-client.test.ts-snapshots/SET-CAMERA-POSE-darwin.png
Binary file modified e2e/snapshot.test.ts-snapshots/SNAPSHOT-DROP-BOX-PB-GZ-darwin.png
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export default defineConfig(
'unicorn/prevent-abbreviations': 'off',
'unicorn/require-module-specifiers': 'off',
'unicorn/prefer-global-this': 'off',
'unicorn/no-nested-ternary': 'off',

// TODO
// 'unicorn/filename-case': [
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
"@testing-library/jest-dom": "6.8.0",
"@testing-library/svelte": "5.2.8",
"@testing-library/user-event": "^14.6.1",
"@threlte/core": "8.5.13",
"@threlte/extras": "9.15.2",
"@threlte/core": "8.5.14",
"@threlte/extras": "9.17.0",
"@threlte/rapier": "3.4.1",
"@threlte/xr": "1.6.0",
"@types/bun": "1.2.21",
Expand Down
26 changes: 13 additions & 13 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 40 additions & 29 deletions src/lib/FrameConfigUpdater.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type { Vector3Like } from 'three'
import type { Frame } from '$lib/frame'

import { hierarchy, traits } from '$lib/ecs'
import { createPose, matrixToPose, poseToMatrix } from '$lib/transform'

const tempPose = createPose()

type UpdateFrameCallback = {
(componentName: string, referenceFrame: string, pose: Pose, geometry?: Frame['geometry']): void
Expand All @@ -28,19 +31,21 @@ export class FrameConfigUpdater {

if (x === undefined && y === undefined && z === undefined) return

const change: { x?: number; y?: number; z?: number } = {}
if (x !== undefined) change.x = x
if (y !== undefined) change.y = y
if (z !== undefined) change.z = z
const current = entity.get(traits.EditedMatrix)
if (!current) return
matrixToPose(current, tempPose)
if (x !== undefined) tempPose.x = x
if (y !== undefined) tempPose.y = y
if (z !== undefined) tempPose.z = z

entity.set(traits.EditedPose, change)
poseToMatrix(tempPose, current)
entity.changed(traits.EditedMatrix)

const name = entity.get(traits.Name)
const parent = hierarchy.getParentName(entity) ?? 'world'
const updatedPose = entity.get(traits.EditedPose)

if (name && updatedPose) {
this.updateFrame(name, parent, updatedPose)
if (name) {
this.updateFrame(name, parent, { ...tempPose })
}
}

Expand All @@ -59,27 +64,30 @@ export class FrameConfigUpdater {
return
}

const change: { oX?: number; oY?: number; oZ?: number; theta?: number } = {}
if (oX !== undefined) change.oX = oX
if (oY !== undefined) change.oY = oY
if (oZ !== undefined) change.oZ = oZ
if (theta !== undefined) change.theta = theta
const current = entity.get(traits.EditedMatrix)
if (!current) return
matrixToPose(current, tempPose)
if (oX !== undefined) tempPose.oX = oX
if (oY !== undefined) tempPose.oY = oY
if (oZ !== undefined) tempPose.oZ = oZ
if (theta !== undefined) tempPose.theta = theta

entity.set(traits.EditedPose, change)
poseToMatrix(tempPose, current)
entity.changed(traits.EditedMatrix)

const name = entity.get(traits.Name)
const parent = hierarchy.getParentName(entity) ?? 'world'
const updatedPose = entity.get(traits.EditedPose)

if (name && updatedPose) {
this.updateFrame(name, parent, updatedPose)
if (name) {
this.updateFrame(name, parent, { ...tempPose })
}
}

public updateGeometry = (entity: Entity, geometry: Partial<Frame['geometry']>) => {
const name = entity.get(traits.Name)
const parent = hierarchy.getParentName(entity) ?? 'world'
const pose = entity.get(traits.EditedPose)
const matrix = entity.get(traits.EditedMatrix)
if (matrix) matrixToPose(matrix, tempPose)

if (geometry?.type === 'box') {
const { x, y, z } = geometry
Expand All @@ -95,8 +103,8 @@ export class FrameConfigUpdater {

const box = entity.get(traits.Box)

if (name && box && pose) {
this.updateFrame(name, parent, pose, { type: 'box', ...box })
if (name && box && matrix) {
this.updateFrame(name, parent, { ...tempPose }, { type: 'box', ...box })
}
} else if (geometry?.type === 'sphere') {
const { r } = geometry
Expand All @@ -107,8 +115,8 @@ export class FrameConfigUpdater {

const sphere = entity.get(traits.Sphere)

if (name && sphere && pose) {
this.updateFrame(name, parent, pose, { type: 'sphere', ...sphere })
if (name && sphere && matrix) {
this.updateFrame(name, parent, { ...tempPose }, { type: 'sphere', ...sphere })
}
} else if (geometry?.type === 'capsule') {
const { r, l } = geometry
Expand All @@ -123,18 +131,19 @@ export class FrameConfigUpdater {

const capsule = entity.get(traits.Capsule)

if (name && capsule && pose) {
this.updateFrame(name, parent, pose, { type: 'capsule', ...capsule })
if (name && capsule && matrix) {
this.updateFrame(name, parent, { ...tempPose }, { type: 'capsule', ...capsule })
}
}
}

public setFrameParent = (entity: Entity, parentName: string) => {
const name = entity.get(traits.Name)
const pose = entity.get(traits.EditedPose)
const matrix = entity.get(traits.EditedMatrix)

if (name && pose) {
this.updateFrame(name, parentName, pose)
if (name && matrix) {
matrixToPose(matrix, tempPose)
this.updateFrame(name, parentName, { ...tempPose })
}
}

Expand All @@ -149,9 +158,11 @@ export class FrameConfigUpdater {
public setGeometryType = (entity: Entity, type: 'none' | 'box' | 'sphere' | 'capsule') => {
const name = entity.get(traits.Name)
const parent = hierarchy.getParentName(entity) ?? 'world'
const pose = entity.get(traits.EditedPose)
const matrix = entity.get(traits.EditedMatrix)

if (!name || !pose) return
if (!name || !matrix) return
matrixToPose(matrix, tempPose)
const pose: Pose = { ...tempPose }

if (type === 'none') {
this.updateFrame(name, parent, pose, { type: 'none' })
Expand Down
6 changes: 5 additions & 1 deletion src/lib/__tests__/draw.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ describe('drawTransform', () => {

expect(entity.get(traits.Name)).toBe('box-frame')
expect(hierarchy.getParentName(entity)).toBe('arm')
expect(entity.get(traits.Pose)).toStrictEqual(createPose({ x: 100, y: 200, z: 300 }))
// Pose translation is in mm; matrix translation is in m (× 0.001).
const matrix = entity.get(traits.Matrix)
expect(matrix?.elements[12]).toBeCloseTo(0.1)
expect(matrix?.elements[13]).toBeCloseTo(0.2)
expect(matrix?.elements[14]).toBeCloseTo(0.3)
expect(entity.get(traits.Box)).toStrictEqual({ x: 10, y: 20, z: 30 })
expect(entity.has(traits.ReferenceFrame)).toBe(false)
expect(entity.has(traits.ShowAxesHelper)).toBe(false)
Expand Down
Loading
Loading