FE-1510: Render the Petrinaut canvas centered in a single initial render - #9352
Conversation
The canvas previously painted once at the origin, then a useEffect asked React Flow to measure and fitView, causing a visible jump. Node dimension constants, net bounds and min-zoom math now live in petrinaut-core; the view measures its container before mounting React Flow and derives the initial viewport and zoom floor during render, so the first frame is the final one. Net switches re-center via a keyed remount instead of an effect, and the lodash debounce utilities are replaced by a debounced value hook. Node CSS fills the wrapper React Flow sizes from the declared dimensions, removing the duplicated (and drifted) size constants, and auto-layout on import uses the layout-stable dimensions.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
The container size state updated on every ResizeObserver tick, so a window resize or panel animation re-rendered the canvas once per frame. Nothing consumes per-frame sizes: the initial viewport reads the size at mount, the zoom floor wants settled values, and the recenter hook reads it at panel-open instants. useContainerSize now reports the first real size immediately (the pre-paint measurement that mounts the canvas) and later sizes only once they have held still for 100ms.
PR SummaryMedium Risk Overview The SDCPN canvas no longer paints at the origin then React Flow nodes get explicit width/height from core; node cards fill the wrapper so sizes match declarations. Import auto-layout always passes Reviewed by Cursor Bugbot for commit b2065e5. Bugbot is set up for automated code reviews on this repo. Configure here. |
🌟 What is the purpose of this PR?
Opening a net painted the canvas twice: once at the origin at zoom 1, then again once a
fitVieweffect centered it. The canvas now renders centered on the net from its very first frame. petrinaut-core owns node dimensions, net bounds and zoom limits, and the canvas derives its initial viewport and zoom floor from them during render — no fit effects, no manual memoization, React Compiler throughout.Kapture.2026-08-27.at.01.59.53.mp4
🔗 Related links
🔍 What does this change?
layout/gains the render dimension constants (moved from the UI),getComponentInstanceHeight(port-based height), and a newgeometry.tswithgetBoundsOfCenteredBoxes,getMinZoomForBoundsandZOOM_PADDING.layoutNodeDimensionsis now derived (per-axis max) instead of hand-copied.SDCPNViewmeasures its container (useContainerSize: layout effect + ResizeObserver, null until the element has area; the first real size lands pre-paint, later sizes only once they have settled for 100ms, so resizes and panel animations re-render once instead of per frame) and only then mounts<ReactFlowProvider key={petriNetId}>with the newSDCPNCanvas. The initial viewport is computed in a lazyuseStatefrom core bounds and React Flow's puregetViewportForBounds, and passed asdefaultViewport— the first render is the final one. Switching nets re-centers via the keyed remount, replacing thefitView-on-id-change effect.minZoomis derived during render: the core candidate, clamped by auseStoreselector that yields the current zoom only while it is below the floor, then debounced with a newuseDebouncedValue. The lodashuseDebounceCallbackanduseResizeObserverutilities are deleted; theonInit/instance-state/onViewportChangeplumbing is gone.ui/lib/viewport.tsis now pure: bounds come from core instead ofinstance.getNodesBounds, andgetViewportRecttakes a size instead of a DOM element. The recenter-on-panel-open hook usesuseReactFlowand the measured container size.layoutNodeDimensions, so its output no longer depends on the compact/classic setting (the drift the core docs warned about).Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
No user-guide page describes initial centering or the zoom floor; behaviour is the same, minus the initial jump.
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
Intentional behaviour deltas, all making declared values match what was actually rendered:
🐾 Next steps
libs/@hashintel/petrinaut/docs/visual-settings.mdmentioned a "fit-to-view" button the viewport controls do not have — pre-existing drift, fixed in FE-1511: List the real viewport-control buttons in the visual-settings guide #9353 (FE-1511).🛡 What tests cover this?
layout/geometry.test.tsandlayout/dimensions.test.tsin petrinaut-core (bounds, min-zoom formula, port heights, derived layout dims).ui/lib/viewport.test.tscoversgetInitialViewport(centering, zoom cap, large-net fit) and its fixtures moved to the center-origin convention the runtime actually uses.❓ How to test this?
yarn workspace @hashintel/petrinaut dev, open thePetrinaut → Handle Spike With Sirstory.📹 Demo
Screenshots pending.
🤖 Generated with Claude Code