APP-16360a: Fix baseline matrix and EditedMatrix update conditions in useFrames#742
Conversation
🦋 Changeset detectedLatest commit: 26bee79 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
micheal-parks
left a comment
There was a problem hiding this comment.
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
| // 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 |
There was a problem hiding this comment.
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
No longer contains PR-specific traces of fixing the bug.
| // dependency of this derivation the moment the user makes any edit. | ||
| if ( | ||
| didRecentlyEdit || | ||
| partConfig.isDirty || |
There was a problem hiding this comment.
Thought about this, could we instead put didRecentlyEdit in an $effect.pre? Does that work? Seems like more of an explicit fix
There was a problem hiding this comment.
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
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.
|
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.
Bug fixes: edit mode timing (baseline, edited matrix, frame source)
All three issues share the same root cause —
isEditModeanddidRecentlyEditare 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 wasif (!isEditMode && !partConfig.hasPendingSave). SinceisEditModeis set in a$effect, there's a one-frame window on the very first edit where it's stillfalse. Baseline gets overwritten, formula collapses tolive, frame snaps back to robot position.Fix: swapped to
if (!partConfig.isDirty).isDirtyis$stateand flips synchronously inupdateFrame(), 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 seesfalse, returns live frames — edit is applied but invisible for one cycle.Fix: added
partConfig.isDirtyto the condition alongsidedidRecentlyEdit.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: