v0.13.0 → v0.15.0: licensing, accounts, the navigation shell, and the engineering registry - #19
Merged
Merged
Conversation
…bar, real currency conversion
Prices: DEFAULT_PRICES grows from 121 keys to 186 exact entries + 12 category
fallbacks, so every one of the 194 library symbols has its own researched price
instead of a flat category default. Researched Aug 2026 against distributor and
manufacturer listings, published cost correlations (Towler & Sinnott / Matches,
CEPCI-escalated) and tender awards.
The flat fallbacks were badly wrong in both directions: solids-handling symbols
sat at $250 (a ball mill is ~$500k, a crystallizer ~$700k), while fittings were
over-priced by the same number (a 2in reducer is ~$18). Vessels were low by
3-11x, safety/relief by 2.4-4x. A few were high: instr.analyzer 15000 -> 4000,
valve.solenoid 700 -> 340.
Each entry now also carries low/high, the basis it was priced at, and ev
('sourced' vs 'est') so the dialog can show what a number actually means.
UI: the live budget chip moves from the bottom status bar into the toolbar and
absorbs the separate plain Budget button — the running cost is something you
steer by while drawing, not a status readout. The dialog is rebuilt: summary
figures with a budget progress bar, per-row pricing basis, est badges on rows
resting on a correlation rather than a published price, outlined overrides, a
new 1.5x install factor, and real CSS classes in place of inline styles.
Currency: the selector only swapped the symbol before, printing a rupee sign in
front of a USD number. New model/currency.ts converts for real across 12
currencies at rates frozen 27 Aug 2026. Prices stay stored in USD so a project
opens identically for anyone, and convert on display; typed values convert
back. PropertyPanel's Cost field follows the same rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nting zeros
The budget target field gains a scale selector: type 7.05, pick Cr, get
₹7,05,00,000. Rupees offer — / K / Lakh / Cr; every other currency offers
— / K / M / B. A hint under the field spells out the expanded amount so there
is no ambiguity about how many zeros landed.
The scale is display-only — the budget is still stored in USD, so switching
Cr -> Lakh re-expresses 7.05 as 705 without changing the budget. The unit is
auto-picked from the stored value (the largest one that leaves a value >= 1,
i.e. what a person would say out loud) and re-picked when the currency changes,
since lakh/crore only apply to rupees.
Also adds stable testids to the currency and install-factor selects, since the
new scale select otherwise shifted what ".bd-controls select" first matched.
tests/model/currency.test.ts covers the conversion round-trips, the legacy bare
symbol docs ('₹' -> INR), unit sets per currency, auto-pick, and that 7.05 Cr
is exactly 70,500,000.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and range, and says when you override it Dropping a component opened a Cost field whose researched price was only a faint placeholder, so the number driving the estimate was invisible until you clicked in. The field is now a small price block: - the budgetary price spelled out under the input - the size/rating that price assumes (the researched basis) - the realistic low–high range for the component class - an "est" badge where no vendor publishes a price and the default comes from a correlation or build-up rather than a listing Three prices can apply to one component, most specific winning: its own cost, the project-wide override from the Budget dialog, then the table default. The note now names which one is in force — "$9,500 budgetary", "$7,000 project price", or "Your price — overriding $9,500 budgetary" — so a number on the drawing is never unexplained. Typing a price overrides; ↺ clears it and hands the component back to the default. Extracted the field into a CostField component so it owns its own store reads instead of threading doc/setNodeCost down from NodeProps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and a workspace design pass Fit was broken, and the cause was structural. The paper was created at the SHEET's pixel size — 1587px for an A3, 3178px for an A1 — so transformToFitContent fitted the drawing to the sheet, not to the window, and whatever the panels left visible then clipped it. The paper is now a viewport: it fills its container, tracks it with a ResizeObserver, and the sheet is drawn as a white page on a grey desk inside the transformed layer group. - fitView() measures the container's own client box, so it is automatically correct with the palette and properties panels open, closed or mid-resize. It fits the sheet UNIONED with the content bbox, so a symbol dragged off the page still lands on screen instead of silently outside it. - The view re-fits itself when the container changes size, until the user pans or zooms; after that the view is theirs (viewState.userMoved). Fit claims it back. Opening a sheet, switching sheets and changing sheet size all re-fit. - The canvas opens fitted instead of showing the top-left corner at 1:1. - Shift+F fits, Shift+1 returns to 1:1, and the toolbar gained a live zoom readout that doubles as a reset-to-100% button. - MIN_ZOOM drops to 0.05 so an A1 can actually be fitted in a laptop window. Fixed a latent bug in the same area: the DXF underlay was inserted as a sibling of .joint-layers rather than inside it, so it was drawn unscaled at the SVG origin and desynced from the drawing as soon as you zoomed. Both the underlay and the new sheet page now live inside the transformed group. The SVG/PNG exports strip .pid-sheet alongside .pid-underlay, so the on-screen page and grid never reach a file. Design pass on the P&ID workspace (HMI untouched): - one cool-neutral token ramp instead of scattered literals - toolbar holds a single row from 1180px up, with consistent 26px control heights, square icon buttons and a segmented zoom cluster; labels drop at breakpoints rather than wrapping the row and shoving the canvas down - titled, sticky SYMBOLS / PROPERTIES panel headers that align across the shell - sheet tabs and drawer tabs read as real tabs joined to their surface - status bar aligned with findings pushed right, and its unsaved-changes dot actually renders (it was emitting a literal ●) - visible focus rings on toolbar, palette, tabs and panel inputs e2e/interactions.spec.ts pinned the paper to 1:1 implicitly by relying on the old default view; it now resets zoom before measuring an on-screen box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Marquee-select a drawing, drag it, and the symbols moved while every hand-routed line stayed behind — the waypoints kept their old coordinates, so the routing sprayed across the sheet. moveNodes() only ever mapped over sh.nodes; sh.edges was untouched. Both the drag path and the arrow-key nudge funnel through it, so both were wrong. A line whose every connected symbol is in the moving set travels rigidly with them, so its vertices — and any free end, which no selection can contain — translate too. A line with one end outside the selection is being stretched instead, and its waypoints stay put; that is the correct deformation and moving them would be wrong. The live drag preview needed the same treatment, or the routing looked broken until you released the mouse: vertices of links fully inside the drag set are captured at grab and translated each frame. That write is deliberately graph-only — change:vertices ignores programmatic updates that carry neither `ui` nor `tool`, so the preview costs no history entries and cannot double-apply against the store write on drop. Verified: dragging 17 symbols with 38 waypoints translates everything by exactly the same delta, adds exactly one undo step, and one undo restores the original geometry. duplicateSelection() already offset vertices correctly, which is why paste kept its routing while move did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Relicenses the project and adds the account layer the cloud features need. Licensing - Moves from AGPL-3.0-only to PolyForm Noncommercial 1.0.0: source-available, free for personal, academic, nonprofit and government use; commercial use requires a paid licence. Not retroactive — v0.12.1 and earlier remain AGPL for everyone who received them, and that grant is not revoked. - Adds NOTICE, SECURITY.md, and SPDX headers across the source tree, with an @license banner preserved through minification so the terms travel with any copy of the bundle. Accounts and cloud drawings - Email/password and Google sign-in; the editor opens behind the gate. - Drawings save to Firestore per account with debounced autosave, a size cap the deployed rules enforce, and readable messages for the failures a user can actually act on. - A marketing homepage at /, the editor at /app, split so a homepage visitor does not download the editor. Firebase config is read from the environment, with no hardcoded fallback A Firebase web config is not a secret — it identifies the project to the client and ships in the bundle by design, with access enforced by firestore.rules and the authorized-domains list. It is env-only for a different reason: a hardcoded default is inherited by every fork, so a fork's users would sign into THIS project — real accounts and real drawings landing in someone else's Firestore, against their quota, with them as the data controller. Absent config is therefore the safe default. Without .env.local the app runs fully local: draw, validate, export and save .pnid files, with the sign-in gate and cloud drawings switched off. .env.example documents standing up your own project, firestore.rules ships so a fork can reproduce the security model, and .firebaserc is untracked so `firebase deploy` never targets this project by accident. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The toolbar was full: about eighteen controls already scrolling horizontally, with a source comment conceding it. There was nowhere to put a second screen. This adds the top-level navigation the engineering-platform plan needs, and improves getting around on its own merits. Workspaces - A rail down the left edge: Draw, Data, Checks, HMI. Ctrl+1..4 switches, and the Checks entry carries a badge only when there are findings — badges are the one thing allowed to pull attention out of Draw. - Each workspace is a real route (/app/draw, /app/data, …) so it is linkable and survives the back button, and each is a lazy chunk so Draw does not pay for the report tables or the HMI simulator. Bare /app and older deep links still open the drawing. - Draw itself is untouched: the same grid, one 48px strip to its left. Command palette (Ctrl+K) One way in for an app that now has more screens than a toolbar can hold. Jump to any tag from any workspace, or type > for commands. Ctrl+F still opens it — the find-a-tag habit is years old and there was no reason to break it. Data and Checks - Data shows the instrument index and line list on screen, not only in a downloaded CSV. Both are built from the SAME rows the CSV writers emit, so the screen and the report cannot disagree. Every row locates its object back on the sheet. - Checks shows every finding and suggestion full-screen and grouped, instead of a 190px drawer, and can apply fixes. - The inspector gains a Where used tab: the sheet, the rest of the loop, the connected lines, and the HMI screens showing this tag. All derived; every row is a jump. Two measured fixes - Cloud saves are about half the size. serializeDoc() pretty-printed and the cloud payload used that verbatim, so half the 900kB Firestore budget was indentation — 328 B/item pretty against 165 B/item compact on the 3-sheet refinery sample. The .pnid file stays readable; only the cloud copy is compact. - Validation runs once per edit instead of three times. The status bar, the drawer and the validation panel each held their own useMemo(doc), so every keystroke ran the checks 3x and the suggestions 2x, each walking every sheet. They now share one pass, which also makes it impossible for two panels to disagree about what is wrong. The drawer's Validation and Advisor tabs merge into one Issues tab; they were always one engine split by an implicit severity flag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Engineering data finally has a home of its own, separate from the drawing that displays it. Until now it was scattered: node.datasheet for instruments only, node.config for things that are really engineering attributes (a control valve's fail position), edge.lineNumber for four strings, node.cost for a number. Nothing existed for equipment specs, line specs, status or ownership. Worse, records hung off node ids, so deleting and redrawing a symbol silently destroyed a whole filled-in datasheet. Records are keyed by tag, not node id A tag is the engineering identity; a node is one placement of it. That matters because the same tag legitimately appears twice (an off-page continuation, a valve shown on two sheets), because delete-and-redraw is normal drafting, and because every deliverable already joins on tag. The trade-off is deliberate: an untagged object gets no record, and the advisor offers to tag it. Four invariants, each covered by a test - Renaming an object MOVES its record; if another symbol still wears the old tag it COPIES instead. - Renaming onto a tag that already has a record REFUSES to merge and leaves both intact — merging two engineering records is unrecoverable, and the duplicate tag is already a finding to resolve. - Deleting a symbol NEVER deletes its record. Deleting a symbol and discarding an approved datasheet are two different intentions and only one was expressed; the orphan is surfaced with a purge action instead. - A pasted symbol arrives untagged and gets no copied record. The Engineering tab The record for the selected object, with a field catalog per kind: instruments get calibrated range and signal, valves get trim, Cv and fail position, equipment gets duty and construction, lines get pipe class, design conditions, insulation and tracing. A fill meter shows how complete it is, and a status runs draft / in review / approved / issued. The datasheet form, the printed datasheet and the datasheet-matrix CSV are now views of that one record rather than a second store. schemaVersion 4 -> 5, non-destructively Documents saved by this version cannot be opened by v0.14.0 or earlier; opening older documents is unaffected, and v1 through v4 all migrate in. node.datasheet is deprecated but still WRITTEN and still READ as a fallback for one release, so a half-migrated document loses nothing and an older build keeps working. A datasheet on an untagged symbol is parked under __unassigned:<nodeId> rather than dropped — binning someone's filled-in datasheet because they never tagged the symbol would be the worst possible outcome. The migration is idempotent and will not clobber a later edit. tests/model/examples.test.ts runs every bundled example — they span schemaVersion 1 through 4 and are the Templates menu — through the migration, checking the node count survives and the document round-trips. Every future schema bump has to keep it passing. Also removes PlantNode.attrs, declared since v1 and never read or written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mainhas been trailing production by five releases. This brings it up tov0.15.0, which is what is live at
pid-studio-praharsh.web.app today.
The branch name says
v0.13.0-licensing; it grew past that and now carriesthree releases.
What's in it
v0.13.0 — PolyForm licensing, accounts, cloud drawings (
978d84b)AGPL-3.0-only → PolyForm Noncommercial 1.0.0: source-available, free for
personal, academic, nonprofit and government use, paid for commercial use. Not
retroactive — v0.12.1 and earlier stay AGPL for everyone who received them.
Adds NOTICE, SECURITY.md and SPDX headers across the tree.
Plus the account layer: email/password and Google sign-in, per-account cloud
drawings in Firestore with debounced autosave, and a marketing homepage at
/with the editor at
/app.v0.14.0 — the navigation shell (
efaf865)A workspace rail (Draw · Data · Checks · HMI) with real routes and lazy chunks,
a
Ctrl+Kcommand palette that absorbsCtrl+F, on-screen instrument index andline list, a full-screen findings report, and a "Where used" inspector tab.
Two measured fixes: cloud saves are ~50% smaller (half the 900 kB Firestore
budget was JSON indentation — 328 B/item pretty vs 165 B/item compact), and
validation runs once per edit instead of three times (three components each held
their own
useMemo(doc)).v0.15.0 — the engineering registry (
0aae247)Engineering data gets a home of its own, keyed by tag rather than node id —
so a record survives deleting and redrawing a symbol, and follows a rename.
Four invariants (move / copy / refuse-to-merge / never-delete-on-delete), each
covered by a test.
Documents saved by v0.15.0 cannot be opened by v0.14.0 or earlier. Opening
older documents is unaffected — v1 through v4 all migrate in,
node.datasheetis still read as a fallback for one release, and a datasheet on an untagged
symbol is parked rather than dropped.
tests/model/examples.test.tsruns all five bundled examples (which spanschemaVersion 1–4 and are the Templates menu) through the migration and a round
trip. Every future schema bump has to keep it passing.
Fork safety
The Firebase web config is read from the environment with no hardcoded
fallback. A hardcoded default is inherited by every fork, so a fork's users
would sign into this project — real accounts and drawings in someone else's
Firestore, against their quota. Without
.env.localthe app runs fully localwith the account layer switched off;
.env.exampledocuments standing up yourown project, and
.firebasercis untracked sofirebase deploycan't targetthis project by accident.
Verification
Run locally against this branch, and re-run under CI conditions (no
.env.local, fresh dev server) to confirm this PR goes green:npm run build— cleannpm test— 714 passingnpx playwright test— 57 passing, 3 skipped (the two sign-in-gate specsskip when no backend is configured, which is the CI case)
Note on history
The three commits are split by logical unit so the log reads properly, but the
working tree only ever held the final state — the SPDX headers alone touch ~137
files, so the changes are interleaved at file level. Only
0aae247(HEAD) isverified green; the two earlier commits are a reconstruction and may not build
in isolation. That affects
git bisectacross this range and nothing else.Merge with a merge commit or rebase — not squash, which would also flatten the
legitimate v0.11.x and v0.12.0 history this branch carries.
🤖 Generated with Claude Code