fix: recompute default position when dimension order changes - #1084
fix: recompute default position when dimension order changes#1084AKnassa wants to merge 6 commits into
Conversation
`Position` reused its existing coordinates whenever the previous coordinate space was invalid and the rank matched. Those coordinates may have been inferred from the bounds of a different coordinate space, so re-applying a state that lists the dimensions in a different order carried the old inferred midpoint over unchanged, leaving the position outside the volume bounds. Track whether the current coordinates were inferred from the bounds rather than specified explicitly, and only reuse them in the latter case. Positions restored from JSON, set explicitly, or copied via `assign` are unaffected. Fixes google#885
Extend the regression suite around the default-position inference so the fix in the previous commit is pinned from more than one angle. Three of these fail without the fix: re-inference after several consecutive invalid coordinate spaces, after `snapToVoxel` has rounded an inferred position, and across `assign` (which must carry inferred-ness with the coordinates it copies). The rest lock behaviour that must not change: an explicitly restored out-of-bounds position stays put, an inferred position does not jump when the same space returns, dimensions are still matched by id when the previous space stays valid, unbounded dimensions still infer a finite coordinate, and voxel-centre rounding is respected per dimension. Fixes google#885
The previous commit tracked whether the coordinates had been inferred with a boolean that was cleared by `value`'s setter, `restoreState` and `reset`. Several callers do not use any of those: they move the position by writing into the `Float32Array` returned by the `value` getter and then dispatching `changed` directly. `NavigationState.translateVoxelsRelative` and the slice-view zoom-at-mouse handler both do this, so panning or zooming left the flag set, and the pan was discarded and replaced with the centre of the bounds the next time the coordinate space was replaced while invalid. Keep a copy of the inferred coordinates instead of a flag, and treat the position as inferred only while the current coordinates still equal that copy. Any move detects itself, whichever path made it, and a position moved back onto the inferred coordinates is again indistinguishable from one that was never touched. The remapping path carries the copy through the same permutation, since remapping is not a move. Regression tests cover both directions: panning in place is preserved, and untouched inferred coordinates are still recomputed. Fixes google#885
Adds three tests found missing while reviewing the fix: - the sequence reported in the issue, where resetting the viewer state leaves an inferred position behind for the outgoing coordinate space - an inferred position stays inferred across a dimension-id remap, so it is still re-inferred if the space is later replaced while invalid - an explicit position stays explicit across the same remap Also makes the two `assign` tests observe the target's own invalid coordinate space before the new one arrives. The second was previously a no-op: `assign` copies the source's valid space, so re-assigning the invalid space the target already held dispatched nothing, and the test crossed no transition at all.
| // several callers, such as `NavigationState.translateVoxelsRelative`, move | ||
| // the position by writing into the array returned by `value` instead of going | ||
| // through the setter. Comparing against the copy detects those moves as | ||
| // well. |
There was a problem hiding this comment.
it might be best to update those usages because that still opens up the case for one of those callers to navigate away and then back to the same value as inferredCoordinates_ and have it treated as coordinatesAreInferred.
Whereas in your setter, you purposely clear out inferredCoordinates_. However maybe that usage isn't relevant.
There was a problem hiding this comment.
good catch, this turned out to be relevant. stepping rounds to the voxel centre, so an arrow key away and back lands exactly on inferredCoordinates_ and got treated as inferred again. I updated those usages: the in-place movers now report via markMoved (only when a coordinate actually changed, so no-op writes still re-infer), and the two channel widgets just go through the setter now. kept the comparison as a fallback for any caller that doesn't report. added tests driving each caller, including the away-and-back cases.
Callers that move the position by writing into the array returned by `Position.value` now report the move via `markMoved`, so navigating away and back onto exactly the inferred coordinates no longer counts as inferred. Only writes that actually change a coordinate are reported, so no-op interactions still allow re-inference. The two channel position widgets assign through the setter instead of writing in place, and syncing an offset-linked position no longer allocates per update.
Drives every caller that bypasses the position setter: keyboard steps (including ones the bounds clamp), panning, rotation about a fixed point, updateDisplayPosition, playback, drag-and-drop through the real drop handler, and layer global/local positions via `setLayerPosition`. Also checks repeated away-and-back cycles, no-op writes that must not count as moves, offset-linked syncs, and a rank-8 space stepped ten thousand times.
What this does
When you load a saved view that has no position saved in it, Neuroglancer picks a sensible starting point for you — the middle of the data. This makes sure it works that out fresh for the view you're actually loading, instead of reusing the one it picked for the previous view.
Why
If the new view lists its dimensions in a different order than the old one (say
z, x, yinstead ofx, y, z), the old starting point gets applied to the wrong axes. You end up far outside the data, looking at an empty screen. That's what #885 reports — it shows up the second time you press apply in the JSON state editor, because the first apply leaves a position behind.What changed
How to see it
Run
npm run dev-server, open the viewer, and in the browser console apply the state from #885 twice:Checked against the dataset in the issue (
fafb_v14, boundsz: 7063,x: 248832,y: 134144):[124416.5, 67072.5, 3531.5]—zis124416.5against azlimit of7063. Outside the data, blank panels.[3531.5, 124416.5, 67072.5]— inside the data.Note for reviewers
This tracks a copy of the inferred coordinates rather than a boolean flag, because several callers move the position by writing into the array returned by
position.valueand dispatchingchangeddirectly, never going through the setter —NavigationState.translateVoxelsRelativeand the slice-view zoom-at-mouse handler insrc/sliceview/panel.ts. A flag would go stale on pan or zoom and silently discard the move.There is a plausible alternative I'd be glad to switch to if you prefer it: remember the last valid coordinate space and remap by dimension id across the invalid gap. That would carry a panned position through a reorder rather than re-centring it.
Fixes #885