Back to planner

Changelog

Every notable change shipped in the planner, newest first. TheUnreleasedsection is what's been merged since the last tagged release.

v0.7.0

2026-06-20

Added

  • **Customise the schematic view.** The 2D schematic centreline is thicker by default and its weight is now adjustable, and you can recolour every track state — straight, curve, turnout/crossing/slip, structure, selected, validation error, warning/off-board, incompatible, and the placement ghosts — from a new "Schematic appearance" section on your profile. The colours apply across the 2D views (the schematic lines and the footprint bed tint); the line weight is schematic-only. Your choices save to your account and follow you across devices, with a one-click reset to the defaults.
  • **Pick any board colour.** Alongside the preset wood and neutral swatches, the board-colour picker (in the New layout dialog and the layout-settings popover) now has a custom-colour swatch that opens your system colour picker for any hex you like. The board recolours live as you choose, and the colour saves with the layout.
  • **Collapse the inspector.** A chevron in the inspector's header minimises the panel to a thin rail, giving the width back to the canvas; the rail's chevron expands it again. Whether it's collapsed is remembered per browser, and its resizable width is preserved across collapse/expand.
  • **Hold Shift to drag along one axis.** While moving a placed piece (or a whole selection), hold Shift to lock the drag to a single axis — whichever the cursor has moved farthest along, switching live if you change direction — so you can slide a piece straight up/down or left/right without it drifting off line. A dashed guide line through the grab point shows which axis you're pinned to. It works the same in the 2D editor and the 3D view. (Shift when you first grab a piece still adds it to the selection; the axis lock kicks in once you're actually dragging.)
  • Only pay for the track you don't already have. The editor's **Bill of Materials** now lets you record how many of each piece you already own — a small −/+ stepper on every row — and the shopping-list price drops to just what's left to buy, with the saving shown next to the total. Owned counts sync to your account (the same collection the encyclopedia feeds), so they follow you between layouts and devices; an "Account for track I already own" toggle flips the list back to the full price any time.
  • The trains you mark as owned in the encyclopedia now sync into the planner alongside your owned track — the groundwork for owned-aware validation (checking a layout against the trains you actually run), coming next.
  • **Validate against your trains.** The Inspector's Validation panel can now run min-radius and clearance checks against the strictest of your owned trains for the layout's scale, instead of a generic per-scale default — so a curve that's too tight for a loco you actually own gets flagged. A "Validate against my trains" toggle (on by default when you own matching trains) switches between your collection and the manual reference-train picker, with a one-line summary of what's being checked. (Pro)
  • **Grade checks against your trains.** Validation now flags a ramp that climbs steeper than your trains can pull — red when a piece's grade exceeds the strictest max grade across your reference train (or owned collection). It's train-relative, so a grade that's fine for one loco can warn for a heavier one.
  • **Build a layout for your trains.** The New Layout dialog now has an optional "Build for my trains" step: pick trains from your collection and the new layout opens in their scale with the catalogue already filtered to track that fits — and, on Pro, validating against exactly those trains (not just everything you own in that scale) from the first piece.
  • **Diamond crossings are placeable.** The Hornby OO set's left- and right-hand diamond crossings now appear under a new **Crossing** tab in the catalogue — they were already imported but had no home in the dock (and were quietly dropped from a layout on load). They place, snap, and render like any other piece. More imported-but-hidden track (slips, buffer stops, turntables, buildings) follows.
  • **Buffer stops and accessory track are placeable.** Two more catalogue tabs — **Buffer stop** (end-of-track bumpers) and **Accessory** (re-railers, uncouplers, cross-system transition pieces) — surface track that was already imported but previously hidden. Kato, Jouef and Märklin sets all gain pieces here.
  • **Turntables are placeable.** A new **Turntable** tab surfaces the imported turntables (Kato, Märklin) — they carry their full pit and radial-road geometry from import and were already hydrating, just hidden from the dock. They place and render like any other piece.
  • **Double slips are placeable.** A new **Slip** tab surfaces the Märklin double slips — they place and render their full trackwork. (Choosing which of a slip's four routes is set comes later, alongside richer turnout routing.)
  • **Ramps and helixes are placeable.** The Hornby OO set now carries an inclined straight and a full-turn helix under a new **Ramp** section — drop them in to climb between decks. They snap by connector like any piece, render with the right slope/spiral in 3D, and report rise, grade %, and pitch in the Inspector; the grade check flags one that's too steep for your trains. (These first SKUs are synthetic proofs-of-concept, not real Hornby parts.)
  • **Markings on track.** The 2D footprint view now draws each piece's markings over the bed, in the same near-black plan style as the outline. Uncouplers — which import as a plain straight — get a distinct synthetic glyph (a ring with a bar across the track) so they read as uncouplers at a glance; other pieces show whatever marking they carry from the imported parameter file. They mirror with a flipped piece and stay out of the way of selection.
  • **Buildings and scenery are placeable.** Structures shipped inside an imported parameter file — buildings, sheds, roundhouses, platforms — now appear under a new **Structure** tab in the catalogue and drop freely wherever you click, with no connector snapping (a structure is an outline for clearance and context, not track). Each renders its real footprint outline, rotates and mirrors like track, warns when it pokes past the baseboard, and reports its width and depth in the Inspector. They save and load with your layout.
  • **Buffer stops read as buffer stops.** A buffer stop now draws a crossed box on its blocked dead end in the 2D footprint view — the standard plan symbol — so an end-of-track bumper is recognisable at a glance instead of looking like a plain stub.
  • **Orientation gizmo in 3D.** The 3D verification view now shows a Unity-style axis gizmo in the top-left corner; click an axis to snap the camera to it. Right-drag still orbits freely.
  • **Linear 3D zoom.** Scrolling in the 3D view now moves the camera a constant distance per notch instead of a shrinking percentage, so zooming in close no longer slows to a crawl — every scroll covers the same ground at any distance.
  • **Resizable, scrollable catalogue.** The catalogue dock's cards now wrap into rows and the dock scrolls vertically, so you can flick through a whole system at once. Drag the dock's top edge to make it taller or shorter; the size is remembered per browser.
  • **Incline track you already own.** Select a connected run of plain straights and curves, type a grade % in the Inspector's new *Incline* field, and the run becomes a continuous climb — straights turn into ramps, curves into helixes, and the elevation restacks along the joins so the pieces stay mated. The grade spreads evenly across the run, the anchored end stays put, and typing 0 levels it back. No need to buy a dedicated ramp piece — incline the track you have, just like propping it on risers. The climb is grade-checked against your trains like any other ramp. Inclined pieces show an uphill chevron marker in the 2D plan (a climb is otherwise invisible from the top-down view) and tilt in the 3D view.
  • **Connect two decks by dragging one piece.** Put a piece on a lower deck and another on a higher deck, then drag a plain straight or curve so one end snaps to the lower piece and the other reaches the higher piece — the planner inclines the new piece to exactly the deck-height difference and joins both ends in a single drop. No separate "make it a ramp" step. If the required climb would be steeper than 25% it won't force the join: the piece drops flat with just the first end joined and an amber hint explains why, so you can reach for a longer piece or a helix.
  • **Switch decks and toggle layers from the top bar.** The editor's top bar now carries a deck switcher (pick the active deck, or add one) and a layer menu (hide/show each layer, set the active one, or add one) right next to the view-mode toggle — no more digging into the Inspector to change levels mid-build. The full deck/layer editors still live in the Inspector. Decks are a Pro capability, so the deck switcher shows a one-click upgrade prompt for free layouts; the layer menu is free for everyone.
  • **Failing pieces are marked on the canvas.** Grade, clearance, and min-radius problems now tint the offending track right on the canvas — in both 2D and 3D — instead of living only in the Inspector's issue list. A piece that fails a hard check (too steep for your trains, fouling another track, or a curve below your loco's minimum radius) glows a loud validation red; one that only trips a warning, like a tight overpass, shows the same amber as a piece hanging off the baseboard. The colours read straight off the issues the panel lists, so the canvas and the panel always agree — and selecting a piece still highlights it over any warning.
  • **Set the route on a slip or turnout.** Select a double slip and the Inspector's Route control now lists all four of its routes by name — and a turnout lists its two — so you can throw which way each switchable piece is set; the same choice is on the right-click "Set route" menu. The route drives the polarity check (the routes a piece isn't carrying stop conducting, so a slip set one way no longer shorts what's wired to the other diagonal) and the rail overlay paints only the active route. Routes read by their real catalogue names ("Normal"/"Left", "Cross1"/"Slip2") instead of a generic Main/Diverging, so the label matches the piece. Diamond crossings stay fixed — both tracks always live.

Changed

  • **Footprint / Schematic moved onto the canvas.** The 2D drawing-style switch now sits in the canvas's top-left corner — like AutoCAD's in-viewport visual-style control — instead of the centred top bar. It still hides in the pure 3D view and remembers your choice on the device.
  • **Preferences without an account.** The in-app profile is now reachable while signed out (a "Preferences" link on the home) — it shows as "Not signed in" and lets you set your units and the schematic appearance, saved on the device. Account-only defaults (snap step, scale filter) show a sign-in prompt instead. Sign in later and your account settings take over.
  • **"New layout" always asks for a name now.** The home page's New layout button used to create an unnamed "Untitled layout" and drop you straight into it. It now opens the same naming dialog the rest of the app uses, so every new plan starts with a name (and baseboard size) you chose. The dialog no longer closes when you click outside it — so a stray click won't discard the name and baseboard you're setting up; close it with the X or Escape.
  • The 2D footprint view no longer draws sleeper-tie "rungs" across the track when zoomed in — the plan stays a clean band at every zoom. Sleeper-tie detail still renders in the 3D verification view, where it's useful for reading joints and clearances at an angle.
  • **Snap-to-grid now starts off.** Connector-to-connector snapping already handles the common case, and grid snapping was quietly nudging free-form placements; it's now opt-in via the SNAP pill in the status footer.
  • **Pan the board with the spacebar.** Hold space and drag anywhere — over empty bench or over a piece — to pan the view, just like Figma. The cursor turns to an open hand while space is held; releasing it returns to normal selection.
  • **The "Start your layout" panel is now first-run only.** Once you've seen it on a new layout, reopening that layout while it's still empty no longer brings it back. The starter-set dialog stays reachable from the top bar and command palette.

Fixed

  • The top-bar layout switcher now lists your on-device plans too, not just cloud ones. It was fed only the account's cloud layouts, so a plan made offline, made while signed out, or not yet synced never showed up — and a signed-out builder with several local plans saw an empty switcher. It now merges the same local + cloud lists the home page does, so every plan you can open is reachable from the switcher.
  • Turning on the Polarity overlay no longer crashes the editor. A piece whose imported geometry defined no named routes (some crossings and slips) produced an empty rail outline, which the 3D line renderer couldn't draw. Those pieces now fall back to drawing all of their track, and the overlay safely skips anything with nothing to draw.
  • The Validation panel now focuses on what you've selected. Pick a piece (or several) and the issue list narrows to just the problems on that track, with a note when there are more elsewhere on the layout; deselect to see everything again.
  • The Layout Stats panel now shows real numbers. Total track length, bounding box (width × depth), tightest curve radius, and steepest grade are computed live from your layout instead of showing dashes.
  • Placing track around a turntable no longer trips the overlap warning. A turntable is a piece you build around — its radial roads fill its whole pit, so the placement check used to flag every piece dropped at its rim as overlapping. Turntables are now exempt from that check, in both directions (placing track next to a turntable, and placing a turntable next to track).
  • Turntables now read as turntables. Their imported geometry carries a bridge track across the pit for every position, which drew as a solid spirograph disc of crossing lines. They now render as an open pit (a ring) with the approach stubs at the rim — the standard plan symbol — at their correct real-world size.
  • Structures no longer trip the overlap warning. A building or scenery piece is an outline for context, not track, so it legitimately sits over, under, or beside track and other structures — but the placement check was treating it like track and flagging it red. Structures are now exempt from that check in both directions (a structure near a piece, and a piece near a structure).
  • The Structure catalogue tab let you select only the first item. Clicking any other building or scenery piece snapped the selection straight back to the first one; every structure is now independently selectable.
  • Structure footprint outlines no longer thicken as you zoom in. The outline was drawn in real millimetres, so it ballooned into a heavy band at high zoom; it now holds a constant on-screen thickness at every zoom level (and keeps its modelled width in the 3D view).
  • The 3D view can now be spun horizontally around the layout. The perspective camera was left at its default up-axis, which pinned the orbit to the wrong axis; right-drag now circles the bench as expected.

v0.6.0

2026-06-12

- Cloud sync now enforces the Pro license server-side for multi-level / helix data, not just in the editor UI. The save endpoint reads license ownership per-account from the database and refuses to persist a layout carrying more than one deck (or any raised deck) when the account doesn't own the eternal license — so a flipped client flag or a device-scoped local unlock can't push Pro layered data to a non-owning cloud account. Legitimate Pro owners are unaffected; the editor already gates deck creation client-side, so this only ever fires on a payload that went around the UI.

Added

  • Real rails and sleepers in 3D. Switch to the 3D (or Split) view and your track now stands up as genuine model railway — two raised steel rails at the system's real gauge running on timber sleeper ties at the system's tie spacing, instead of a plain extruded slab. It reads as the track you're holding, not a coloured bar. Built efficiently (one or two merged meshes per piece) so even a big layout stays smooth, and the 2D footprint / schematic views are unchanged.
  • Print your plan at full size. A new **Export as PDF (1:1)** option (Export menu and ⌘K) produces a true-scale PDF — 1 mm of track is 1 mm on paper — tiled across pages with overlap and corner registration marks so a layout bigger than a sheet still prints true-size. Each footprint prints at its system's real sleeper-base width, so the trimmed-and-taped sheets are a build template you lay real track straight onto. Every page carries a 100 mm calibration ruler to confirm your printer didn't shrink it — print at 100% / actual size, not "fit to page".
  • See a turnout's frog at a glance. Select a turnout and the inspector now shows its **frog** number (e.g. #2.5) and **frog angle** — the spec that tells you how sharply the routes diverge — derived from the piece's geometry. (Manufacturer-published frog specs can override the derived value once the catalogue carries them.)
  • Switch the 2D view between **Footprint** and **Schematic**. A new toggle on the editor's top bar (next to 2D / 3D / Split) flips placed track between the footprint — the sleeper outline that looks like the plastic piece you're holding, the default — and a schematic: a thin centerline per piece for reading busy trackwork at a glance. It's the same editing surface either way (same snapping, selection, and connections); only the drawing changes. Your choice is remembered on the device, and the toggle hides in the pure 3D view.
  • The website speaks French. A language selector (EN/FR) in the nav and on the content-page headers switches the whole site between English and French, with the locale carried in the URL (`/` for English, `/fr/…` for French) following the encyclopedia's routing pattern. The marketing landing, pricing, sign-in / registration, the account & plan pages, and the docs / changelog / roadmap page chrome are all translated; documentation articles, changelog entries, and roadmap item text stay in English. Your choice is remembered and the in-app planner handoff is unaffected.
  • Lay a run of track in one drag. With the Place tool armed and a straight or curve picked, press and drag on the canvas to lay a whole run at once — each piece previews as a ghost that snaps onto the one before it, just like dropping track by hand, so dragging out adds pieces and pulling back removes them (straights make a line, curves an arc), with the chain's open end marked by a connector dot. Start near a free endpoint and the run continues that track; from empty space it points the way you first drag. Let go and the whole run drops in a single undo step. A plain click still places one, and turnouts/crossings/flex/ramps stay one-per-click.
  • Choose metric or imperial units. Switch between millimetres and inches from the editor's status-bar **mm / in** pill, the **layout settings** popover, or the planner **profile** — and every length the planner shows follows: the cursor readout, inspector dimensions, catalogue tiles, the scale bar, validation messages, and the New-layout, snap-step, and deck-elevation inputs (angles and grade are unitless and don't change). Metric stays the source of truth: all geometry, snapping, and exports remain in millimetres, and imperial is purely how numbers are shown, parsed back to mm when you type. The choice is per-user (defaults to metric) — synced to your account when signed in, remembered on the device otherwise.
  • Editor preferences, inside the planner. The in-app **profile** now has an **Editor defaults** section — measurement units, the default snap-to-grid step, and the catalogue-scale filter — so you can set them in the app instead of only on the website. The website profile keeps the same controls, and its previously-inert "Measurement units" selector is now wired to the real metric/imperial choice.
  • Your account, inside the app. The planner now has an in-app account page (the **Profile** menu item) showing who you're signed in as, your Cloud tier and how many of your synced-layout slots are used, and whether you own Locodex Pro — plus sign out. Billing, plan changes, and account edits still open on the website (the planner doesn't reimplement them), now one click away. It works offline too: your identity comes from the device, and only the plan/Pro details wait on a connection. Previously the Profile link went nowhere.
  • Pro stays unlocked offline. Your one-time Pro (eternal) license is now verified on-device from a signed token (ES256, with the app holding only the public key), so Pro modules and capabilities keep working with no connection instead of waiting on a live server check. The installed app fetches the token after sign-in, stores it, refreshes it whenever you're online, and clears it on sign-out; the local check can only unlock Pro, never revoke a license the server already confirmed.
  • Use the planner without an account. The app now opens to a home of your on-device layouts; **New layout** creates one locally and opens it — no sign-in, no network. Layouts are saved on the device and work offline; signing in is an optional upgrade for cloud sync and cross-device backup. The installed app opens to this home (its `start_url`) instead of the marketing page, and the website's "Start planning" CTA enters the app directly.
  • Open and edit your layouts offline. The editor now renders client-side and loads its layout, the track catalogue, and your account context from on-device caches (IndexedDB) when there's no connection; the installed app's service worker serves the editor shell offline. Open a layout once online and it's available offline next time — edits save locally first and sync to the cloud when you reconnect. Online behaviour is unchanged: the live route still serves when connected, and the read APIs enforce auth (offline trusts the local data).
  • First steps toward offline support. The service worker now caches the app's static assets (stale-while-revalidate), so repeat loads are fast and survive a flaky connection, and a branded **You're offline** screen replaces the browser's error page when you navigate without a connection. Opening the editor still needs network for now — loading layouts from an on-device store is the next step. The worker registers in production only, so it never interferes with the dev server's hot reload.
  • Install Locodex Planner as an app. The landing nav and the bench header now carry an **Install app** button: on Chrome / Edge (desktop and Android tablets) it opens the browser's native install prompt; on iPad / iPhone Safari it shows the Add-to-Home-Screen steps; once installed the planner runs in its own window with a home-screen / dock icon, and the button hides itself. Backed by a web app manifest, generated brand app icons, and a minimal service worker. This is installability only — working offline is a separate upcoming workstream.
  • Stripe Checkout for the Hobby & Club Cloud subscriptions. The /pricing CTAs for the two paid Cloud tiers open a Stripe-hosted Checkout Session created server-side from a new `createCheckoutSessionAction`. The action carries the user's id via `client_reference_id` so the webhook handler can resolve the right account on completion; price IDs come from the `STRIPE_PRICE_HOBBY_MONTHLY/ANNUAL` and `STRIPE_PRICE_CLUB_MONTHLY/ANNUAL` env vars. When the `STRIPE_*` env vars aren't set the action returns a friendly error and the dev shortcuts stay the working path — the existing free CTA is unaffected.
  • Stripe Customer Portal access from /profile. The previously-disabled **Manage billing** button on the Account & plan tab now opens a real Stripe Customer Portal session via `createBillingPortalSessionAction`. The portal owns cancel-at-period-end, plan switching, payment-method updates, and invoice history — the planner doesn't reimplement any of it. The button stays disabled for subscriptions without a `provider_customer_id` (dev-shortcut rows) so the dev tools card remains the canonical way to manage them.
  • `POST /api/stripe/webhook` for the Stripe subscription lifecycle. Verifies `stripe-signature` against `STRIPE_WEBHOOK_SECRET` and handles `checkout.session.completed` (insert subscription row with provider linkage), `customer.subscription.updated` (status / period / cancel flag sync), `customer.subscription.deleted` (mark canceled), and `invoice.payment_failed` (mark past_due). Every handler writes through the existing `upsertActiveSubscription` and a new `updateSubscriptionByProviderId` query so the DB seam is shared with the dev shortcuts. Unknown event types acknowledge with 200 OK so Stripe stops retrying; handler errors return 500 so it does.
  • Subscriptions documentation under /docs. New "Subscriptions and billing" page in the Getting started category covers picking a plan, managing the subscription from /profile, cancellation behaviour, and what happens on failed renewals. Cross-linked from the roadmap entries for the Stripe pieces.
  • Locodex Pro — the one-time eternal license now unlocks the grow-into capabilities, not just modules. Pro modules stay registry-driven (each a single descriptor carrying `tier: "core" | "pro"` — flipping free/paid is that one field), and built-in **pro capabilities** now gate on the same license via `canUsePro`: multi-level layouts (the Decks panel) and loco-aware validation (the reference-train picker; basic checks stay free) render locked with a one-click route to /pricing when unlicensed. Advanced export and scenery are classified Pro for when they land. The /profile and /pricing surfaces derive what Pro unlocks from the module registry (`MODULE_SUMMARIES`) + the `PRO_CAPABILITIES` list, so the copy can't drift.
  • Profile account management — the /profile page's previously-static controls now work. **Edit profile** renames your account (display name); **Change password** updates the password for email sign-in (shown only for email+password accounts, hidden for Google-only ones); **Export all data** downloads every layout as a `.zip` of portable `.trackplan` files (one per plan, plus a `manifest.json`); **Import data** restores layouts from such a `.zip` backup, creating one layout per file up to your plan cap and reporting any files it couldn't read; and **Open a .trackplan** in the Library tab imports a single file and opens it in the editor. "Back to bench" now returns to the bench you opened this session (falling back to the bench selection, or your only plan, when profile was your entry point) instead of always going to the layout list.
  • New marketing pages on the website — **About**, **Planner**, and **Encyclopedia**. About lays out the ecosystem philosophy and values; Planner is a focused overview (hero, three feature cards, and a pricing summary that links to /pricing); Encyclopedia is a coming-soon page with a presentational email capture that submits nowhere. All three reuse the existing landing nav, footer, design tokens, and theme, are reachable from the nav (About) and a new footer Ecosystem column, and are translated EN/FR. The /pricing page is unchanged.

Added

  • In-app plan management at `/profile/plan`. The Account & plan tab's "Change plan" / "Upgrade" CTA now opens a focused in-app page instead of the public marketing `/pricing` page. Upgrades (a higher tier, or monthly → annual) apply immediately with the prorated difference charged today — previewed as "you'll be charged €X today" before you confirm via a new `previewPlanChangeAction` / `changePlanAction` pair. Downgrades are deferred: you keep your current plan until the period ends, then it switches, tracked via new `pending_tier` / `pending_billing_period` columns on `subscriptions`. With Stripe configured the action drives the live subscription (immediate proration invoice for upgrades, a subscription schedule for downgrades) and the webhook clears the pending change once it lands; without Stripe the dev fallback writes the rows directly so the flow is demoable locally. A new "Apply pending change" dev-tools shortcut simulates the period rolling over.
  • Choose which plans keep syncing when you downsize. Cancelling to Free (which syncs a single plan), or downgrading to a smaller-cap tier (e.g. Club → Hobby) while you hold more plans than the destination syncs, now opens a picker in the confirmation dialog: select which plan(s) keep syncing, and the rest turn read-only. Read-only plans stay backed up and exportable to `.trackplan`, and become editable again the moment you resubscribe — nothing is deleted. A new `layouts.frozen` flag records the choice; read-only is *derived* (a frozen plan only locks once the account is actually over its cap, so the pick made at cancel time doesn't bite until the period ends, and deleting plans back down to the cap auto-unlocks the remainder). The editor shows a read-only banner and stops auto-saving on a locked plan, the layout list and Library tab badge locked plans, and save / rename / layout-settings refuse server-side. Upgrades and fresh checkouts release the freezes.
  • The canvas grid gets finer as you zoom in. The 2D grid's spacing now adapts to the zoom — the familiar 100 mm cell / 500 mm section at normal zoom, stepping down through 50, 20, 10, 5, 2 mm to a 1 mm graph-paper grid up close (and coarser as you zoom out) — so the lines stay a legible size and give precise reference under the cursor. The 2D zoom is also bounded to that range — out until the 10 m grid shows, in until the 1 mm grid — so the view no longer scrolls endlessly into empty space. It's a visual aid only: it doesn't change where pieces snap (that's the grid snap step).
  • A scale bar in the 2D canvas. The bottom-left of the bench now shows a Google-Maps-style ruler — a bar of a round real-world length (mm / cm / m) sized to the current zoom — so you can read distances on the layout at a glance. It replaces the old non-functional "80%" zoom chip.

Changed

  • Track now draws at its real footprint width. Each piece's 2D footprint (and 3D bed) uses the catalogue's sleeper-base width for its system instead of a single fixed width — so a wide integrated-roadbed system (e.g. Kato Unitrack) reads visibly broader than bare track (e.g. Hornby/Jouef), and mixed-system layouts show their true relative proportions. Systems without a published width fall back to the system's profile width, then a sensible default.
  • The 2D footprint shows sleeper detail when you zoom in. Once you're zoomed past normal, track in Footprint mode draws sleeper "rungs" across the band at the system's tie spacing — the familiar ladder look that reads as real track. Zoomed out for an overview, it stays a clean band (so a big layout neither clutters nor slows down). Detail level follows the zoom automatically.
  • The 2D view now uses standard model-railway track symbology. Each placed piece is outlined in black with a cap line at every connector, so individual pieces and their joints read at a glance instead of merging into one continuous colour blob. The colour-coded fill (piece kind, selection, off-board warnings, layer tint) stays underneath, and the 3D view is unchanged.
  • The editor now has a single settings icon. The duplicate gear at the bottom of the left tool rail is gone — **Layout settings** (board colour, snap step) opens from the top bar's cogwheel next to the layout name, which is the one place to find it.
  • The language selector is now a flag dropdown. Instead of a single button that toggled EN ⇄ FR, the nav shows the current language's flag and opens a menu listing each language with its flag and native name (English / Français), a check on the active one. The flags are drawn as small SVGs so they render consistently across platforms (emoji flags show as bare letters on Windows).
  • The landing nav's primary CTA is now an **Open app** launcher instead of a single "Get started" button. Since the home is a multi-tool hub, the launcher drops a menu of the ecosystem's apps — **Planner** (live, opens the bench) and **Encyclopedia** (coming soon) — rather than implying one product. On mobile the entry reads "Open Planner →"; signed-in visitors get the same launcher next to their profile. Retired the leftover single-app landing components (hero, auth, track-library, and value-prop sections) that the hub rework had already replaced.
  • The website home is now the ecosystem hub. The landing page is rebuilt around a two-door **Planner / Encyclopedia** diptych above a shared-account band and the "Why Locodex" values, replacing the single-app planner landing — the hero reads "Plan the layout. Look up the rest.", the Planner door links to the new /planner overview, and the Encyclopedia door is a coming-soon card with a presentational email capture. It reuses the existing nav, footer, design tokens and theme, and is translated EN/FR.
  • Scales are now fully data-driven — the planner no longer assumes HO/OO/N. The editor's status-bar ratio reads from the scale record (any catalogue scale shows its real 1:xx instead of a blank), the catalogue's scale picker now lists each scale with its ratio and track gauge (e.g. "OO · 1:76 · 16.5 mm", gauge in your chosen units) rather than just a two-letter code, and the preferred-scale filter on both the website profile and the in-app profile lists whatever scales the catalogue actually carries, from one shared source rather than a fixed list. The in-app filter falls back to a small static list when offline. (Defining your own custom scale — ratio + gauge — is a separate, later step.)
  • The Inspector now reads as three labelled zones instead of one long mixed list. **Selected piece** (the selection card and its Properties), **Whole layout** (Validation, Bill of Materials, Layout Stats, Polarity & Wiring), and **Authoring** (Decks, Layers) are now grouped under their own bands, so "what's selected" vs "the whole plan" vs "how the layout is organised" are visually separate. Same panels, same behaviour — just regrouped.
  • The Inspector panel is now resizeable. Drag its left edge (or focus the divider and use the arrow keys) to widen it from the default up to roughly double, so long piece names, system labels, and SKUs that used to clip mid-word — "Jouef Jouef HO (mode…", "R.6…" — fit in full; the width is remembered per browser. The selection card's title also wraps to the panel instead of overflowing.
  • Catalogue pieces now drop **centred on the cursor** instead of by their connector-A end, so the piece sits where you point. Connector snapping is unchanged (a piece still mates where the join dictates), and grid snap now rounds the piece's centre to the grid.
  • The New layout dialog now asks for the baseboard width and depth in centimetres (e.g. 200 × 100) instead of millimetres (2000 × 1000), which reads more naturally at board scale. The values are still stored and used as millimetres everywhere else, so snapping and geometry stay millimetre-precise.
  • The /profile page now remembers which tab you're on via a `?tab=` query param, so a refresh or a return from Stripe keeps you put instead of snapping back to Layouts. Landing from a billing flow (`?billing=success`) opens the Account & plan tab directly.
  • The Account & plan billing-cycle line now reads "Billed monthly · until 12 Jun 2026" (was "ends") once a cancellation is scheduled, and the **Cancel subscription** button is hidden while a cancellation is already pending — the orange "Cancellation scheduled" line conveys the state instead. A scheduled downgrade shows its own "Switches to X on DATE" line.
  • **Two-product monetization.** Locodex is now a buy-once **Pro** software (the eternal license) plus an optional **Cloud** service, on independent axes. Cloud is a three-tier subscription gating only the saved-plan count — Free (1), Hobby (€3/mo, 10), Club (€8/mo, soft 100 — warns, never blocks) — replacing the earlier Hobbyist/Pro tiers. The subscription grants no software capability; the license alone unlocks Pro (wiring/sectioning, multi-level & helix, loco-aware validation, advanced export). Track-piece count is uncapped on every tier, and export + read-only share links are free for everyone. The /pricing page, /profile Account & plan tab, and the dev-tools shortcuts all reflect the model.
  • Removed the placeholder "Stripe customer portal not wired yet" tooltip on /profile's Manage billing button. The button is now wired to a real action; the tooltip explains the disabled state instead (no provider linkage on the active sub).
  • Big layouts now edit smoothly. Placing or dragging track used to check the moving piece against *every* connector and footprint on the bench on each cursor move, and every curve drew as 16 separate meshes — both fine on a starter oval, but a few-hundred-piece layout would lag under the cursor and drop frames. Snap and overlap checks now consult a spatial index (only nearby candidates are tested), each curve draws as one merged mesh, the connector dots render as a single instanced batch, and the canvas re-renders far less on unrelated changes (layer renames, panel toggles). Same behaviour, same snapping — just fast at scale.
  • Cloud autosave now syncs only what changed. A save used to rewrite the entire layout — every placement, join, layer, and deck — even when you'd nudged one piece; it now diffs against the stored plan and writes just the touched rows, so autosaves on a large layout are far lighter and faster. The layout list also reads its piece counts from a stored counter instead of recounting every placement you own on each visit, and the shared track catalogue is now cached at the CDN edge so the editor's first online load doesn't wait on the database.

Fixed

  • The ecosystem landing page now brands itself **Locodex** — the platform name — in the nav and footer, instead of "Locodex Planner". "Locodex Planner" is reserved for the planner app itself; the hub is the shared entry point for the planner, encyclopedia, and future tools.
  • Laying a run now stops at existing track instead of laying pieces across it. As the run grows toward the cursor, the first piece that would land on an existing piece it isn't joining previews red and the run stops there — nothing past it is laid, and on release only the clean pieces commit. Continuing an existing track from its open end is unaffected (that join is meant to touch).
  • Dragging a placed piece can no longer drop it on top of another. A move that would overlap a piece it isn't joining now turns the dragged piece red and refuses the drop — it snaps back to where it was, the same rule the Place tool already used. Snapping a connector onto the other piece is still fine (joined pieces are meant to touch), and cross-level pieces (one passing above another) are unaffected.
  • Laying a run now shows the next piece much sooner. The next ghost used to appear only after you'd dragged a full piece-length past the open connector — so for curves, whose next end sits off in a direction you can't pre-aim, the run was hard to steer. It now appears as soon as the cursor passes the midpoint (about half a piece past the connector), tracking the nearest whole-piece count to your drag, so the line forms under the cursor as you go.
  • Placing a piece with a click-drag (laying a run) no longer rotates it to the drag direction or shifts it to the click point. Starting a run from empty space now keeps the first piece exactly where the ghost previewed it — centred on the cursor and flat — and extends the run along that orientation toward the cursor, so a straight you drop stays straight and stays put instead of swinging to whatever angle you happened to drag or jumping to the raw click. Runs that continue from an existing connector are unchanged, and a plain click still places one flat piece.
  • Corrected the landing's facts strip: the unverified "Stays smooth past 5,000 pieces" claim — which had never been measured against a real large layout — is replaced with a true one, that the planner **works fully offline**.
  • The command palette (⌘K) no longer lists commands that do nothing. **Place piece…** now switches to the Place tool and **New layer…** adds a layer; the placeholder entries for unbuilt features (Insert turnout, Draw flex track, Measure distance, Export PDF) are gone — so every command in the palette now actually does something.
  • Removed the dead **···** ("More tools") button from the mobile bottom tool strip. All tools already fit inline, and the button had no action.
  • Off-baseboard pieces are now flagged on the canvas, not just in the Validation panel. Any placed piece whose footprint pokes past the baseboard edge is tinted amber, so you spot it at a glance instead of having to read the issues list. It stays a warning — off-board pieces are still allowed (modular benches overhang on purpose).
  • A refused placement now explains itself. When the placement ghost turns red because the piece would overlap an existing one, a hint at the bottom of the canvas names the reason ("Overlaps an existing piece") instead of the click silently doing nothing. The same hint will carry future placement checks (off-baseboard, loco clearance) as they land.
  • Dragging an already-placed piece now respects snap-to-grid. When the move doesn't snap to a connector and grid snap is on, the piece's position rounds to the grid (a multi-piece selection moving with it) — previously it ignored the grid and moved pixel-precisely.
  • The editor's tool rail no longer presents tools that do nothing. **Group** and **Scenery** are gone — grouping is already just shift-click / marquee, and scenery is out of scope — and **Flex Track**, **Measure**, and **Annotate** now sit dimmed and labelled *coming soon* (not selectable, no active shortcut) until they're built. Previously all five highlighted as active but had no effect.
  • The top-right **Fit to view** button on the canvas now works (it had no action). Clicking it frames your whole layout in the 2D view — centring and zooming to fit, with a little margin — or frames the baseboard when nothing's placed yet. Its tooltip no longer claims an "F" shortcut, since `F` is the Flex Track tool.
  • The "See plans" / upgrade links in the planner app no longer hit a 404. The planner doesn't host the pricing page itself, so those links now open it on the Locodex website instead of looking for a route that isn't there.
  • The /pricing page no longer offers a license-holder discount. The Cloud subscription and the one-time Pro license are fully independent, so owning the license doesn't reduce the subscription price — the "I own the eternal license — take €2/mo off" toggle and the discounted "cloud-only rate" are gone, and the license banner now states the two are independent.
  • Signing in now takes you into the planner app. After login or registration you're handed off straight to the planner — signed in via a one-time code — instead of landing on the website's in-migration `/app` page. Logging in with a specific return target (for example from a billing flow) still returns you there.
  • Signed-in visitors to the marketing site now see the vitrine with their profile in the nav, instead of being bounced into the app. The sign-in panel and the Log in / Sign up links (in both the nav and the footer) are hidden while you're signed in.
  • The landing's "Start planning", "Your bench", and "Get started" buttons now open the planner app (the SPA bench) instead of the website's in-migration `/app` page. Signed-in users are handed off already signed in; anonymous visitors land on the app's on-device home — no account needed.
  • Buttons across the planner now show the pointer cursor on hover. Several inline-styled buttons (the New layout trigger, the dialog Cancel / close buttons, and others) were missing it, so they didn't read as clickable; a single global rule now covers every enabled button while disabled buttons stay neutral.
  • The landing page's "See how snap works" secondary button is now clearly visible. It had a transparent background and a faint border that barely showed against the page, so it didn't read as a button; it now has a filled surface and a stronger outline in both light and dark themes.
  • Corrected the landing page's facts strip: the export line now lists the real formats — PNG, parts-list CSV, and the portable `.trackplan` file — instead of PDF and SVG, which aren't user-facing exports; and the scales line reads "Any in the catalogue" rather than implying only HO / OO / N are supported.
  • Catalogue card text no longer spills past the bottom of the tile. The cards were a touch too short for their contents (thumb + name + SKU + dimension rows), so the dimension line bled past the border by a few pixels; the card and the dock are now sized to fit, with tightened line-heights and a clip as a safety net.
  • The gear at the bottom of the editor's tool rail now opens **Layout settings** (board colour, snap step) instead of doing nothing — it had been an unwired stub. It opens the same panel as the top bar's settings button, and shows a pointer cursor on hover.
  • Moving a placed piece by dragging works again — press a piece and drag to move it (it selects the piece first if it wasn't already) instead of the press only panning the camera. The canvas had required a piece to be pre-selected before a drag could begin, even though the group-drag path already handled selecting-then-dragging. Click-to-select, marquee selection, and empty-ground panning are unchanged.
  • Dragging a piece from the catalogue onto the board is now one-shot: it places a single piece and returns to the Select tool instead of leaving Place mode armed for more clicks. Clicking a catalogue card and then clicking the board still places repeatedly. The single-pane 2D/3D canvas had been wired to an older drag-end handler that only cleared the in-flight flag and never exited Place mode.
  • You can now resume a canceled subscription in-app, instead of only through Stripe's hosted portal. While a subscription is set to cancel at the period end, both the change-plan page (a **Resume [tier]** CTA on your current plan's card, plus a banner) and the /profile Account & plan tab (a **Resume subscription** button next to the "Cancellation scheduled" line) clear the pending cancellation via a new `resumeSubscriptionAction` — the plan renews as normal rather than dropping to Free, and any plans that went read-only for the cancellation's keep-selection are unfrozen.
  • You can now cancel a scheduled downgrade and stay on your current plan. A deferred downgrade keeps you on your higher plan until the period ends, so the change-plan page showed your live tier as "current" with no way to undo the pending switch — re-picking it just said "you're already on that plan". On the change-plan page, your current plan's card now shows a **Keep [tier]** button that drops the pending downgrade (releases the Stripe schedule, clears the pending columns, and unfreezes any plans frozen for it), and the tier you're scheduled to move to shows a disabled **Scheduled** state instead of an actionable "Switch". No charge — you stay on the plan you're already paying for.
  • Cancelling (or upgrading) right after a scheduled downgrade no longer errors. A deferred downgrade leaves the subscription managed by a Stripe subscription schedule, and Stripe rejects a direct cancel-at-period-end or item change in that state ("the subscription is managed by the subscription schedule … updating any cancelation behavior directly is not allowed"). Both `cancelSubscriptionAction` and the upgrade path now release the attached schedule first — the new action supersedes the pending downgrade — then apply the change; the cancel also clears the now-moot `pending_tier` / `pending_billing_period`.
  • Cancelling a subscription now updates the account immediately and reliably. The **Cancel subscription** action (on /profile, the change-plan page, and the /pricing Free card) writes `cancel_at_period_end` through to our own subscription row in the same request via a new `cancelSubscriptionAction`, instead of bouncing to Stripe's hosted cancel page and waiting on the async `customer.subscription.updated` webhook. Previously, if that webhook never arrived (no Stripe CLI forwarding locally, or an unsubscribed event in production) or the portal redirect dropped its `?billing=success` flag, a cancelled subscription kept showing "renews DATE" with the cancel button still live — and re-clicking hit Stripe's "already set to be canceled at period end" error. The action is now write-through and idempotent (the webhook still reconciles the same fields when it lands), happens in-app behind a confirmation dialog (a new reusable `ConfirmDialog`), and replaces the old `createCancelPortalSessionAction`. **Manage billing** still opens the full Stripe portal for payment methods, invoices, and plan switching.
  • Theme toggle on the editor's TopBar and on /profile no longer logs a React hydration mismatch on first load. `useTheme()`'s `resolvedTheme` is undefined during SSR (no localStorage on the server), so the icon swap + title attribute hydrated against the wrong default; both surfaces now gate the read behind a mount effect, matching the existing `ThemeToggle` and `LandingNav` pattern.

Removed

  • The redundant **Erase** tool (and its `D` shortcut) is gone from the tool rail. It only ever selected the piece you clicked — deleting was always done by selecting and pressing `Delete` — so the tool added a rail slot and a hotkey without doing anything Select didn't. To remove track, select it and press `Delete` (or `Backspace`).
  • The non-functional "Conflict log" placeholder row in Preferences → Data & account. Multi-device conflict review/merge isn't a built feature, so the dead row is gone rather than implying it exists.

v0.5.0

2026-05-28

Added

  • Multi-deck layouts. New **Decks** accordion in the inspector, separate from Layers: each deck has a name and an elevation in mm (default 0). New pieces land on the active deck (orange dot) at its elevation; editing a deck's elevation rebases every piece on it in a single undo step. The 3D camera mode reveals the stack — set Ground to 0 mm, Mezzanine to 80 mm, Top to 160 mm and the decks line up the way they would on a real bench. Decks are deliberately distinct from layers (visibility tags, colour-coding) — a piece sits on exactly one deck and is tagged with zero-or-more layers. Cross-deck connections via ramps / helixes and grade-aware validation are tracked as separate roadmap items.
  • `.trackplan` portable file format. New top-bar Export menu entry "Export .trackplan file" (with a matching command-palette entry) downloads a versioned JSON file that captures everything needed to reconstruct a layout — placements (with turnout route state and the mirror-flip flag), joins (including the insulated-rail-joiner flag), layers + memberships, baseboard size, board colour, and snap-step override. Pieces are referenced by manufacturer system slug + SKU rather than the catalogue's internal cuid, so a file made today survives a catalogue reseed tomorrow and moves cleanly between machines or accounts.
  • Import a `.trackplan` file as a new layout. A new "Import" button next to "New layout" on the home page opens the native file picker; the import action parses + validates the file, checks the user's cloud-save cap, resolves every piece's SKU against the live catalogue, generates fresh cuids for every id in the file (so the same file can be re-imported safely), and redirects to the new layout's editor on success. Strict by design — refuses the import (rather than silently dropping pieces) when a SKU isn't in the target catalogue, surfacing the missing list in a dismissable banner so the user can act on it. Cap, malformed-file, and newer-version-than-supported cases each render their own banner with the relevant context.
  • In-app documentation at `/docs`. Every user-visible feature now has a short, focused page — editor basics, the tool rail, snapping, the 2D / 3D camera modes, the minimap, the baseboard, joins and the green / yellow / red signal, the `.trackplan` import/export flow, and a complete keyboard-shortcut reference. Markdown-sourced (one file per topic in `apps/planner/src/content/docs/`), grouped into categories via lightweight frontmatter, with a sidebar nav and prev/next links between pages. Linked from the landing footer; updates ship in the same commit as the feature they describe, so the docs stay in sync with the running app.
  • Read-only share links. A new Share button next to Export in the editor's top bar opens a dialog that toggles a public link on / off, displays the resulting URL (`/share/<token>`), and offers a one-click copy. Anyone with the link sees a stripped-down viewer — same R3F canvas as the editor (placements, joins, baseboard, 2D / 3D camera toggle) but no tool rail, no catalogue, no inspector, no save path, no account required. Migration 0046 adds `planner.layouts.share_token` (nullable, unique-indexed cuid2); enabling issues a fresh token, disabling clears it, and re-enabling deliberately mints a new URL so the old one stays dead.
  • Ramp pieces (proof-of-concept) — new `"ramp"` piece category and a new `Ramp` UI bucket in the catalogue dock. Ramps have connector A at `(0,0,0)` and connector B at `(length, 0, rise)`, so the B-end's non-zero Z lets the existing snap engine mate the ramp to a piece on a higher deck without any new snap rules. In 3D the ramp body tilts around its A-end Y-axis so the rendered slope matches the connector rise. The Properties accordion shows the ramp's length, rise, grade %, and pitch (with an orange highlight when grade > 3%). v1 ships a single synthetic SKU under Hornby Setrack (`LCDX-RAMP-50`, 672mm × 50mm rise) to validate the mechanics; real-world piece data lands as a follow-up. Migration 0049 adds `"ramp"` to the `piece_category` enum; reseed required to pick up the new piece.

Changed

  • `.trackplan` wire format bumped to v3 — splits decks from layers. Decks are a new top-level array carrying elevation; placements gain `deckId`; layers lose their elevation field. The parser still accepts v1 and v2 files: v1 synthesises a single Ground deck; v2 derives one deck per distinct layer elevation and resolves each placement's deck via its primary layer.
  • `/docs` index and `/roadmap` are now cross-linked. Docs declare an optional `roadmapId` in their frontmatter; when it resolves to a real `RoadmapItem.id`, the docs index renders a status badge next to the page title and the doc page shows a "View on roadmap" link. The reverse direction also lights up: every roadmap item with an `id` that some doc points at now shows a "Read the docs" link next to its title. Five existing pages get the wiring on day one — Baseboard, Minimap, Keyboard shortcuts, .trackplan files, and Share links — and the same plumbing applies as new docs land.

v0.4.0

2026-05-26

Added

  • Polarity & Wiring inspector accordion. New accordion below the built-in panels, registered through the editor's module seam, listing every detected polarity conflict with a plain-English reason, a suggested fix that names insulated rail joiners directly, and a clickable card that jumps the canvas selection to the first offending piece. The panel header doubles as a one-click toggle for the canvas overlay so the user can flip it on without leaving the inspector. Empty state reads "No polarity conflicts" with a teal check.
  • Per-conflict descriptors from the polarity solver. Solver now returns a `Conflict[]` alongside the existing per-track map + count: each entry names every placement in the offending connected component, the specific joins the BFS blamed for the contradiction (any of which is a valid spot for an insulated joiner), a `reason` string, and a `suggestedFix` string. Classification taxonomy lives in `lib/planner/polarity/conflicts.ts` so future kinds (wye, cross-system, live-frog turnouts) plug in without rewiring the solver.
  • Turnout route state — main vs diverging. `planner.placements.turnout_state` (which existed all along but did nothing) is now wired end-to-end: DB column → hydration → editor store → auto-save → polarity solver → 3D rail overlay → inspector → right-click menu. A turnout thrown to main no longer electrically connects what's wired to its diverging exit, and the rails overlay paints only the active route's rails. UI: a Main / Diverging segmented pill in the Properties accordion when a turnout is selected; a right-click "Set route" submenu with current-state badges per side (mixed selections show N/M counts, all-on-one-side shows "current" and disables that entry to make a no-op click impossible).
  • Insulated rail joiners. Migration `0045` adds `is_insulated` to `planner.joins`. Toggling a join's insulation flips the electrical behaviour immediately — a reverse loop with one insulated join now reports zero conflicts and every track keeps a clean polarity assignment, exactly like a real pair of insulated rail joiners across both rails on the bench. UI: a small pill next to each inspector endpoint's jump-to-neighbour link (dashed em-dash off, filled orange ‖ on); a right-click "Insulated joiner at \<connector\>" entry per joined endpoint on single-piece right-clicks, suffixed ON / OFF; a short perpendicular amber bar painted at each insulated join in the polarity overlay.

Changed

  • Editor modules now plug in through a registry instead of being hardcoded in the shell. New `apps/planner/src/app/edit/[id]/_modules/` directory: a `PlannerModule` descriptor type with optional slots for an R3F overlay, a top-right chrome toggle, a badge contribution, and an inspector panel; a `MODULES` array of descriptors that the shell iterates instead of referencing any module by id. The Polarity & Wiring feature is the first (and currently only) entry. Adding a module = a new descriptor file and a new line in the registry; removing one = deleting the file and the line. No other consumer needs to change.
  • Polarity 3D rail overlay now paints only the active route's rails for turnouts (matching the solver's electrical model) and draws a short perpendicular amber bar at every insulated join so the electrical break reads from the canvas as well as the inspector.

v0.3.0

2026-05-26

Added

  • Baseboard size is a real layout property. The New layout dialog gains width + depth inputs (mm, range 200–10 000, defaults 2000 × 1000); the dimensions persist to `planner.layouts.baseboardWidthMm` / `baseboardDepthMm` (the columns were already there but nothing wrote them). The canvas renders the baseboard as a subtle wood-tinted rectangle with an orange outline centred at world origin, in both 2D and 3D modes, so a beginner can see what they have to fit pieces into.
  • Validation: pieces extending past the baseboard. New `checkBaseboardBounds` walks every placement's world-space polyline against the baseboard rectangle and emits one yellow-band "Piece off baseboard" issue per offender (sorted worst overhang first). Surfaces in the issues list, the bottom-left issue count, and the "Checks performed" panel as a new "Baseboard bounds" row (skipped when the layout has no baseboard). Off-board placements aren't blocked — modular benches break the edge intentionally; the validator just flags them.
  • Layout switcher in the top-left breadcrumb. Clicking the layout name now opens a dropdown of every other layout the user owns (most-recently edited first, capped at 10 with a "+ N more" hint when there are more), a "+ New layout…" footer that opens the same dialog the list page uses, and a "Browse all layouts…" link back to the home page. Starter-set dialog stays reachable from the command palette and the empty-canvas welcome panel.
  • User-wide preferred-scales filter. `/profile` Preferences gains a multi-select chip group (All / HO / OO / N) replacing the old single-scale segmented control. Selection persists to a new `users.preferred_scales` jsonb column (migration `0042`); empty / null means "All scales". When a filter is set, the editor's catalogue dock shows only those scales — pick HO + OO and the N selector hides until you change the preference or override it.
  • Editor shortcut ⇧S to temporarily show all scales for the current view. Session-only override — doesn't write to the user's preferred-scale preference; pressing again restores the filtered view. A small in-canvas toast announces the new state. Falls through silently when no filter is set (nothing to override). Plain S stays bound to the Scenery tool — the shift modifier mirrors the ⇧F / ⇧L / ⇧R pattern for layout-level actions.
  • Layers are real now. The Inspector's Layers accordion was a placeholder with two hardcoded rows ("Main" and "Staging") and no plumbing; it now manages a per-layout list with full CRUD — add via "+ New layer", rename inline (double-click the name), recolour from an eight-swatch palette, toggle visibility with the eye icon, delete via the row's kebab menu (refuses to delete the last remaining layer; pieces that lose their only membership get reassigned to the first remaining layer so they stay addressable). One layer is marked *active*; new placements auto-join it. Each layout is seeded with a "Main" green layer at creation, and a backfill migration (`0041`) adds the same Main layer to every pre-existing layout with all its pieces auto-assigned.
  • Per-layer visibility hides pieces whose every layer is hidden, but a piece in two layers stays on screen as long as *one* of them is visible — so a "Hidden" layer can hide a working subset while a "Main" or "Selected for export" overlay keeps them visible from the other side. Hidden pieces are not hit-testable either, so a marquee or click doesn't pull them back into view by surprise.
  • Layer colour coding tints each placed piece with its primary layer's colour. A toggle at the top of the Layers accordion turns it on (default) or off; with it off, pieces revert to the per-kind defaults (straight blue, curve orange, composite purple). Selection and incompatible-with-queued-piece signals still win over the tint, so picking a piece or hovering an incompatible target reads clearly even when colour coding is on.
  • Right-click on a placed piece (or multi-selection) now lists every layer twice: a "Move to layer" section that replaces the pieces' memberships with the picked layer, and an "Also add to layer" section that appends the picked layer without dropping existing ones. Each layer row shows its colour swatch. Both sections honour the selection so a single right-click moves five pieces at once.
  • Bill of Materials groups rows by scale in both the inspector and the CSV export. Mixed-scale layouts (an HO bench with an N module on the side) now read as one section per scale — each with its own piece count and per-currency subtotal — instead of one mashed-together list. Single-scale layouts skip the section chrome entirely so the flat list stays clean. The CSV gains a small section heading row (`"OO — 12 piece(s)"`) and a `"Subtotal (OO)"` row per scale, sitting above the existing grand totals. A new `groupBomByScale` helper backs both renders, and `BomRow` now carries the catalogue's scale name so anything downstream can group without re-walking the catalogue.
  • Drag pieces directly from the catalogue panel. Press on a catalogue card and drag onto the canvas; release commits the placement at the drop point, with the same snap / closure / overlap behaviour as a normal click-to-place. The existing two-click flow (click card → click canvas) still works and now coexists with the one-motion drag — pressing on a card enters place mode the same way a click does. Releasing on the dock or off-canvas cancels the drag without placing. Right- and middle-mouse on a card fall through to the click path so they don't accidentally start drags.
  • Validation accordion now lists every check the validator runs. A "Checks performed" panel under the issues shows Closure, Minimum radius, Clearance, and Grade as separate rows with a pass / fail / skipped badge each; failing rows carry their issue count, skipped rows carry the reason ("pick a reference train", "no joins yet", "no graded pieces yet"). Beginners get visibility into what the validator actually inspected rather than reading silence as "nothing to check".
  • Reference-train dropdown now filters by the focused piece's scale. Selecting an HO piece on the canvas (or highlighting one in the catalogue) hides N-scale and OO-scale trains from the list; when the current selection drops out of the filtered list it auto-switches to the first remaining match so the validator stays consistent with the user's focus. Falls back to the full list when the filter would empty the dropdown.
  • Cursor-side auto-flip for the placement ghost. While a piece is queued for placement and its ghost is snapping to a free connector, the cursor's side relative to the target's tangent line decides which hand the ghost takes — drag the mouse above the snap point and the ghost is a left-hand curve; drag below and it flips to right-hand. Works the same for turnouts (left vs right) and for any other asymmetric piece. Symmetric pieces like straights are left alone (they have no hand to flip). A new toggle in the top-right canvas chrome (flip-horizontal icon next to the polarity overlay button) opts out of the rule when you want manual Shift+F control instead; defaults to ON. The rule never overrides Shift+F mid-placement — it just sets the desired state when the cursor crosses to the opposite side of the snap tangent, so you can still flip manually and it'll respect that until you move the cursor enough to re-trigger.
  • Inspector endpoints A and B now name their joined neighbour and let you jump to it. When a selected piece has a join on either connector, a small "→ Neighbour name" link sits under the endpoint's status line; clicking it replaces the selection with the joined piece, so you can walk along a chain by clicking through the inspector instead of fishing pieces out of the canvas. The link only shows up when there's actually a joined neighbour to navigate to — open endpoints and unselected states stay single-line as before. Long piece names truncate inside the inspector's narrow column with the full name in the tooltip.

Changed

  • Rotation pivot is now an endpoint of the selection, not the centroid. The pivot is the selection's *free* connector (any connector with no join attached) closest to the cursor — for a right-click rotate, that's right where you clicked; for the R / ⇧R shortcut, it's wherever the mouse currently is on the canvas. Move the cursor near the end of a chain you want to swing from, then rotate. Falls back to the centroid (the previous default) only when the cursor is off the canvas or no piece in the selection has a free connector (e.g. the selection is a fully closed loop). For a single piece this resolves cursor-near-A → pivot at connector A vs cursor-near-B → pivot at connector B, finally giving the user a way to pick which end of a piece spins.
  • Catalogue tile footer stacks SKU above dimensions instead of putting them on one justify-between row. The previous row wrapped mid-string on platforms with wider mono fonts (notably macOS) when the dim label ("R 438 · 22.5°") couldn't fit alongside the SKU, leaving "22.5°" stacked on top of the SKU. Two short lines fit reliably without OS-dependent surprises.
  • Baseboard now reads as a solid wooden surface with depth. Defaults to a lighter "light oak" (#c9a875) instead of the previous denser medium-oak (#a17a4a) so the board sits naturally against both light and dark backgrounds, and pickable colours moved to a per-layout setting (eight swatches: four wood tones — light/medium oak, walnut, pine — plus four cool neutrals — slate, pale grey, mint, pale rose). The colour is set in the New layout dialog at creation and can be changed at any time from a new "Layout settings" cog button next to the rename pencil — a small popover that owns more layout-level settings as they land (board resize, polyline shape, …). Persists to a new `planner.layouts.board_color_hex` column (migration `0043`); the in-canvas side faces and outline derive from this single colour so the user only picks one hex.
  • Snap to grid actually rounds the placement ghost now. The roadmap claimed it had shipped, but the placement code only ever used the raw cursor position — no rounding logic existed. Place mode now snaps the ghost (and the committed placement) to the nearest grid cell, defaulting to 100 mm to match the section grid. Connector-to-connector snap still wins when an endpoint is in range; grid snap is the fallback when no connector is nearby. The SNAP pill in the status footer becomes an interactive toggle (it was a static badge) and displays the active step on wide screens. The step is layered: a per-user default lives on /profile Preferences (range 10–1000 mm; persists to `users.snap_step_mm`), and a per-layout override sits next to the board colour in the Layout settings popover (persists to `planner.layouts.snap_step_mm`; "Use default" button clears the override). An N-scale plan can snap on a finer step than an HO plan without retoggling the user default each time.
  • Right-click "Move to layer" / "Also add to layer" are submenus now instead of flat sections that listed every layer twice. The two items expand to a fly-out on hover (or Right arrow), keeping the parent menu compact regardless of how many layers a layout has. Each submenu row carries the layer's colour swatch and a small suffix that answers "which layer is this piece on?" without having to look at the Inspector: `current` when every selected piece is already in that layer, `N/M` when only some are. The Add submenu greys out layers the whole selection is already in so the click can't be a silent no-op.

Fixed

  • Pieces in a hidden+visible layer pair no longer borrow the hidden layer's colour. The colour-coding resolver was returning the primary layer's colour unconditionally, so a piece in (Main hidden, Testing visible) still rendered Main's green even though Main was off. Fixed by skipping the primary when it's hidden and falling through to the first visible membership — the tint now comes from whichever layer is actually keeping the piece on screen.
  • Reference-train dropdown's scale filter actually filters now. The Inspector was passing the long-form scale name ("HO Scale") down to the dropdown, but the reference-train profiles key on the short code ("HO"), so the comparison never matched and the safety fallback silently restored the full three-train list — an HO selection still saw the N profile. The DB scale `id` column is a cuid2 (so it can't substitute for the short code either), so the fix keys both sides off the stable scale `slug` ("oo-scale" / "ho-scale" / "n-scale") — a new `scaleSlug` field on `ReferenceTrainProfile` matches the encyclopedia's `CatalogueSystem.scale.slug`. The same lookup also drives a new fallback: when nothing is selected, the dropdown is filtered by the layout's scale when every placed piece shares one; mixed or empty layouts still show every train. A Hornby OO layout no longer surfaces N or HO entries in the validator.
  • Rotating a multi-selection no longer tears the layout apart. Group rotation now treats the selection as a rigid body: every piece's world position rotates around the resolved pivot (see the Changed entry above for the pivot rule) before its own heading bumps by the same delta, so the group's internal structure is preserved. Joins straddling the rotated set (one end moved, the other didn't) drop in the same undo step — they recorded a connection that no longer exists — while joins internal to the set survive because both ends rotate by the same transform. New helpers `rotateAroundPoint`, `centroid`, and `pivotForGroupRotation` back the math.

v0.2.0

2026-05-25

Second release. Six weeks of work focused on three threads: editor polish (multi-select, mirror flip, right-click menu, empty-canvas start), exports (PNG track plan + parts-list CSV), and the account / monetisation surface (landing, in-app auth, Google sign-in, profile, /pricing, subscriptions schema, tier-cap enforcement, dev shortcuts, layout thumbnails). Stripe checkout itself is still ahead.

Added

  • Right-click contextual menu on the canvas. Right-click on a placed piece opens a menu with Rotate clockwise (R), Rotate counter-clockwise (⇧R), Mirror flip (⇧F), and Delete (Del) — when the right-clicked piece is part of a multi-selection the actions apply to the whole group and the labels say so ("Rotate clockwise (5 pieces)", "Delete 5 pieces"); right-clicking on a piece that isn't yet selected replaces the selection with just it first, matching the CAD convention that a right-click acts on what's under the cursor. Right-click on empty ground (2D only) opens a layout-level menu — Select all, Clear selection, Toggle grid, Toggle polarity overlay — with the first two grey-disabled when they'd be no-ops. The menu navigates with the arrow keys and activates with Enter; Escape, an outside click, or any item activation closes it. Disabled in place mode so the placement flow stays uninterrupted.
  • Multi-select and group operations. Shift+click on a placed piece adds (or removes) it from the selection; plain click replaces the selection. Left-click drag on empty ground draws a marquee rectangle — release selects every piece whose bounding box touches the rectangle, with shift to extend the existing selection instead of replacing it. Left-click drag on a selected piece moves every selected piece by the same delta in one undo step — and the snap solver runs against the rest of the layout for both single- and multi-piece drags. Single-piece drags rotate-and-translate the piece onto the matching connector; group drags do the same but apply the matched piece's rigid transform (rotation around the snap pivot + translation) uniformly across every dragged piece, so the whole group swings into the snap together. Closure joins are detected at the snapped pose — when a dragged piece's *other* connectors also line up with free stationary connectors (e.g. the classic "slot a piece into a gap that closes a loop" case), every closure join is created alongside the primary snap in the same undo step. Drag-move also manages joins correctly: dragging a piece away from a joined neighbor drops the stale join; multi-piece drags preserve joins internal to the moved group and only drop joins that straddle the move boundary; and a connector that was joined to the dragged piece is visible to the snap solver as the drag moves (because the moment the drag commits the old join drops). R / Shift+R rotates every selected piece by ±15° in place; Delete / Backspace removes the whole group. Escape now backs out one layer at a time — closes any open dialog, then exits place mode, then clears the selection. The inspector's selection card collapses to a "N pieces selected" header when 2+ pieces are picked so the single-piece readouts don't claim to describe the group. The minimap highlights every selected member.
  • Parts list (Bill of Materials) export. The top-bar Export menu (and the matching "Export parts list (CSV)" entry in the command palette, plus the Export CSV button inside the inspector's Bill of Materials accordion) downloads a CSV with one row per distinct piece — SKU, name, brand, system, quantity, unit price, line total, currency — sorted brand → system → SKU so it reads naturally as a shopping list. The file is spreadsheet-ready: RFC-4180 quoting, CRLF line endings, and a UTF-8 BOM so Excel under Windows opens accented piece names correctly. Trailing summary rows roll up totals per currency, so a layout mixing a Hornby (GBP) and a Jouef (EUR) bench shows separate per-currency totals instead of a fake unified number. The inspector's BoM accordion now shows the same headline total live and updates as you place / remove pieces — the placeholder "€124.80" four-row sample card it shipped with is gone.
  • Export layout as PNG. The top-bar Export menu (and the matching "Export layout as PNG" entry in the command palette) downloads a 2000-px-wide track plan styled the way every model-railway manufacturer renders theirs (Roco, Märklin, Hornby, Kato manuals): each piece is a filled track footprint with visible bed width — straights are rectangles, curves are ring segments, turnouts are composites of both — and adjacent pieces touch with their per-piece outlines forming natural boundaries between them. The manufacturer SKU ("R604", "24130", …) is rendered at each piece's centroid so the printed plan stands on its own when ordering parts or building. A horizontal dimension line and metres label sit under the layout, a vertical one to the left, and a bottom-right title block carries the layout name and piece count. Rasterisation runs entirely in the browser via SVG → Image → canvas → toBlob, so the file never leaves the device.
  • Mirror flip for placed pieces and the placement ghost. Shift+F toggles a mirror across the piece's local X axis — a left-hand curve becomes a right-hand curve, a left turnout becomes a right turnout. Straights are symmetric and unaffected. With nothing selected the shortcut flips the ghost while the place tool is active so building a left-hand cascade only needs one toggle; once selected, the same shortcut flips the placement in place. The inspector's Properties panel surfaces the same control as a "Mirror flip" row with an ON/OFF pill. Flipped state persists to the layout row (`planner.placements.flipped`, migration `0040_nostalgic_champions`) so refreshes preserve handedness, and every downstream geometry consumer — renderer, snap, overlap gate, polarity overlay, minimap, thumbnails — routes through a shared `effectiveGeometry(placement)` helper so the flip stays consistent end-to-end.
  • "Start with empty canvas" option on the new-layout welcome panel. Alongside the existing "Build from a starter set" and "Try the demo oval" buttons, a third button dismisses the welcome overlay so you can start placing pieces immediately. Dismissal is session-scoped (reloading the page brings the panel back if the layout is still empty), and the starter-set dialog stays reachable from the top bar and command palette in case you change your mind.
  • Public landing page at the planner root for signed-out visitors. Replaces the single-card placeholder with a marketing surface — hero with an animated snap illustration, value-prop cards, a manufacturer catalogue strip, and an inline auth panel.
  • Planner-side `/login` and `/register` pages, themed with the planner's palette so the auth handoff no longer jumps you to the encyclopedia's vellum theme. Both forms post to the shared `@locodex/auth` backend, so a single users table and JWT cookie still cover both apps.
  • Landing's inline auth panel now wires its tabs to the same `registerAction` / `loginAction` server actions, with confirm-password mismatch caught client-side and pending / error states surfaced on the submit button.
  • Sign in with Google. New `/auth/google/start` + `/auth/google/callback` route handlers verify Google's ID token via the shared `@locodex/auth` helpers, then either log in, link the OAuth identity to an existing user (auto-linked when the verified email matches), or create a new account. The Google button now appears above the email/password form on the landing, /login, and /register surfaces. Requires `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in `.env`; with them blank, Google sign-in renders a "not configured" notice and password sign-in keeps working.
  • Profile page at `/profile`, reachable from the UserPill menu in the layout list and the editor's top bar. Renders a builder's card with the signed-in user, their tier, and aggregate stats (layouts, pieces placed), then tabs for the layout library (with scale + cloud/local filters and a cloud-quota meter against the tier's `maxSavedLayouts`), the current plan, and editor preferences (default scale, units, theme). Billing, sync conflicts, and the offline-module license cards are placeholders pending their backend.
  • Mixed-scale layout support. `layouts.scaleId` is now nullable and is no longer picked up-front in the new-layout dialog — it's derived at save time from the unique scale of placed pieces, or left NULL when the layout is empty or mixes scales (e.g. a modular bench with N and HO sections side-by-side). The profile library card shows the derived scale tag, or an italic "Mixed" badge when none. `LayoutSummary` now also carries `scaleName` joined from the scales table so list/profile UIs can render labels without a second fetch.
  • Public `/pricing` page. Marketing surface for the Free / Hobbyist / Pro tiers and the one-time eternal license, with a monthly / annual billing toggle, a license-owner discount toggle that previews the cloud-only rate, a side-by-side comparison table, and a six-question FAQ. The landing nav and footer now link to it, and the in-page anchor links (`#features`, `#library`) were promoted to `/#features` / `/#library` so they keep working from off-landing routes. All pricing CTAs hand off to `/register`; the checkout backend isn't wired yet.
  • Subscription + license data layer. New `subscriptions` and `licenses` tables in `@locodex/db` (shared schema, public Postgres) with new enums for subscription tier (`hobbyist`, `pro`), status (`active`, `trialing`, `past_due`, `canceled`, `incomplete`), billing period (`monthly`, `annual`), and license kind (`eternal`). Payment-provider columns are nullable so hand-seeded rows work without Stripe. New queries `getActiveSubscription`, `getUserLicense`, and `getUserEntitlements` in `@locodex/db/queries`.
  • Hobbyist tier in `@locodex/auth/tiers`. The planner tier model is now Anonymous / Free / Hobbyist / Pro, matching the /pricing surface. `resolveTier(user, subscription?)` reads an active sub row and promotes the user to Hobbyist or Pro; cancelled, past-due, or incomplete rows fall back to Free. Cloud-save quotas now match marketing (1 / 10 / 100). Pro keeps share-via-link as its sole exclusive entitlement above Hobbyist.
  • Profile page billing card now renders real subscription state — billing cadence ("Billed monthly · renews 12 Jun 2026" or "ends ..." when cancellation is scheduled), an orange cancellation-scheduled notice, and a license-owned badge with purchase date when the eternal license is on file. Module unlock state is derived from tier + license (paid sub or license owner lights up the basic modules; Pro or license owner lights up the rest). The "Manage billing", "Upgrade", and "Buy lifetime license" CTAs stay disabled placeholders pending the Stripe slice.
  • Cloud-layout cap enforcement. `newLayoutAction` now refuses to create the (N+1)th cloud layout when the user is already at their tier's `maxSavedLayouts`. Refusal is a typed `LayoutAtCapResult` returned to the dialog (not a thrown exception) so the UI can render an explicit "you're at your cloud-layout cap" panel with a routed CTA ("Upgrade to Pro" / "Upgrade plan" / "See plans" depending on tier) that links to /pricing. A new `countLayoutsForUser` query gives the server action a cheap count without the placement-counting LEFT JOIN. The layout-list header gained a `"X / Y cloud plans"` pill that turns orange at cap; the New layout button dims; the dialog body shows the live quota line above the input even before cap. The profile billing card's "Upgrade to Pro" / "Upgrade plan" / "See lifetime license" buttons are now real `<Link>` routes to /pricing instead of disabled-with-tooltip placeholders.
  • Piece-cap enforcement in the editor. The Zustand layout store now carries the user's resolved tier + `maxPieces` and gates `addPlacement` / `addPlacementWithJoins` against it — refused placements return `null` (callers ignored the return id before, so no caller change) and set a transient `atCapNotice` slice. A new `AtCapBanner` mounts above the canvas and reads that slice; when set, it renders a floating notice ("At your N-piece cap on the Free plan…") with a routed "Upgrade to Pro" / "Upgrade plan" / "See plans" CTA and a dismiss X. The status footer's piece counter turns orange at cap and drops the "/ unlimited" tail entirely for paid tiers (the 999999 sentinel). On the server, `saveLayoutAction` now resolves the active subscription, computes the same cap, and returns a typed `SaveAtCapResult` (instead of an `{ updatedAt }`) when the persisted count would exceed the limit; the auto-save hook flips the save-status indicator to error and re-raises the at-cap banner if it ever sees this — the client gate should normally prevent it, but the server validation is the real authority.

Changed

  • Right-click no longer pans the camera. Pan stays on middle-mouse drag (both 2D and 3D) and on left-click drag while the Pan tool is active; right-mouse is now reserved exclusively for the new canvas contextual menu. The browser's native context menu is suppressed on the canvas so the editor's menu can take its place.
  • Landing CTAs (nav, hero, footer) point at the planner's in-app `/login` and `/register` routes instead of redirecting to the encyclopedia's auth pages.
  • `NewLayoutDialog` no longer accepts a `scales` prop, and the planner home page no longer loads the catalogue just to populate the dialog.
  • Dropped the All / HO / OO / N / Cloud / Local filter chips from the profile's Layouts tab. The HO / OO / N chips lost their purpose once layouts can mix scales, and the Cloud / Local chips never had a backend (every layout is cloud today). The cloud-quota meter stays.
  • Free tier cloud quota dropped from 3 to 1 saved layout to match the /pricing marketing copy. Local `.trackplan` files are still unlimited and not gated. No-one is on a paid tier yet, so no migration is needed.
  • Dev-only subscription shortcuts on `/profile`. A purple-bordered "Dev tools" card (gated server-side on `NODE_ENV === "development"`) lets the signed-in user flip their own subscription tier (`Free → Hobbyist monthly/annual → Pro monthly/annual`), schedule a cancellation at period end, and grant or revoke the eternal license — without going through a payment provider. The actions delegate to new mutation primitives in `@locodex/db/queries`: `upsertActiveSubscription`, `cancelSubscriptionAtPeriodEnd`, `clearActiveSubscriptions`, `grantLicense`, `revokeLicense`. These primitives are the seam the future Stripe webhook handler will write to, so the dev shortcuts and the production checkout will share the same DB-write path. Every action hard-guards on `NODE_ENV === "development"` as defence-in-depth.
  • Layout list thumbnails. Each card on the signed-in home page now shows a small SVG preview of the layout's centerlines. Thumbnails are computed by the editor's auto-save (a new `generateLayoutThumbnailSvg` helper in `apps/planner/src/lib/planner/thumbnails/svg.ts`) and persisted on a new `planner.layouts.thumbnail_svg` column, so the list page renders without re-fetching geometry per card. Strokes use `currentColor` to inherit the card's text colour; layouts that have never been saved with pieces fall back to an "Empty layout" placeholder. Migration `0039_demonic_risque` adds the column.

Removed

  • Per-tier catalogue gates. `TIER_CONFIG.availableBrands`, `availableScales`, and `availableTrackIds`, plus the `isBrandAvailableForTier` / `isScaleAvailableForTier` / `isTrackAvailableForTier` helpers, have been removed from `@locodex/auth/tiers`. The legacy slug strings they carried no longer matched the live DB catalogue and the helpers had zero call sites, so the fields were silently no-op. The /pricing surface explicitly markets "the full rail catalogue" on the free app — keeping the dead gates around would just invite re-introducing the contradiction. Tier differentiation now lives in three axes only: cloud-save quota, piece count, and share / export entitlements.

Fixed

  • Starter-set and demo-oval curves no longer flip direction after a refresh. Root cause: both seeders hand-rolled a `{ kind: "curve", direction: "left" }` geometry at place time, but the catalogue's reload path resolves the same `pieceId` to a *composite* via `track_segments` — and for systems imported with CW (negative-sweep) xtp data (Jouef HO, Märklin M-Track / K-Track, Kato Unitram, plus assorted others) the composite bends the *opposite* way. Same placement coords, different geometry, mirrored visual. Fixed by (a) routing the seeders' geometry through `pieceGeometryFromDb` so place-time and reload-time always agree, (b) deriving the effective sweep direction with a new `effectiveCurveDirection` helper (normalises simple-curve `direction` and single-segment-composite `sweepAngleDeg` sign to the same answer), and (c) teaching `layoutEndToEnd` to advance position and rotation with the right sign so the loop closes regardless of which way the catalogue piece bends. A new round-trip test in the starter-set suite exercises the previously-broken CW path and asserts the world-polylines match end-to-end through `pieceGeometryFromDb`.

v0.1.0

2026-05-23

Initial baseline. Establishes the version timeline so subsequent entries have somewhere to live. The planner already ships the features listed under `shipped` items in `src/lib/planner/roadmap.ts` — they are not re-enumerated here. The notes below capture the additional work that landed alongside this version cut.

Added

  • Infinite background grid in the 2D editor. The grid extends indefinitely as you pan instead of cutting off at a 2 m boundary.
  • Middle-mouse drag pans the view in both 2D and 3D modes (alongside the existing right-click pan).
  • The Pan tool in the left rail now actually works — activating it maps left-click drag to pan.
  • Bottom-right minimap now renders the layout. Every placement's centerline is traced as an SVG polyline, scaled to fit, with the selected piece highlighted in orange.
  • Minimap shows a "you are here" viewport rectangle outlining the world area the 2D camera is currently looking at. Pan, zoom, or resize the editor and the rectangle follows.
  • Pan tool now switches the canvas cursor to an open hand (grab) when idle and a closed hand (grabbing) during an active pan drag.
  • Clicking on the minimap recentres the 2D camera on that world point — RTS-style "jump to here" navigation. Cursor on the minimap switches to a crosshair when click-to-pan is available (2D and Split modes; not in 3D-only mode).
  • Catalogue compatibility hint. When a piece is selected on the canvas, the catalogue dock marks compatible pieces with a green-tinted border + corner dot. Cards stay clickable in either state.
  • Canvas compatibility tint. When a catalogue piece is queued for placement, existing placed pieces that can't accept it (no shared connector type) render in muted red. Selection still wins — a clicked piece stays orange.
  • Split view — the 2D / 3D toggle now has a third "Split" mode that shows both side-by-side with a draggable divider. Selections and edits in one pane are reflected in the other.
  • User-facing `/changelog` page rendering this file.

Changed

  • Track pieces can no longer be placed overlapping each other. The ghost preview turns red when the candidate position would land on top of an existing piece, and the click is refused at that pose. Pieces about to be joined (via snap or loop closure) are still allowed to touch.
  • Connector snap radius bumped from 8 mm to 25 mm. Placement no longer demands near-pixel-perfect mouse aim — moving roughly toward a free connector is enough to engage the snap.
  • Camera now stops the instant you release a pan / orbit gesture. Previously drei's default inertia let it slide and decay, which felt imprecise for a CAD tool.
  • Catalogue dock body now stays at a fixed 116 px regardless of which category is selected or whether it's empty. Switching tabs no longer reflows the editor below.
  • Split-view divider drag is now live again — both panes resize in real time as you drag, no ghost-line preview. Achieved by bypassing React during the drag (direct DOM width updates, rAF-coalesced) and capping the canvas device pixel ratio at 1.5×, so the WebGL resize+render cycle fits comfortably in a frame.
  • Ghost-piece preview is now shared across split-view panes — hovering in either the 2D or 3D pane shows the candidate placement in both at the same time.

Fixed

  • Minimap no longer rescales when you pan the camera. Its scale is now anchored to the layout's content (and to the baseboard footprint once a layout has one), so the tracks hold their size and position on the map no matter where you pan. The viewport overlay clips to the minimap border when the camera is partially off-frame, and an orange chevron points back toward the camera when its centre is fully outside the framed area. Drag the viewport rect to pan the canvas.
  • Inspector panel no longer bleeds over the catalogue dock when multiple accordions are expanded — the right rail now scrolls independently.
  • Inspector no longer shows a horizontal scrollbar — both the outer rail and the inner accordion area now clip stray horizontal overflow (long cuids, wide BoM rows, etc.).
  • Place-mode ghost piece now sits under the cursor reliably. The previous on-demand R3F frameloop was leaving the raycast camera matrix stale between events, drifting the ghost off-cursor.
  • Canvas compatibility tint now also covers placements whose pieceId isn't in the catalogue (demo-oval seeds, starter-set placeholders). Previously they stayed neutral, falsely implying "this might fit"; they now resolve to the same `demo-track` fallback the snap module uses and tint red when the queued catalogue piece can't mate with them.
  • Placement overlap gate no longer false-positives in the open area between a turnout's branches. The check now traces each segment as a short polyline of sub-AABBs instead of one piece-level bounding box, so a ghost hovering in the gap between a main branch and a diverging branch stays green.
  • Catalogue dock height is now genuinely fixed (120 px) instead of just floor-clamped, so switching categories or systems can't shift the editor below by even a pixel. CataloguePiece cards are locked at 96 px tall as part of this.
  • Ghost preview for XTrkCAD-imported pieces (e.g. Jouef HO) no longer sits offset from the cursor. The catalogue adapter now re-anchors each piece so connector A lives at the piece-local origin — the convention the renderer, snap, and placement code have always assumed. Without this, XTrkCAD pieces inherited an arbitrary origin from the source file and the ghost appeared shifted by that offset.
  • XTrkCAD curve segments using a negative signed radius (CCW arcs — Hornby OO Setrack, Peco, and any other manufacturer that draws left-hand curves CCW in the source file) now render with their arc actually passing through connector A. The parser was treating the xtp `ang1` field as the math start angle directly, but for CCW arcs `ang1` is the *end* of the math sweep; the parser now shifts by the sweep magnitude to recover the start. Symptom this fixes: the ghost (and any placed piece) drawn one piece-length offset from where it should sit.

Removed

  • Unused Turnout tool button from the left rail. It had no behaviour and was confusing — the Turnout *category* in the catalogue dock already covers turnout selection.