Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
faf59c0
Add shared ECS traits and helpers for entity pose plugins
DTCurrie Jun 1, 2026
9e637c6
Add changeset
DTCurrie Jun 1, 2026
02bc0d0
Refactor Details panel into focused subcomponents
DTCurrie Jun 1, 2026
bf49747
Add changeset
DTCurrie Jun 1, 2026
9bd8689
Small component tweaks to support per-entity pose mutators
DTCurrie Jun 1, 2026
030b969
Add changeset
DTCurrie Jun 1, 2026
77eed59
Address review feedback on writeMatrix and useMouseRaycaster
github-actions[bot] Jun 1, 2026
c18c0e7
Merge branch 'main' into app-16292/1-ecs-utils
DTCurrie Jun 2, 2026
6854ba0
Address review feedback: hierarchy comment, disableTooltip test
github-actions[bot] Jun 2, 2026
0d33dea
Address review feedback on Details refactor
github-actions[bot] Jun 2, 2026
dfef837
Address review feedback: writeMatrix early exit, undefined tests, fir…
github-actions[bot] Jun 2, 2026
4b68aae
Merge branch 'app-16292/1-ecs-utils' into app-16292/2-details-refactor
DTCurrie Jun 2, 2026
c00b48a
Update src/lib/ecs/traits.ts
DTCurrie Jun 2, 2026
2bcdba2
Merge branch 'app-16292/1-ecs-utils' into app-16292/2-details-refactor
DTCurrie Jun 2, 2026
b18c167
Add invalidate() to subcomponents and fix Details handleParentChange
github-actions[bot] Jun 2, 2026
fd1cd53
Apply suggestion from @claude[bot]
DTCurrie Jun 2, 2026
b890b95
fix claude's bad suggestion
DTCurrie Jun 2, 2026
82d5076
cleanup
DTCurrie Jun 2, 2026
51809f3
Address review feedback on Details refactor
github-actions[bot] Jun 2, 2026
3741933
Address review feedback on writeMatrix and useMouseRaycaster
github-actions[bot] Jun 1, 2026
fea6014
Make measure tool a plugin (#732)
micheal-parks Jun 1, 2026
8d67f43
Address review feedback: writeMatrix early exit, undefined tests, fir…
github-actions[bot] Jun 2, 2026
db5c541
Update src/lib/ecs/traits.ts
DTCurrie Jun 2, 2026
83b98a6
Add invalidate() to subcomponents and fix Details handleParentChange
github-actions[bot] Jun 2, 2026
3d6bf32
Apply suggestion from @claude[bot]
DTCurrie Jun 2, 2026
e1b0cbe
fix claude's bad suggestion
DTCurrie Jun 2, 2026
e25aa16
cleanup
DTCurrie Jun 2, 2026
f8c93fb
Merge branch 'app-16292/2-details-refactor' into app-16292/3-componen…
DTCurrie Jun 2, 2026
b11891e
Add disableTooltip inactive test case to Button spec
github-actions[bot] Jun 2, 2026
1bc584f
Merge branch 'main' into app-16292/1-ecs-utils
DTCurrie Jun 2, 2026
8e8bc5e
Merge branch 'app-16292/1-ecs-utils' into app-16292/2-details-refactor
DTCurrie Jun 2, 2026
12b8101
Merge branch 'app-16292/2-details-refactor' into app-16292/3-componen…
DTCurrie Jun 2, 2026
ddd050a
add handlers for changes
DTCurrie Jun 2, 2026
e47694e
Merge branch 'app-16292/2-details-refactor' into app-16292/3-componen…
DTCurrie Jun 2, 2026
9ad63d7
use callbacks with no defaults
DTCurrie Jun 2, 2026
5061ec9
Merge branch 'app-16292/2-details-refactor' into app-16292/3-componen…
DTCurrie Jun 2, 2026
2450370
revert opacity behavior
DTCurrie Jun 2, 2026
1aeff3b
Merge branch 'app-16292/2-details-refactor' into app-16292/3-componen…
DTCurrie Jun 2, 2026
94f3b26
revert opacity fallbacks
DTCurrie Jun 2, 2026
081f852
split out opacity and axes helper details
DTCurrie Jun 2, 2026
ad9d95d
Merge branch 'app-16292/2-details-refactor' into app-16292/3-componen…
DTCurrie Jun 2, 2026
f2b8a19
line color updates
DTCurrie Jun 2, 2026
4c7ab2f
default to first hit
DTCurrie Jun 2, 2026
d076b3f
Merge branch 'main' into app-16292/1-ecs-utils
DTCurrie Jun 2, 2026
b7b7d7c
Refactor Details panel into focused subcomponents
DTCurrie Jun 1, 2026
168cd3e
Add changeset
DTCurrie Jun 1, 2026
473af2e
Address review feedback on Details refactor
github-actions[bot] Jun 2, 2026
873a67b
Add invalidate() to subcomponents and fix Details handleParentChange
github-actions[bot] Jun 2, 2026
72a6fce
Apply suggestion from @claude[bot]
DTCurrie Jun 2, 2026
a92007c
fix claude's bad suggestion
DTCurrie Jun 2, 2026
2d7c9c1
cleanup
DTCurrie Jun 2, 2026
d5a080e
add handlers for changes
DTCurrie Jun 2, 2026
17e814f
use callbacks with no defaults
DTCurrie Jun 2, 2026
76b80ef
revert opacity behavior
DTCurrie Jun 2, 2026
053d6e1
split out opacity and axes helper details
DTCurrie Jun 2, 2026
51d9b2b
line color updates
DTCurrie Jun 2, 2026
d3f4b78
Merge branch 'app-16292/2-details-refactor' of github.com:viam-labs/m…
DTCurrie Jun 2, 2026
70a5b6f
Merge branch 'app-16292/2-details-refactor' of github.com:viam-labs/m…
DTCurrie Jun 2, 2026
22b2dc9
Merge branch 'main' into app-16292/2-details-refactor
DTCurrie Jun 2, 2026
5626401
Merge branch 'app-16292/2-details-refactor' into app-16292/3-componen…
DTCurrie Jun 2, 2026
156feed
Apply suggestions from code review
DTCurrie Jun 3, 2026
299bed5
fix: correct useThrelte mock structure and remove duplicate guard
github-actions[bot] Jun 3, 2026
2cdca10
Apply suggestion from @claude[bot]
DTCurrie Jun 3, 2026
fb7f589
Apply suggestion from @claude[bot]
DTCurrie Jun 3, 2026
7d48588
claude is bad at suggesting code edits
DTCurrie Jun 3, 2026
5ccbd2b
remove outdated mock
DTCurrie Jun 3, 2026
16b6268
Merge branch 'app-16292/2-details-refactor' into app-16292/3-componen…
DTCurrie Jun 3, 2026
84b0156
test: remove duplicate 'keeps original array intact' test case
github-actions[bot] Jun 3, 2026
b85557c
Merge branch 'app-16292/2-details-refactor' of github.com:viam-labs/m…
DTCurrie Jun 3, 2026
e715f79
Merge branch 'app-16292/3-component-tweaks' of github.com:viam-labs/m…
DTCurrie Jun 3, 2026
fec6d11
Merge branch 'main' of github.com:viam-labs/motion-tools into app-162…
DTCurrie Jun 4, 2026
6bc1fd6
cleanup
DTCurrie Jun 4, 2026
43ab2e9
cleanup and clarify
DTCurrie Jun 4, 2026
f376b4d
cleanup
DTCurrie Jun 4, 2026
36463bf
Merge branch 'main' into app-16292/3-component-tweaks
DTCurrie Jun 8, 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/component-tweaks-for-gizmo-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@viamrobotics/motion-tools': patch
---

Default `Line` and `Mesh` opacity to `1`, fix `LineDots` instance ID tracking, enable BVH raycasting in `'gizmo'` interaction mode, fix stale matrix reads in `SelectedTransformControls`, and add `isOpen` / `close` to `Popover` snippets plus a `shapes` icon and `disableTooltip` prop on dashboard `Button`
11 changes: 8 additions & 3 deletions src/lib/components/Entities/LineDots.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,15 @@

$effect(() => {
if (!positions) return
// Track the IDs `addInstance` returns rather than assuming they're a
// sequential 0..N-1 range — when positions changes (e.g. a line gizmo
// being placed), cleanup-by-index would target slots that were never
// allocated for this effect run and throw "Invalid instanceId".
const instances: number[] = []
for (let i = 0, l = positions.length; i < l; i += 3) {
const dotIndex = i / 3
const instance = mesh.addInstance(geometryID)
instances.push(instance)
matrix.makeTranslation(positions[i + 0], positions[i + 1], positions[i + 2])
matrix.scale(vec3.setScalar(scale))
mesh.setMatrixAt(instance, matrix)
Expand All @@ -55,9 +61,8 @@
}

return () => {
if (!positions) return
for (let i = 0, l = positions.length / 3; i < l; i += 1) {
mesh.deleteInstance(i)
for (const instance of instances) {
mesh.deleteInstance(instance)
}
}
})
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/Scene.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
const bvhEnabled = $derived(
settings.current.renderSubEntityHoverDetail ||
settings.current.interactionMode === 'measure' ||
settings.current.interactionMode === 'select'
settings.current.interactionMode === 'select' ||
settings.current.interactionMode === 'gizmo'
)

bvh(raycaster, () => ({ helper: false, enabled: bvhEnabled }))
Expand Down
112 changes: 65 additions & 47 deletions src/lib/components/SelectedTransformControls.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { useThrelte } from '@threlte/core'
import { TransformControls } from '@threlte/extras'
import { Matrix4, Quaternion, Vector3 } from 'three'
import { Matrix4 } from 'three'

import type { FrameEditSession } from '$lib/editing/FrameEditSession'

Expand All @@ -11,14 +11,7 @@
import { useFrameEditSession } from '$lib/hooks/useFrameEditSession.svelte'
import { usePartConfig } from '$lib/hooks/usePartConfig.svelte'
import { useSettings } from '$lib/hooks/useSettings.svelte'
import {
createPose,
matrixToPose,
poseToMatrix,
quaternionToPose,
solveEditedMatrix,
vector3ToPose,
} from '$lib/transform'
import { createPose, matrixToPose, poseToMatrix, solveEditedMatrix } from '$lib/transform'

const { scene } = useThrelte()
const settings = useSettings()
Expand Down Expand Up @@ -64,9 +57,10 @@
})
const isSphereScale = $derived(activeMode === 'scale' && sphere.current !== undefined)
const isCapsuleScale = $derived(activeMode === 'scale' && capsule.current !== undefined)
const transforming = $derived(
ref && entity && activeMode && !isFragmentComponentWithVariables && !invisible.current
)

const quaternion = new Quaternion()
const vector3 = new Vector3()
const refPose = createPose()
const tempRefMatrix = new Matrix4()
const tempEditedMatrix = new Matrix4()
Expand Down Expand Up @@ -119,28 +113,14 @@
}

const onChange = () => {
if (!ref || !entity || !activeMode) {
return
}
if (!ref || !entity || !activeMode) return

const isFrameEntity = entity.has(traits.FramesAPI)

if (activeMode === 'translate' || activeMode === 'rotate') {
if (isFrameEntity) {
stageFrameTransform()
} else {
const matrix = entity.get(traits.Matrix)
if (matrix) {
matrixToPose(matrix, tempPose)
if (activeMode === 'translate') {
vector3ToPose(ref.getWorldPosition(vector3), tempPose)
} else {
quaternionToPose(ref.getWorldQuaternion(quaternion), tempPose)
ref.quaternion.copy(quaternion)
}
poseToMatrix(tempPose, matrix)
entity.changed(traits.Matrix)
}
stageLocalTransform()
}
} else {
// scale → bake the gizmo's scale factor into the geometry trait,
Expand Down Expand Up @@ -194,40 +174,46 @@
}

/**
* Frame.svelte renders frame entities by writing the entity's WorldMatrix
* into group.matrix and decomposing it into position/quaternion. The gizmo's
* Three.js parent has identity world, so `ref.position` / `ref.quaternion`
* are world-space values. Matrix and EditedMatrix store local-to-parent
* transforms, so we left-multiply by the parent's inverted WorldMatrix
* before staging — otherwise WorldMatrix recomposition (parent × edited)
* re-applies the parent's rotation/translation and the frame ends up at
* parent × where-the-user-pulled-it.
* Build the entity's parent-relative drag target from the gizmo's world-space
* `ref` transform into `out`.
*
* With a kinematic offset (LiveMatrix + Matrix both present), the local
* target M(local) feeds solveEditedMatrix to back out the EditedMatrix
* that satisfies live × baseline⁻¹ × edited = local.
* Entity renderers mount at the scene root with `matrixAutoUpdate = false`
* and recompose `group.matrix` from the `WorldMatrix` trait, so
* `ref.position` / `ref.quaternion` are world-space. Matrix-shaped traits
* store local-to-parent, so we left-multiply by the parent's inverted
* WorldMatrix. Otherwise recomposition (parentWorld × local) re-applies the
* parent transform and the entity lands at parentWorld × where-it-was-dragged.
*/
const stageFrameTransform = () => {
const computeLocalDragTarget = (out: Matrix4) => {
if (!ref || !entity) return

tempRefMatrix.makeRotationFromQuaternion(ref.quaternion)
tempRefMatrix.setPosition(ref.position)
out.makeRotationFromQuaternion(ref.quaternion)
out.setPosition(ref.position)

const parentEntity = entity.targetFor(relations.ChildOf)
const parentWorld = parentEntity?.get(traits.WorldMatrix)
const parentWorld = entity.targetFor(relations.ChildOf)?.get(traits.WorldMatrix)
if (parentWorld) {
tempParentInverse.copy(parentWorld).invert()
tempRefMatrix.premultiply(tempParentInverse)
out.premultiply(tempParentInverse)
}
}

/**
* Stages a translate/rotate drag for a frame system entity into the edit
* session. With a kinematic offset (LiveMatrix + Matrix both present), the
* parent-relative target feeds solveEditedMatrix to back out the EditedMatrix
* satisfying live × baseline⁻¹ × edited = local. Without one, Frame.svelte's
* blend short-circuits to EditedMatrix, so we stage the target pose directly.
*/
const stageFrameTransform = () => {
if (!ref || !entity) return

computeLocalDragTarget(tempRefMatrix)
matrixToPose(tempRefMatrix, refPose)

const live = liveMatrix.current
const config = configMatrix.current

if (!live || !config) {
// No live matrix available — Frame.svelte's blend short-circuits to
// editedMatrix, so the parent-relative target is what we stage.
if (activeMode === 'translate') {
session?.stagePose(entity, {
x: refPose.x,
Expand All @@ -249,9 +235,41 @@
matrixToPose(tempEditedMatrix, tempPose)
session?.stagePose(entity, { ...tempPose })
}

/**
* Stages a translate/rotate drag for a non-frame-system entity (e.g. a gizmo)
* by writing the dragged component into the Matrix trait. Gizmos carry no
* LiveMatrix, so there's no live-pose blend to invert — the parent-relative
* target is the new local transform.
*/
const stageLocalTransform = () => {
if (!ref || !entity) return

const matrix = entity.get(traits.Matrix)
if (!matrix) return

computeLocalDragTarget(tempRefMatrix)

// update only the dragged component
matrixToPose(matrix, tempPose)
matrixToPose(tempRefMatrix, refPose)
if (activeMode === 'translate') {
tempPose.x = refPose.x
tempPose.y = refPose.y
tempPose.z = refPose.z
} else {
tempPose.oX = refPose.oX
tempPose.oY = refPose.oY
tempPose.oZ = refPose.oZ
tempPose.theta = refPose.theta
}

poseToMatrix(tempPose, matrix)
entity.changed(traits.Matrix)
}
</script>

{#if ref && entity && activeMode && !isFragmentComponentWithVariables && !invisible.current}
{#if transforming}
{#key entity}
<TransformControls
object={ref}
Expand Down
10 changes: 6 additions & 4 deletions src/lib/components/overlay/Popover.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,26 @@
import { normalizeProps, portal, useMachine } from '@zag-js/svelte'

interface Props {
trigger: Snippet<[HTMLButtonAttributes]>
children: Snippet
trigger: Snippet<[HTMLButtonAttributes, { isOpen: boolean }]>
children: Snippet<[{ close: () => void }]>
Comment thread
DTCurrie marked this conversation as resolved.
}

let { children, trigger }: Props = $props()

const id = $props.id()
const service = useMachine(popover.machine, { id })
const api = $derived(popover.connect(service, normalizeProps))

const close = () => api.setOpen(false)
</script>

{@render trigger(api.getTriggerProps())}
{@render trigger(api.getTriggerProps(), { isOpen: api.open })}

<div
use:portal={{ disabled: !api.portalled }}
{...api.getPositionerProps()}
>
<div {...api.getContentProps()}>
{@render children()}
{@render children({ close })}
</div>
</div>
9 changes: 7 additions & 2 deletions src/lib/components/overlay/dashboard/Button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
import type { ClassValue, HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements'

import { Icon, type IconName, Tooltip } from '@viamrobotics/prime-core'
import { Focus, MousePointer2, Ruler } from 'lucide-svelte'
import { Focus, MousePointer2, Ruler, Shapes } from 'lucide-svelte'

interface Props extends HTMLButtonAttributes {
icon: IconName | 'ruler' | 'mouse-pointer' | 'focus'
icon: IconName | 'ruler' | 'mouse-pointer' | 'shapes' | 'focus'
active?: boolean
description: string
hotkey?: string
class?: ClassValue | null | undefined
tooltipLocation?: 'bottom' | 'right' | 'left' | 'top'
disableTooltip?: boolean
onclick?: MouseEventHandler<HTMLButtonElement> | null | undefined
}

Expand All @@ -21,6 +22,7 @@
hotkey = '',
class: className = '',
tooltipLocation,
disableTooltip = false,
onclick,
...rest
}: Props = $props()
Expand All @@ -29,6 +31,7 @@
<Tooltip
let:tooltipID
location={tooltipLocation ?? 'bottom'}
state={disableTooltip ? 'invisible' : undefined}
Comment thread
DTCurrie marked this conversation as resolved.
>
<label
class={[
Expand All @@ -50,6 +53,8 @@
<Ruler size="16" />
{:else if icon === 'mouse-pointer'}
<MousePointer2 size="16" />
{:else if icon === 'shapes'}
<Shapes size="16" />
{:else if icon === 'focus'}
<Focus size="16" />
{:else}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,34 @@ describe('<Button> (dashboard)', () => {

expect(screen.getByText('1')).toBeInTheDocument()
})

it('preserves aria and role when disableTooltip is true and active', () => {
render(Button, {
props: {
icon: 'cursor-move',
description: 'Translate',
active: true,
disableTooltip: true,
},
})

const radio = screen.getByRole('radio', { name: 'Translate' })
expect(radio).toBeInTheDocument()
expect(radio).toHaveAttribute('aria-checked', 'true')
})

it('preserves aria and role when disableTooltip is true and inactive', () => {
render(Button, {
props: {
icon: 'cursor-move',
description: 'Translate',
active: false,
disableTooltip: true,
},
})

const radio = screen.getByRole('radio', { name: 'Translate' })
expect(radio).toBeInTheDocument()
expect(radio).toHaveAttribute('aria-checked', 'false')
})
})
Comment thread
DTCurrie marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { render, screen } from '@testing-library/svelte'
import { createWorld } from 'koota'
import { Matrix4 } from 'three'
import '@testing-library/jest-dom/vitest'
import { describe, expect, it } from 'vitest'
import '@testing-library/jest-dom/vitest'

import { traits } from '$lib/ecs'
import { WORLD_CONTEXT_KEY } from '$lib/ecs/useWorld'
Expand Down
Loading