Repository navigation
DS-view pointer arrows for every live pointer + hover polish - #6
Merged
Merged
Conversation
The default border color already matches the background when no accent is applied; the explicit transparent left border caused a subtle color mismatch with the rest of the 1px border.
The column doesn't currently separate globals from the stack; the shorter label is less misleading and matches the heap header's style.
Groups the view-affecting toggles (arrows, LE/BE) together on the right side of the toolbar, separated from the Allocations/Bytes tabs by a flexible spacer.
- "@antfu/eslint-config": "^8.2.0", - "@types/node": "^25.6.0", - "@unocss/eslint-config": "^66.6.8", - "@unocss/eslint-plugin": "^66.6.8", - "@unocss/reset": "^66.6.8", - "@vitejs/plugin-vue": "^6.0.6", - "@vitest/coverage-v8": "^4.1.4", - "eslint": "^10.2.1", - "knip": "^6.4.1", - "typescript": "^6.0.3", - "unocss": "^66.6.8", - "vite": "^8.0.8", - "vitest": "^4.1.4", - "vue-tsc": "^3.2.7" All checks pass: typecheck, lint, knip, 177 unit tests, build (10.30s).
Extends usePointerGraph beyond struct->struct edges: a second pass walks every live pointer scalar (standalone pointer variables, pointer fields whose target is not a struct, nested pointers inside arrays) and emits `extraEdges` / `extraDanglingEdges`. DataStructureView renders them as additional direction='dynamic' arrows that use nearest-border endpoints. Placement: after tree layout and non-pointer standalone placement, a small pass tries to place primitive-pointer cards just left of their target using placeRelative (direction='left', tighter 28px gap). Two passes cover short chains (p1 -> p2 -> target); anything still unresolved falls through to placeNew. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The overlay SVG was positioned inside the view-host's full box, so bezier arches (control point is always above-and-left of the midpoint) could extend above the STACK/HEAP sticky headers and below the column's bottom padding. Both views now mark their scroll containers with `data-overlay-clip-region`; ArrowOverlay unions their bounding rects and applies a `clip-path: inset(top 0 bottom 0)` to the SVG so the vertical extents stay inside the content band. Horizontal overflow is preserved for arrows that cross columns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hovering a pointer card or any pointer arrow in the DS view now mirrors the effect of hovering the pointer's byte in ByteMap: it publishes a PointerArrow into the hover store so MemoryMap renders the source/target highlight + arrow with primary emphasis. Also extends the arrow-hover visual in the DS view so primitive-pointer cards (not just struct field rows) pick up the same blue-500/15 + ring hover treatment as the struct field-row highlight, so the user can see which pointer the arrow stems from. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for viz-list ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
The post-drag bounds change was triggering autoPanToContent, which 'undid' the user's intentional placement. A one-shot suppressAutoPanOnce flag set in onDragEnd and consumed in the next onUpdated keeps the auto-pan path for real bounds changes (new allocations, re-layouts). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zoom (Figma/Miro pattern): - Cmd/Ctrl+scroll zooms around the cursor; plain scroll still pans - Trackpad pinch (wheel+ctrlKey) rides the same path - Toolbar cluster adds -/%/+ plus a separator and the existing Auto-layout button; the % label clicks to reset to 100% Zoom math: contentRef transform is `translate(pan) scale(z)` with origin 0,0. Pan stays in screen pixels so existing pointerdown/move logic stays untouched. getDragDelta + onDragEnd descale by z so dropped items land at the right content-space coord regardless of zoom. clampPan multiplies bounds by z when comparing to screen viewport. Placement revert: primitive-pointer cards no longer try to snap left of their target. The extra placeRelative pass produced chaotic layouts when multiple pointers shared a target. Arrows alone carry enough signal, so pointer items fall through to the standard placeNew empty-space finder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
measureFieldY was mixing screen-pixel offsets (from getBoundingClientRect, which includes the canvas's CSS scale) with content-space placement coords. At zoom 2x the arrow's starting Y was doubled relative to the field row, making every field-anchored arrow droop or rise as the user zoomed. Divide the screen-pixel offset by the current zoom before adding it to pos.y. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first render after a zoom button click reads getBoundingClientRect before Vue has patched the contentRef style with the new scale, so arrows were drawn at the pre-scale anchor positions and stayed stale until pan or another interaction forced a re-render. A post-nextTick tick bump gives the render a second run against the already-scaled DOM. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wrapper now uses `flex items-center`, so when the column is wider than a row's intrinsic width (labelWidth + bpr*cellWidth) the rows sit in the middle with equal gutters on either side. The overlay SVG is a sibling of the wrapper (so it spans the full column height without being virtualized), which means the byte grid coordinates no longer match. OverlayGeometry gains an xOffset field. ByteMap computes it per column as max(0, (width - rowWidth) / 2) and threads it through ByteMapOverlay into allocationRects / allocationPath. Unit test pins the behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-introduces target-aware placement for primitive pointer cards, fixing the prior attempt's cascade issues: - Non-pointer standalone items place first via the usual free-slot search, so by step 3.5 every pointer's target has a committed position. - Pointer items are topo-sorted so chained pointers (p1 -> p2 -> x) land target-first, dependency-before-dependent. DFS-based with cycle fall-through so every pointer still gets placed exactly once. - Each pointer probes four target-adjacent slots (right / left / below / above), sorts by center-to-target distance, and takes the nearest free candidate via a new non-destructive `placement.tryPlaceAt`. - A distance cap of ~2x the larger card dimension keeps crowded graphs from shoving pointers into corners just to hug the target; past that they fall through to the ordinary free-slot placement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`tryPlaceAt` always bumped placement.version, even when committing the same slot a previous pass had already assigned. Combined with DataStructureView's step-3.5 loop, which re-probed every pointer on each onUpdated run, version bumped -> render -> onUpdated -> re-probe -> version bump, freezing heavy templates like btree as soon as they ran. Two idempotency guards: tryPlaceAt no-ops when the key already sits at the requested (x, y); the pointer-placement loop skips items that already have a position (matching placeNew's existing idempotency). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Items placed early during a run-through don't know about structure that appears later, so the final layout is often messy until the user hits Auto-layout. A debounced watcher on memoryVersion now fires a partial re-layout ~400ms after activity stops — typically at breakpoints or between slow steps. Manual drags are preserved via a new `clearPositions(keepUserDragged)` variant on the placement engine, so idle relayouts never clobber positions the user set themselves. The trigger only fires when the total item count changed since the last layout, so resets, pans, and hovers don't kick off a reshuffle. During a fast run (memoryVersion ticks faster than 400ms) the debounce window never closes so playback isn't interrupted; the relayout lands once play pauses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the 400ms debounced idle trigger and the legacy autoPanToContent with a single step-driven path: when content bounds change and anything overflows the viewport, re-run layout preserving user drags. If the re-layout still overflows, stay with the current pan — panning can't shrink content that doesn't fit, and an unexpected top-left realignment disrupts the user's reference frame more than it helps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the dropped auto-pan with an auto-scale step: after a triggered relayout, shrink the canvas zoom enough to fit the entire content bounding box inside the viewport (capped at 1x so the resting layout stays at natural size) and center it. If the content is still larger than the viewport at MIN_ZOOM (0.25x), setZoom clamps there and we accept partial visibility — better than scrolling around blind. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triggered relayouts now reshape the canvas unconditionally — user drags are committed for the immediate render (via setPosition) but aren't tracked as sticky, so the next bounds-change reaction re-places them from scratch alongside everything else. - Removes userDragged Set, markUserDragged / isUserDragged / clearUserDragged from the placement engine. - `clearPositions()` drops its `keepUserDragged` parameter. - `invalidateStaleTreePositions` unconditionally removes everything the tree diff flagged. - Renames the step-triggered variant to `autoRelayoutInPlace`. - Updates PLACEMENT_ENGINE.md with the new lifecycle. `suppressAutoPanOnce` still survives one drag-drop render so the dropped position isn't teleported away in the same tick — feels less jarring than a post-release snap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs in the onUpdated auto-relayout trigger could chase their tails under the right conditions (tree wider than the viewport at zoom<1): 1. The overflow check mixed units — it compared content-space bounds against `-panOffset..-panOffset+viewport` as if zoom were 1, so at zoom 0.5 content that actually fit on screen still registered as overflowing, re-triggering the relayout. 2. prevBoundsKey was snapshotted BEFORE autoRelayoutInPlace, so the in-block relayout moved positions but the key still reflected the pre-relayout bounds. The next render saw "bounds changed" vs the stale key and re-entered the branch for the same structural state. Switched the overflow check to screen-space (pan + p*z) and snapped prevBoundsKey to the post-relayout bounds so subsequent ticks stay quiet until real structure actually changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces 15 focused tests in visualization.spec.ts with a single parameterized spec (templates.spec.ts) that, for each .cpp template, boots the app, runs to the breakpoint at fastest speed, and diffs a structured view-state snapshot against a per-template golden JSON. The snapshot — exposed by index.vue via a dev-only window.__viz hook — captures "what the three views should show": - endianness - pausedAt (line + node text) - allocations (base/size/region/layout) - liveBytes (per-alloc hex dump) - pointerEdges (tree + extra + dangling) These are the semantics that matter; pixel/layout/zoom details stay out so snapshots don't churn on visual tweaks. Goldens live in test/e2e/templates.spec.ts-snapshots/ and regenerate with `bun run test:e2e -- --update-snapshots`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Goldens are pure JSON derived from a deterministic interpreter with no platform-specific output, so Playwright's default darwin/linux split would just force CI to regenerate the same bytes. A custom snapshotPathTemplate strips the suffix; Darwin and Linux CI now share a single checked-in golden per template. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two targeted layout improvements that address messy DS-view placement in deeply-branched trees and waste of arrow-size gutter space: 1. Tidy-tree Y allocation. `placeTreeChildren` now passes each branch's subtree leaf count × rowPitch as the sibling 'height' to placeRelative, instead of the child's physical card height. Deep branches reserve vertical space proportional to their descendant count, so a 3-deep-5-leaf subtree no longer lands in the root's row just because intermediate nodes each centered themselves by card height only. Forward edges only (skip direction='left' back-links) with a visit-guard cycle check. 2. Arrow-gutter pointer candidate. `tryPlacePointerNearTarget` now also probes the empty space between the target's tree parent and the target itself as a fifth candidate slot. Rewards large `@arrow-size` annotations with pointer cards that sit on the visualised arrow path rather than floating at the canvas edge. Snapshots regenerated — payload shape didn't change (same allocations/bytes/edges), so goldens are stable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously a pointer card placed in the first empty slot found early in a run (often the top-left corner before the tree had grown) stayed put forever, even after the target moved somewhere completely different. Now, on every step, an already-placed pointer is re-evaluated against fresh target-adjacent candidates; if the best free candidate is at least RELOCATION_HYSTERESIS (40px) closer to the target than the current slot, the card moves. Hysteresis prevents oscillation when small target nudges would produce nearly-equal candidate distances. Convergence: each render picks at most one better slot, so a single bump-and-settle cycle is enough. Factored the candidate-set generation out as pointerCandidatesAround so initial placement (tryPlacePointerNearTarget) and relocation (maybeRelocatePointerNearTarget) share identical geometry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the instant dark-mode swap with the animation from antfu.me — toggleDark clicks now expand (going to light) or contract (going to dark) a circular clip-path centered on the click, using the browser's startViewTransition when available. Reduced-motion and unsupported browsers fall back to the instant swap. Implementation notes: - Monaco's theme is swapped BEFORE startViewTransition so its canvas repaints in time for the new-state snapshot. Doing it via the existing watch() would fire on Vue's flush inside the transition callback, after the browser has already captured the new snapshot — that was causing a tail-end colour flash on light→dark. - Awaiting rAF inside the transition callback deadlocks (the browser pauses rendering during the callback, so rAF never fires and the whole transition times out with 'Transition was aborted because of timeout in DOM update'). - CSS layers old/new pseudo-elements per theme direction so the correct one sits on top of the other, and disables the browser's default cross-fade so our clip-path animation is the only one that plays. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n gap The flash was visible because Vue's microtask scheduling added a gap between transition.ready resolving and our .animate() attaching — for ~1 frame the pseudo-elements rendered in their static default state (unclipped, both layers fully opaque, z-index deciding top), showing the wrong state briefly before the clip animation started. Adding `fill: 'both'` to the WAAPI options pins the animation's first keyframe as soon as .animate() is called, so even if there's a paint before the animation's time-based progression begins, the pseudo- element already has the correct starting clip applied. Also reverts the earlier Monaco pre-swap: that made Monaco flip instantly (before the circle animation), which was a regression. Monaco's theme now swaps inside the transition callback again via its watcher, which works correctly — the flash was from the unclipped start frame, not from Monaco's canvas timing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n art Moves the help modal out of index.vue into its own component and refreshes the content to match the current visualizer + editor UX: - Each action description now renders the actual button mark inline (icon + styling via shared .icon-btn / .toolbar-group utility classes) so readers see exactly what to click, not a name they have to match back to the toolbar. - Adds coverage for features added since the original write-up: breakpoint() pausing, Allocations/Bytes tabs, the 'arrows' toggle, LE/BE endianness switch, primitive-pointer arrows and their hover cross-highlight, canvas zoom (Cmd/Ctrl+scroll, pinch, zoom toolbar), the auto-compact + auto-scale-to-fit step behavior, and the @arrow-size gutter room for pointer cards. - Drops the theme-toggle blurb (unrelated to visualizer/editor UX). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Struct allocations on the heap (and in stack cards that flowed through MemoryCell) used to render array fields as a single summary row like "keys [3]" — useful to know the shape, useless for seeing actual contents. `getFieldValues` now flattens every array field into one row per element (`keys[0]`, `keys[1]`, `children[0]`, etc.) so the card mirrors the memory layout byte-for-byte. Nested arrays recurse with subscript chaining. Each flattened row carries its own address, which lets statement LHS / RHS tinting propagate down to the specific element being written — a step on `root->keys[1] = 20` now paints the `keys[1]` row blue instead of the whole card. MemoryCell gains the corresponding statementLhs/RhsAddresses props and applies the same blue/green tint rules DSValue already used for the DS view and stack rendering paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pointer-target auto-scroll was yanking the same column the user was hovering in when a pointer and its target lived in the same region — scrolling stack while the cursor was anchored to a stack pointer, or heap-to-heap. That lost the hover context and usually the arrow along with it. Now mirrors the AllocationMap pattern: if hover.source is 'byte-stack' and the target is also on the stack, skip the scroll; same for heap. Cross-region targets (the common case — stack pointer to heap alloc) still auto-scroll as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
usePointerGraphto emitextraEdges/extraDanglingEdgesfor every live pointer — primitive pointer variables, pointer fields whose target isn't itself a struct, nested pointers in arrays. Every pointer in the DS view now gets a rendered arrow.blue-500/15+ ring hover treatment as struct field-row highlights, so arrow-hover visibly picks out the source.data-overlay-clip-region+clip-path: inset(...)— arches no longer extend above the sticky headers or below the bottom margin. Horizontal overflow preserved for cross-column arrows.border-l-transparenton memory cards, renamesStack & Globals→Stack, moves thearrowstoggle afterLE/BEin the toolbar.Test plan
singly-insertBack) — confirm every live pointer has an arrow in DS view.arrows+ switch between Bytes ↔ Allocations — arrows stay within the column content band (no bleed into headers / bottom margin).