Skip to content

APP-16360a: Fix baseline matrix and EditedMatrix update conditions in useFrames#742

Merged
sucrammal merged 9 commits into
mainfrom
APP-16360a-World-vs-Local-position-update-discrepency
Jun 10, 2026
Merged

APP-16360a: Fix baseline matrix and EditedMatrix update conditions in useFrames#742
sucrammal merged 9 commits into
mainfrom
APP-16360a-World-vs-Local-position-update-discrepency

Conversation

@sucrammal

@sucrammal sucrammal commented Jun 2, 2026

Copy link
Copy Markdown
Member

Bug fixes: edit mode timing (baseline, edited matrix, frame source)

All three issues share the same root cause — isEditMode and didRecentlyEdit are both $effect-written, so they lag one DOM flush behind. Any guard that relied on them for a decision that needed to land before render was one cycle late.


1. Baseline unlocking too early

WorldMatrix is live × baseline⁻¹ × edited. Baseline should stay frozen while there are unsaved edits — but the old guard was if (!isEditMode && !partConfig.hasPendingSave). Since isEditMode is set in a $effect, there's a one-frame window on the very first edit where it's still false. Baseline gets overwritten, formula collapses to live, frame snaps back to robot position.

Fix: swapped to if (!partConfig.isDirty). isDirty is $state and flips synchronously in updateFrame(), so it's always current.


2. EditedMatrix not updating after frame builder confirm

Old guard was if (!isEditMode) — nothing could update EditedMatrix in edit mode at all. The intent was probably to avoid fighting the gizmo mid-drag, but it was also blocking frame builder confirmations and arm-tab edits, which have nothing to do with dragging.

Fix: if (!isEditMode || !editSession.current) — only blocks the overwrite during an active drag. Config changes flow through otherwise.


3. Frame source lagging on first edit

The derived value that blends config frames over live frames was gating on didRecentlyEdit, which is also $effect-driven. First edit would come in, derivation still sees false, returns live frames — edit is applied but invisible for one cycle.

Fix: added partConfig.isDirty to the condition alongside didRecentlyEdit.

Rationale:

These specific bug fixes were heavily authored by Claude Code. Personally, not too familiar with how the matrix math with the baseline, edited matrix, as well as the world vs. local positions interact with each other. These code changes seemed to make #740 work. Specifically, I ran into bugs where when editing frames in the standard edit session vs. the LLM powered frame builder, the world position would often update to something unexpected, although the local position was updated just fine.

Here's an example flow that I personally went through:

  1. Prompt frame builder to move to x = +200
  2. Click confirm on the frame builder. This changes the config, and prompts the user with the LiveUpdatesBanner to save.
  3. Before saving, start a drag edit session and move to y = +200.
  4. Then save. Final position is correct at (200, 200, 0), but only for a bit.
  5. After a few seconds, the world position = (400, 400, 0), and the local position = (200, 200, 0). Not sure why this bug happens.

@sucrammal sucrammal requested a review from micheal-parks June 2, 2026 21:31
@changeset-bot

changeset-bot Bot commented Jun 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 26bee79

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@viamrobotics/motion-tools Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-10 17:51 UTC

@sucrammal sucrammal changed the title Fix baseline matrix and EditedMatrix update conditions in useFrames APP-16360a: Fix baseline matrix and EditedMatrix update conditions in useFrames Jun 2, 2026

@micheal-parks micheal-parks left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find, I am concerned though that this solution is investing too much in working around Svelte reactivity implementation details - today I'm going to see if there's an alternative solution that doesn't do that. If there isn't then I'd say this is good to go

Comment thread src/lib/hooks/useFrames.svelte.ts Outdated
// which is the user's desired position. Keeping baseline stale while
// the user has unsaved edits is what makes the 3D preview work.
//
// We use isDirty (not isEditMode) because isEditMode is driven by

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we reword this comment so that it's descriptive but doesn't contain traces of fixing this particular bug? some details are specific to this PR change and could be noisy for future readers

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done! See commit 8665079

sucrammal added 2 commits June 3, 2026 14:00
No longer contains PR-specific traces of fixing the bug.
@micheal-parks micheal-parks requested a review from mattmacf98 June 3, 2026 18:29
Comment thread src/lib/hooks/useFrames.svelte.ts Outdated
// dependency of this derivation the moment the user makes any edit.
if (
didRecentlyEdit ||
partConfig.isDirty ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this, could we instead put didRecentlyEdit in an $effect.pre? Does that work? Seems like more of an explicit fix

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Key state variables used in useFrames.svelte.ts:

viewerMode is set by two competing sources:

  • SelectedTransformControls sets it to 'edit' synchronously when a drag starts
  • useConfigFrames has a plain $effect that sets it to isDirty ? 'edit' : 'monitor'. Runs after DOM

isEditMode = $derived(viewerMode === 'edit'):

  • This reflects viewerMode, but with the lag of whichever source set it.

partConfig.isDirty is plain $state, flipped synchronously by updateFrame() (typing a value, confirming the frame builder).

  • isDirty means only one thing: the user has written config changes that haven’t been sent to the robot yet.

didRecentlyEdit is a sticky latch:

  • once true, it stays true for the lifetime of a part.
  • It's the persistent signal that makes frames keep using configFrames after the first edit, even after isDirty clears on save.

What was done in the original PR:

I decided to use isDirty to gate one thing in particular: Freeze the baseline matrix during in-flight edits (line ~251):
The baseline (Matrix trait) represents where the robot was before the current round of edits. The formula live × baseline⁻¹ × edited only gives you edited when live ≈ baseline – i.e., when the robot hasn't moved relative to where it was when you started editing. If baseline tracked live continuously, the formula would always cancel and you'd always see edited – but that's actually correct for monitor mode. The formula is only interesting in monitor mode when the robot is doing live kinematics (arm moving) and you want edited to compose on top of the actual arm position.

The moment isDirty goes true, we freeze baseline. It captures "where the robot was right before the user touched anything." Then while the user edits, live × baseline⁻¹ × edited shows the desired preview. When isDirty clears (save or discard), baseline unfreezes and catches up to the robot's actual position before the next edit session begins.

The original gating conditional was just checking for isEditMode. isEditMode is derived from viewerMode, which is set by a plain $effect in useConfigFrames – it runs after DOM, one flush late. On the very first edit from monitor mode, even if we place isEditMode in an $effect.pre instead of a simple $effect on its own, isEditMode would still be false until a later flush.

Persistent issue: World Position doubling.

I thought this fix in the original PR would solve the strange bug where if we updated the local position of a frame, the world position would apply double updates. This video demoing the bug was captured this morning while testing.

double-bug.mov

I just implemented a new gate:
$effect(() => { return world.onChange(traits.LiveMatrix, (entity) => { if (!partConfig.isDirty && !partConfig.hasPendingSave) return

After the user presses save(), isDirty = false and hasPendingSave = true. The robot then moves to apply the new config (Pose.svelte updates LiveMatrix), but the ECS baseline update runs in a separate later flush. In between, live has moved but baseline hasn't. The formula doubles. By gating on either flag being set, we intercept every LiveMatrix change during "the user's edit is in flight" and synchronously keep baseline = live, collapsing the formula to just edited.

Could use some help looking through this file and bug fix one more time.

doubleBugFixed.mov

sucrammal added 2 commits June 8, 2026 13:16
When a save completes, isDirty clears before the robot confirms the new position, leaving a window where Pose.svelte updates LiveMatrix but the ECS baseline hasn't caught up. This causes WorldMatrix = live × baseline⁻¹
× edited to double-apply the delta. Fix: sync baseline to live via
world.onChange(LiveMatrix) whenever isDirty or hasPendingSave is set, so
the formula collapses to just edited during the entire edit+save window.
@mattmacf98

Copy link
Copy Markdown
Member

synced with Marcus, this is an issue we have seen in the past with https://viam.atlassian.net/browse/APP-16024 was probably re-intro'd when we added transform controls in I will re-open that ticket and take a look (I may take over this branch to work off of)

Without this conditional change on line ~256, the visualizer will not preview the confirmed frame changes from the LLM frame builder, and instead, only show once the user saves the config.

Other changaes to useFrames are reverted to be investigated separately.
@sucrammal sucrammal merged commit 797a8a7 into main Jun 10, 2026
8 checks passed
@sucrammal sucrammal deleted the APP-16360a-World-vs-Local-position-update-discrepency branch June 10, 2026 17:51
@claude claude Bot mentioned this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants