15-H: scene notes v2/v3 - model, screen-space markers, saved framing + follow sessions - #87
Merged
Merged
Conversation
…t popover, drawer groups w/ traversal + pins toggle
- H1 model: annotations gain optional name/color/label on top of `text` (still
the description). ONE normalizeAnnotation() at every store boundary (local
set, remote apply, snapshot, autosave restore) so old autosaves, .tpscene
sessions and old-version peers load with defaults. Wire shape UNCHANGED
({type:'annotation', op:'set'}); saves spread the base so a newer peer's
fields survive our edit. displayName()/noteNumber() helpers - numbering is the
GLOBAL 1-based index, matching the in-scene pin labels.
- H2 openAnnotation(id, 'view'|'edit'); activeAnnotation is {id, mode} (a bare
{id} still means view). Pin clicks open the view face.
- H3 showNotePins LOCAL pref (lightHelpers idiom) gates BOTH the pins render
and Scene's pin-click raycast branch; toggle lives in the drawer header.
- H4 pin circle + cone take the note color (amber stays the default).
- H5 popover v2: full runes rewrite, one card with a view and an edit face,
anchored by a continuous rAF screen projection of the pin (it rides the
openAnnotation fly and later orbiting - flyTo has no completion hook), flips
sides + clamps to the viewport, centre-fallback when the pin is behind the
camera, bottom sheet at <=640px (closes the other sheets). Edit fields: name,
description, fixed bright swatch row, label w/ a datalist of existing labels.
- H6 drawer v2: rows are "#n name - description" (grey, single-line truncated),
row click = fly + view, per-row Edit = fly + edit, collapsible label groups
with 'General' first then alphabetical, per-group arrows traversing in pin
order with wrap; runes flip.
- H7 new suite notes-v2 (29 checks): legacy normalization, anchored popover +
re-anchor on pin move, draft at the clicked point, edit saves + replicates the
v2 fields (send spy) and applies from a peer, pin colors, group order,
numbering vs pin indices, arrow traversal + wrap, collapse, pins toggle. A
two-peer handshake check is env-gated (TWO_PEER=1). annotation-anchor,
autosave-object-flows and roadmap-13-notifications-notes stay green;
build green; svelte-check holds 419/62.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ty, persistence fixes
User feedback on the shipped v2 (H8-H10, H12 of the plan; H11 saved-camera/follow
stays for the user to implement).
- H8 pin render pass rework. Every pin now draws TWICE: a depth-tested SOLID pass
and a depth-test-off GHOST pass at 0.3 opacity, with explicit renderOrders
(ghost UNDER solid, so a visible pin reads as its own saturated colour instead
of the ghost border bleeding through the fill). Occluded = dim silhouette, not
gone and not punching through at full strength. The old single pass was
depthTest:false with NO renderOrder, so "does this pin draw over that object"
depended on scene ADD order - that is why your own new pins floated on top
while the ones received from a peer (objects added after the pins group) were
covered. The number is one pass, never depth-tested, above every pin mesh, so
an overlapping neighbour can no longer hide it.
Two traps worth remembering: the solid pass must WRITE depth (the outline/N8AO
postprocessing passes read the depth buffer, so a pin that writes none gets the
AO and selection edges of whatever is behind it painted across its face), and
the visuals sit 0.06 along the group's local +Z (= toward the viewer after
lookAt) so a pin anchored flat ON a surface no longer half-sinks into it - the
pin GROUP itself stays exactly on the anchor, which annotationWorldPosition and
the anchor suite read.
- H9 pin shapes + a professional look: replicated `shape` (round/star/square,
shared geometry per shape), a border ring in a darker shade of the note colour,
slight fill transparency, and a contrast-aware number (YIQ split, so amber and
green get dark ink, blue and violet get light). Shape selector sits next to the
colour swatches in the edit face. Colour math runs on sRGB bytes, never through
THREE.Color (the linear round-trip darkens hexes).
- H10 author identity. `author` stays the DISPLAY name at save time (never the
literal 'me' any more), and a new `authorKey` - a stable per-device key - says
whether a note is ours. Peer ids are re-issued on every reconnect and nicknames
change, so neither could answer that across a rename or a reload. 'Me' is now
purely a display mapping: a saved .tpscene/autosave shows the owner's nickname
to everyone else, and renaming yourself re-stamps your own notes on the next
save. Old notes with no key fall back to the name/peer-id comparison.
- H12 persistence ("some notes disappear on reload"). Three causes fixed:
annotations were snapshot passengers but never marked the autosave DIRTY (only
objectsGroup/flowGraphs did), so a note added after the last object change was
never written; a note anchored on a SCENE-ROOT object (env rig, module content)
orphaned every boot because those are rebuilt with fresh uuids - notes now
remember the anchor NAME and re-key in the sweep; and the orphan prune fired
instantly, so a restore/regeneration race could eat notes - it now needs the
object to stay missing for 3s, re-arming its own follow-up sweep.
- notes-v2 extended to 51 checks: two-pass structure + renderOrder + always-on-top
number, shape geometry/border/ink and shape replication, the whole author story
(stored vs displayed, rename upgrade, foreign-device view), autosave dirty +
snapshot content, scene-root re-key, prune grace. annotation-anchor and
autosave-object-flows stay green; build green; svelte-check holds 419/62.
roadmap-13-notifications-notes' unread-badge check fails identically on HEAD
(known flake, proven by stashing).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The grid "snapped" while panning in look-at follow mode because the centre was quantised to ONE CELL. A cell-step translation maps the thin lines onto themselves, but the SECTION lines repeat every `sectionEvery` cells - so every thick line hopped one cell on each step while the fine grid stood still. Snapping by the section period (cell x sectionEvery) makes the whole pattern map onto itself: every line lands exactly where a line already was, so following is invisible and the lines stay locked to world coordinates (unsnapped following slides the grid under the objects instead, which is why the snap exists). The fade circle is deliberately left alone: threlte's Grid already defaults its fadeOrigin to the camera position projected onto the grid plane, which glides continuously. Feeding it this snapped anchor is what would re-introduce a jumping fade ring - the exact regression I4 fixed by dropping the eased fade. So the grid never disappears during navigation; nothing pops. grid-snapping gains 3 checks: the centre lands on the section lattice, it still tracks the look-at target, and the fade uniform stays on the unsnapped camera point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cclusion, clustering The in-scene quads could be CUT IN HALF by any surface they touched (a billboard rotating into a face) and their occlusion was a per-pixel depth test, so a marker was never reliably "in front" or "behind". Markers are now screen-space DOM badges with a leader line to the exact 3D point (new components/menu/AnnotationMarkers.svelte); the in-scene meshes stay as the VR path only, since DOM is invisible in a headset. The pin GROUP still tracks its anchor in every mode - annotationWorldPosition, the popover and the anchor suite all read it. The triangle/cone under the pin is gone from both paths. - Occlusion is ONE verdict per marker from a raycast (throttled to ~8Hz) with 8cm of slack, so a note resting ON a surface stays solid while one genuinely behind geometry fades its FILL to 50% and dashes its leader. The number keeps full opacity - it is the marker's identity. Nothing can clip a DOM badge, which was the actual request. - Badge style: pill (rounded-rect for the square shape), 26px tall, 13.5px bold tabular number, small type icon, 1.5px light outline + layered drop shadow, contrast-aware ink. The leader is a 1.25px core over a soft 3px glow ending in a dot with a hairline ring exactly on the 3D point. - Clustering: greedy screen-space grouping at 34px (where badges would actually overlap) collapses into one counted badge with a stacked-plates shadow; a click fans the members out in an arc, each keeping its own leader to its own point. - Polish: constant screen size, hover scale + brighter outline + a tooltip with name/preview/author/date, a selected ring on the open note, fade under 0.9m and hidden under 0.35m, off-screen culling that skips the raycast too, and an adaptive outline for light themes / prefers-color-scheme. - GOTCHA worth keeping: an <svg> is a REPLACED element, so `position:fixed; inset:0` still leaves it at its 300x150 intrinsic box and silently clips every line away - it needs explicit width/height 100%. Also, app chrome (Connect bar, z 300) sits above the marker layer (z 28), so a badge under it is not clickable; the suite clicks in page context rather than fighting Playwright's actionability check. notes-v2 is now 63 checks, rewritten around the DOM markers: fill/ink/pill/icon, leader endpoints matching the projected point, occluded fade + dashed leader, the on-surface slack, cluster collapse/expand, hover preview, selected ring, and the pins toggle hiding both paths. annotation-anchor green, build green, svelte-check holds 419/62. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…o jiggle), pill badge everywhere Two follow-ups from the review. - JIGGLE ROOT CAUSE: the marker layer owned a private requestAnimationFrame loop, but threlte's OrbitControls calls controls.update() in a task in the MAIN stage. Those are two separate callback queues: whenever the marker loop ran before threlte's tick it projected LAST frame's camera, so every badge trailed the geometry by one frame while the camera moved. That also explains why entering and leaving VR "fixed" it - an XR session swaps the loop to renderer.setAnimationLoop and threlte re-registers its own rAF afterwards, flipping the callback order so the marker loop happened to read the updated camera. Fix: the projection + occlusion now run in a useTask in the RENDER stage (after the main stage, so the camera is final for the frame) inside AnnotationPins.svelte, which is already in the Canvas, and publish to a new `noteMarkers` store. AnnotationMarkers.svelte became presentation-only: clustering, hover and the badge chrome derive from that store, no loop of its own. Positions therefore always come from the very camera pose that rendered the frame the badge sits on. - Pin SHAPE is now a VR-only distinction: every 2D badge is the same pill, so the overlay reads consistently (the rounded-rect variant and the per-shape icon are gone). The editor's selector stays - it configures the in-scene VR pin - and is labelled "Pin shape - VR only" so it does not look inert. notes-v2 gains a real regression guard for the lag: it spins the camera and compares each badge against the pin projected with the camera at that instant, asserting the residual stays far below the per-frame travel. Verified that the check DISCRIMINATES by temporarily re-introducing a one-frame lag: residual went 0.0px -> 3.5px against a 3.9px/frame step, and the check failed. Plus a check that a 'star' note renders the same pill in 2D. 65 checks green, annotation-anchor green, build green, svelte-check holds 419/62. roadmap-13-notifications-notes' unread-badge check still fails identically on HEAD (known flake). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the last piece of 15-H. The originally locked fork (follow only while the
popover is open) was dropped on review: nobody keeps a card open to keep
tracking, and closing it to see the scene would kill the thing they wanted.
- FOLLOW IS A SESSION, not a popup side effect. Started explicitly (Follow on the
note's view card) and it OUTLIVES the card: a sticky info toast "Following note
#n - the camera rides it as it moves (Esc to stop)" is both the indicator and
the off-switch (reuses showInfoToast/dismissToastById from 15-L). Local and
per-viewer - never replicated, because the editor camera belongs to whoever is
driving it.
- MOTION: each frame we translate camera.position AND orbitControls.target by the
pin's world delta. Moving both ends preserves the user's own orbit/zoom offset,
so they keep navigating freely while the camera rides the object. The tick runs
in a MAIN-stage useTask - before OrbitControls' own task and the render - so the
pose we write is the pose the frame is composed from, and the render-stage
marker projection already sees it.
- SAVED POSE = AUTHORED FRAMING. "Save camera view" stores {position, target} on
the note (replicated on the unchanged annotation message). Opening flies there
instead of the generic 4m approach; a follow session then keeps that offset
live as the object moves, so the author's framing stays true. Plus a per-note
"Follow the pin when opened" hint that auto-starts the viewer's local session.
- HANDOVER is an EXPLICIT signal, not a deviation guess. New `cameraClaim`
counter in sceneStore (a leaf both modules already import) is bumped by
objectActions.flyTo, so focus/bookmarks/opening another note end the session
cleanly - while the user's own PAN, which moves the orbit target exactly like
our follow step does, no longer looks like someone stealing the camera. First
attempt used a target-deviation epsilon; it would have broken every pan.
Sessions also end on Esc, on VR/spectate/play taking the camera, and when the
followed note or its object goes away.
- Nodes route deliberately NOT taken here: a follow/look-at node belongs to the
roadmap-16 camera OBJECTS (replicable, watchable via PiP) and is in backlog. A
peer's graph must never yank your viewpoint.
notes-v2 +16 checks: pose stored and re-flown (comparing against the ACTUAL
parked pose - OrbitControls.update() re-derives position from its spherical
state, so never assert the numbers you asked for), camera+target translate with
the pin, the chosen offset survives, panning keeps the session, closing the card
keeps it, an external flyTo hands over, Esc stops, the flag replicates and
auto-starts, and old payloads normalize to no pose / follow off. 79 checks green;
annotation-anchor, camera-bookmarks, autosave-object-flows green; build green;
svelte-check holds 419/62.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…c order
- The "Follow the pin when opened" control is a SWITCH now: it arms a mode, which
is why Settings uses Toggle for behaviour flags. GOTCHA worth remembering:
flowbite's plugin emits `[type='checkbox']:checked { background-color:
currentColor !important }`, so no background-color of ours can ever win the ON
state - it rendered flowbite blue no matter the specificity. The cure is to work
WITH that rule and drive the fill through `color`. The suite now asserts the ON
colour so it cannot drift back to blue.
- NEW SETTING "Double-click to open notes" (Settings > Interface, LOCAL, off by
default): a single click on a note marker - and the drawer's group arrows -
then only FLIES the camera to the note, and the card opens on a double click.
Reviewing a scene full of notes becomes pure navigation with nothing in the way.
`focusAnnotation(id)` is the go-there-without-opening path (openAnnotation and
it now share one `flyToAnnotation`, so a saved framing and the follow hint apply
either way).
- With no card open there was nothing to say where you are, so a new
`visitedNote` store records the last note travelled to: it highlights the drawer
row and is what the arrows step from, so every press advances one note and wraps
instead of restarting at the top of the group.
- ESC ORDER, as asked: the first Esc gives the CAMERA back (stops a follow
session), the second closes the card. One window-level handler in
annotationsHandler owns it, replacing the card's own keydown - that only fired
when focus happened to sit inside the card, so Esc did nothing after a marker
click - and the follow-only listener that would have raced it. A modal owns Esc
while it is up.
notes-v2 +11 checks (switch shape + ON colour + stored flag, Esc precedence in
both orders and from outside the card, single-click-flies / double-click-opens
with the setting on and off, arrows travelling without a popup while still
advancing + wrapping, and the row highlight). 90 checks green; annotation-anchor,
autosave-object-flows, camera-bookmarks and all 5 settings suites green; build
green; svelte-check holds 419/62.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e trap, camera ownership
- Architecture map: annotationsHandler's full v2/v3 model (text stays the
description, ONE normalizeAnnotation at every boundary, unchanged wire shape,
authorKey identity, noteMarkers/followingNote/visitedNote/focusAnnotation,
sweepAnnotations' re-key + grace) and the notes UI trio (AnnotationMarkers as the
screen-space layer, AnnotationPins demoted to the VR path, popover + drawer).
- New gotchas, each of which cost real debugging here: a DOM overlay that must agree
with a threlte frame may NOT own a requestAnimationFrame (OrbitControls updates
the camera in a main-stage task; project in useTask({stage: renderStage}) instead
— and note that entering/leaving VR "fixing" a jiggle is the signature of exactly
this) - OrbitControls.update() re-derives the camera position from its spherical
state, so direct writes revert and continuous drivers must move camera AND target,
with cameraClaim as the explicit handover signal because deviation-watching cannot
tell a user pan from a takeover - an <svg> is a replaced element, so fixed+inset:0
still clips it to 300x150 - flowbite forces background-color: currentColor
!important on checked checkboxes, so a custom switch must be coloured through
`color` - depthWrite:false loses the outline/N8AO passes, which then paint the
geometry behind a sprite across its face - pattern FOLLOW must snap by the section
period, not one cell.
- Status entry for the 5-commit 15-H branch.
- e2e-verify: how to read the DOM markers (and why not pinsGroup), chrome above the
marker layer, clustering hiding individual badges, the frame-lag guard that was
proven to fail before being trusted, plus the camera-assertion and
synthetic-click-vs-binding rules.
- peer-feature: how to GROW an existing replicated record without a new message
type, and where the line sits between replicated authorship and a local view of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/extensions/Grid.svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Roadmap 15-H (scene notes v2) plus the three review drops that followed it. Plan
and as-built notes: cloud repo
plans-core/pending/15-h-notes-v2.md.Branched off
fix/roadmap16-menus-cameras(now merged here via #86 → #85 → #84), sothis PR is the 9 notes commits on top of the current
release/next.What ships
Model (v2) — notes gain
name,color,label,shape,authorKey,camera,follow,objectName;textstays the description. ONEnormalizeAnnotation()runsat every store boundary, so old autosaves,
.tpscenesessions and older peers loadwith defaults. The wire shape is unchanged (
{type:'annotation', op:'set'}stillcarries the whole object) and saves spread the base record, so a field a newer peer
added survives an edit by an older one. No handshake change.
Markers (v3) — the desktop marker is now a screen-space badge with a leader line
to the exact 3D point. The old in-scene quads could be cut in half by any surface they
touched and their occlusion was a per-pixel depth test; a DOM badge cannot clip, and
occlusion is one raycast verdict per marker with 8 cm of slack — a note resting on a
surface stays solid, one genuinely behind geometry fades its fill and dashes its
leader while the number stays readable. Overlapping markers cluster into a counted
badge that fans out on click. Hover preview, selected ring, constant screen size,
near-camera fade. The in-scene meshes remain as the VR path (DOM is invisible in a
headset) and their groups stay the anchors in every mode.
Popover + drawer — one card with view/edit faces anchored beside its pin
(continuous projection, so it rides the fly and later orbiting); the drawer gained
#n name — descriptionrows, collapsible label groups with ‹ › traversal in pinorder, and a pins-visibility toggle.
Saved framing + follow (H11) — "Save camera view" stores a pose on the note;
opening flies there instead of the generic approach. Follow is an explicit
per-viewer session that outlives the card (sticky indicator toast, Esc), moving
camera and orbit target by the pin delta so your own orbit offset survives.
Handover is explicit via a new
cameraClaimcounter bumped byflyTo— deviationwatching cannot tell a user pan from a takeover.
Fixes found on the way
marked the autosave dirty. Also scene-root anchors now re-key by object name
(those objects are rebuilt with fresh uuids each boot) and the orphan prune waits
out a 3 s grace instead of firing instantly.
requestAnimationFrame,a different callback queue from threlte's scheduler, so it projected last frame's
camera. Positions now publish from a render-stage task. (Entering/leaving VR
"fixing" it was the tell — XR swaps the loop and flips the callback order.)
the section period now.
'me'is never stored — files and peers see the owner's nickname,and 'Me' is a display mapping over a stable per-device key.
New setting — Settings ▸ Interface ▸ "Double-click to open notes" (local, off by
default): single click on a marker and the drawer's group arrows only fly the camera;
the card opens on a double click. Esc order: first press stops following, second
closes the card.
Verification
notes-v2(new, 90 checks) +annotation-anchor+grid-snapping+autosave-object-flows+camera-bookmarks+ the settings suites green on the mergedtree;
npm run buildgreen;npx svelte-checkholds the 419/62 baseline.roadmap-13-notifications-notesfails one unread-badge check identically on HEAD(known flake, proven by stashing).
The frame-lag guard in
notes-v2was verified to FAIL when a one-frame lag isreintroduced (residual 0.0px → 3.5px against a 3.9px/frame step) — a check that
cannot fail is not a check.
Manual checks left for the user: VR pins on device, and the feel of a follow
session on a flow-animated object.
🤖 Generated with Claude Code