opencalque
CLAUDE.md43 starsChanged 15 days agoMITView on GitHub
Free, open-source 2D architectural editor (plans and layouts) with a Figma-like UI. It was called OpenCAD until October 2026: drawings with "format": "opencad", the .opencad extension and settings stored under the old name are still read (FORMERFORMAT, DRAWINGEXTENSIONS, apps/web/src/migrate.ts). Web app first, Electron shell around it. Read docs/ARCHITECTURE.md before changing anything. It holds the design, the rules that are easy to break, what is untested, and the next steps. Keep it current when you change the design. Agent terminals have…
We split this CLAUDE.md into the rules it gives an agent
4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 32
Every text the user sees goes through t() from apps/web/src/i18n.ts (or msg() in tables built at import), written in English, and gets an …
- Very specific: line 35
Every user-triggerable action is a command in apps/web/src/commands.ts.
- Very specific: line 38
Read a drawing for AI or tooling purposes with withoutAssets(doc): assets are megabytes of base64.
- Very specific: line 41
Keep applyOps linear: no scan of every node inside one op.
≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
OpenCalque
Free, open-source 2D architectural editor (plans and layouts) with a Figma-like UI. It was called OpenCAD until October 2026: drawings with "format": "opencad", the .opencad extension and settings stored under the old name are still read (FORMER_FORMAT, DRAWING_EXTENSIONS, apps/web/src/migrate.ts). Web app first, Electron shell around it.
Read docs/ARCHITECTURE.md before changing anything. It holds the design, the rules that are easy to break, what is untested, and the next steps. Keep it current when you change the design.
Commands
pnpm dev # desktop app
pnpm dev:web # editor in a browser
pnpm test # core tests
pnpm typecheck
pnpm e2e # builds the web app and drives it in Chromium (Playwright, e2e/*.e2e.ts); runs in CI
pnpm build && pnpm smoke # scripted interaction test; prints a folder of screenshots to inspect
pnpm dist # package the installer for this system (apps/desktop/dist)
pnpm schema # regenerate schema/*.json after changing packages/core/src/schema.ts
pnpm rooms:gallery # draw the plans rooms are tested on, to look them over
pnpm warehouse && pnpm warehouse:gallery # regenerate the bundled component libraries, then draw every symbol on one pageAgent terminals have ELECTRON_RUN_AS_NODE=1, which breaks Electron. Prefix Electron commands with env -u ELECTRON_RUN_AS_NODE.
Rules
- Documents are immutable and change only through
applyOps(packages/core/src/ops.ts). In the editor, go throughapply(orapplyOrThrow) orpreview/commitinapps/web/src/store.ts, which keep the operations with each undo step; a core helper that returns a document also returns theopsthat make it, and those are what the editor applies. packages/corehas no UI or DOM dependency. Keep it that way: the CLI and a future server use it. It is compiled without the DOM's or Node's types, sopnpm typecheckcatches a slip; what it takes from its host is listed inpackages/core/src/host.ts.- The editor talks to its host only through
Platform(apps/web/src/platform.ts). No Electron or Node APIs inapps/web. - Node types draw themselves as primitives (
packages/core/src/kinds.ts). Do not add type-specific code to the renderers. - Units are millimetres, Y points down, rotation is in degrees. The tree is
parent+ fractionalorder; there are no child arrays. Groups nest to any depth: move nodes withmoveOps, and userootOf/leavesOf/holdsChildrenrather than assuming a node's parent is a page. - The roadmap and feature inventory are in
docs/FEATURES.md; update the status table there when a feature lands. - Every text the user sees goes through
t()fromapps/web/src/i18n.ts(ormsg()in tables built at import), written in English, and gets an entry in each file underapps/web/src/locales/.pnpm testfails if a language is missing one. - To show or toggle a panel, import from
apps/web/src/dock.ts, never fromLayout.tsx. - Anything only the desktop app can do goes through an optional capability on
Platformand is checked for (platform.claudeCli), so the web build keeps working. - Every user-triggerable action is a command in
apps/web/src/commands.ts. Do not add keyboard handling elsewhere (the canvas keeps only Escape, Enter and Space). - What a tool does on the canvas is a gesture in
apps/web/src/canvas/gestures/(PRESSES,MOVES,RELEASESinindex.ts);CanvasView.tsxonly dispatches. The windows over the editor and the toast areuseWindows(windows.ts), the extensions in useregistryanduseExtensions(extensions.ts); neither belongs in the store. - New object types should be parametric objects in an extension unless they need custom editing behaviour. Prefer a declarative (JSON) extension in
packages/warehouse/extensions(listed inpackages/warehouse/src/index.ts) when expressions can describe the geometry; never make the app download and run extension code. - Read a drawing for AI or tooling purposes with
withoutAssets(doc): assets are megabytes of base64. - From 1.0 drawings must stay readable (ARCHITECTURE §4, "The file format and its versions"), and so must extensions written as data (
formatVersion1) and operations (README, "What stays stable").packages/core/test/drawings.test.tskeeps what drawings of 1.0 draw: never edit the drawings intest/fixtures/drawings; when a change of drawing is meant, accept the new SVG withpnpm test -uand say it in the changelog. Read untrusted JSON withreadDocument/parseDocument(packages/core/src/read.ts), which keeps unknown properties and repairs what it can; never strip what the schema does not know. An optional property needs nothing more; a new node type or a changed meaning bumpsFORMAT_VERSIONwith a step inMIGRATIONS. orderis a fractional-indexing key (isOrderKey); ops refuse anything else. Leave it out to put a node on top.- Keep
applyOpslinear: no scan of every node inside one op. Per-drawing indexes (tree, walls, openings, rooms) are cached ondoc.nodes, which everyapplyOpsreplaces. - Values that come from a file, an extension or the assistant (counts, repeats, steps) must be capped where they drive a loop.
- The assistant gets the selection as context (
describeSelectioninapps/web/src/ai/agent.ts); keep what the composer's label says and what is sent the same. - The assistant's agent loop (
apps/web/src/ai/agent.ts) must stay free of store and DOM imports so it remains unit-testable. Claude is called through the official@anthropic-ai/sdk; other providers go throughcompatible.ts. - Lengths are millimetres in documents and in the store. Convert only at the edge, with
apps/web/src/units.ts(formatLength,parseLength,Field'slengthprop); mark length parameters of parametric objects withunit: 'length'. - A provider's
ChatSessionmust implementsnapshot()and accept it back ashistory, without picture data, so conversations can be stored and resumed. - A paper has no children: what lies inside its rectangle is on it, by position (
paperContents). Moving or transforming a paper leaves what is on it where it is (the sheet is a frame put over the drawing); only copying, cutting and duplicating take the contents along, throughwithPaperContentsinapps/web/src/actions/clipboard.ts. Moves on the canvas also addwallFollowOpsso joined walls stay joined. - What shows on every sheet (a title, a logo, a north arrow) is one component with a
sheetsize, in millimetres of real paper, which every paper draws over itself at its scale (templates.ts,sheetContentinkinds.ts). There is one per drawing and nothing is chosen per paper, only left out (plain). Do not bring back layers shown on every page, nor templates chosen from a list: both were tried and removed. The title block stays separate. - A room stores a point, never an outline: its floor comes from
roomAt(packages/core/src/rooms.ts) each time it is drawn. Do not cache a room's shape in the document. A room is the space the walls leave as they are drawn; do not go back to rules about which wall ends nearly meet. When a room comes out wrong, add its plan topackages/core/test/fixtures/rooms(numbers only, no names) and keeprooms-robust.test.tspassing. - A colour property may be a reference to a shared colour,
var(--<id>), orvar(--layer)(LAYER_COLOR), the colour of the node's layer. Resolve it withresolveColor(passing the layer's colour) before using it as a colour; never write the resolved value back into a node. - Modifiers (
packages/core/src/modifiers.ts) are pure functions from primitives to primitives, run in list order at the end ofnodePrimitives. Positions in a modifier are relative to itsframe, which is all that moving and transforming code touches. Never make a modifier read or change the document. - Hints and the size fields depend on
drawStepin the store; show nothing that cannot be used at the current step. - Rotate, mirror and scale nodes with
transformOps(packages/core/src/transform.ts), which asks each type's kind what it means (transforminkinds.ts, required of every type). - What can be brought into a drawing (parametric objects, library components) is in the object library (
WarehouseDialog.tsx); what the drawing holds already is in the Components panel (ComponentsinLeftPanel.tsx), and only there. A library component is takenfresh, a component of the drawing's own each time; only a paste reuses one already there. In the code, keep the words apart: a component is the reusable definition, an instance one placement of it, a parametric object one drawn from its parameters (ARCHITECTURE, "Components"). In the interface, by the owner's choice, the two are not told apart: the Components panel, the instance browser, replacing and the tables of objects all work on entries (entries.ts), a component or a model of parametric objects (variants.ts: a kind with every parameter, so a radiator of 80 cm and one of 1.20 m are two entries; doors, windows, stairs and annotations are not entries,listedAsModel). Do not show the user the difference again. - Exports (SVG, DXF, PDF) draw from primitives through
paintOrder; a new kind of primitive or style needs handling insvg.ts,dxf.ts,pdf.tsandapps/web/src/canvas/draw.ts. - The shape of a wall corner (
joins) and which lines of a wall are drawn (edges) are separate settings. Never draw or hide a wall line as a side effect of a joint: go throughendLineShown/endLineOps. - A wall corner is a position, not a node. Use
wallEndsAt,moveCornerOps,cornerJoinandcornerJoinOps(packages/core/src/walls/joints.ts); how a corner is joined lives on each wall end, injoins. Joints are inferred from where ends meet; where that is wrong, the user says so withjoins'free' on an end (looseEndOps) rather than with one more rule of inference.wallEndStatesays what an end is joined to, touches or nearly meets, for showing it. - A dependency added to or updated in what the editor or the desktop app's main process bundles changes
THIRD-PARTY-NOTICES.txt: runpnpm notices(CI checks it). The main process ships nonode_modules: what it uses is a devDependency ofapps/desktop, which electron-vite bundles. - Preferences store only what differs from their defaults (
storedPrefsinapps/web/src/prefs.ts); a new preference needs only its default. - Every change a user would notice gets a line in
CHANGELOG.md, under the version being worked on. To release: new## x.y.z — datesection, same version in everypackage.json(a test checks them all), then push avx.y.ztag; the workflow builds the installers and publishes the GitHub release with that section as its text. - The desktop menu bar is the window's title bar. Keep its height in step with
titleBarOverlayinapps/desktop/src/main/index.ts. - There are no wall types (taken out in format 2, by the owner's choice): a wall carries its own material, thickness, height and look, and walls are made alike by selecting them and changing them together. Do not bring types back. Geometry code reads
wall.thickness. Heights are for measuring only; nothing is drawn with them. - An arc is an
ellipsewithfrom/to, a curve is apolylinewithsmooth: do not add node types for them. UsearcOfto know whether an ellipse is an arc. - What a component holds has no layer, and an instance has no style (format version 2): each placement gives the layer, and with it the colour of what the component leaves uncoloured.
applyOpsrefuses both; give new nodes their layer throughlayerIn(core) orlayerHere(the store), neveractiveLayerdirectly. - Doors and windows are tied to walls by position, not by reference. Wall geometry lives in
packages/core/src/walls/(index.tssays which part is where). - A wall is of one material (
material), drawn in its look under the wall's own style; a wall of several materials is drawn as several walls side by side, and a door or window goes through the walls laid against its wall (openingIndex). All walls are painted together, those of one material as one shape (union: 'wall:<material>',paintOrder), and a wall stacked over one of another material shows its pattern and outline over it, as the Structure panel orders them (outsideMass), and is not cut where it ends inside it (stopAgainst): between materials, the order decides, never a rule of its own (the owner's choice); their outline centred on their faces as every other line is (mergedOutlines,merge.ts; never draw it outside the walls again, nothing set against a wall would line up), and the line where one material meets another is drawn over them bymaterialSeams(walls/seams.ts): painting each material apart left a sliver where they met. A material is picked from the drawing's own (MaterialChoice); the common ones come in only from the Materials panel. A wall has no fill of its own: its look is its material's. Layers inside a wall, joined by rules at every corner and T, were tried and taken out (the owner found their edge cases endless): do not bring them back. - A table of quantities carries its own words (
title,labels,decimal,language), written by the editor in the user's language when it is made (newTable); the core stays in no language and says in English what a table leaves out. - A wall's
a-bis its line, which is its middle only when it has nojustify; otherwise it is one of its faces. Read where its faces are withwallSides(andwallSpot,wallContains), never as half the thickness each side ofa-b. Doors and windows sit in the middle of the thickness, not on the line.
Open questions for the owner
- Licence: MIT, confirmed by the owner for 1.0. Installers stay unsigned, also the owner's choice.
- The owner wants a hosted, collaborative version to stay possible. Do not introduce designs that block it (see ARCHITECTURE §10).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

