@accurona/elements
v0.4.0
Published
Accurona: the shared element library for Axonometra and Reticulyne. One model per element, rendered as plan, isometric, 2D schematic and 3D views.
Maintainers
Readme
Accurona
The shared core behind Axonometra (floor plans and 3D) and Reticulyne (network diagrams): one source of truth for what the two tools draw, so the same thing looks and measures the same in both, and so other tools can build on it too.
Today Accurona is the element library: furniture, fixtures, comms and security
equipment, with one set of groups, ids and look. It also holds the
shared keymap both tools follow, and the
scene format: one file for floor plans, isometric and 2D
diagrams, with one stable id per object: the file format both tools open and save,
implemented in @accurona/core.
And it holds the shared UI: the MUI theme, menus and panels both
tools are built from, so that switching between them needs no retraining.
Elements
Each element is modelled once, as a few simple solids at real size in centimetres. Every view is generated from that model:
- Plan (Axonometra): top-down, true to scale. Built.
- Isometric (Reticulyne): shaded 30° view. Experimental: generated but not yet reviewed item by item. Needed only for the elements a Reticulyne catalogue item draws with (below).
- 3D (Axonometra's 3D view): the same solids as meshes, from
dist/models.json(solids with colours resolved; decals left out). Built. - 2D schematic (Reticulyne's flat, Visio-style view): not to scale, one
line weight for every symbol. A ceiling or wall device draws its plan
pictogram (see Symbols); anything else is seen from the front, with the
ports, LEDs and bezels on its front face. An element whose telling face is
its top (a desk phone's keypad) sets
schematic: 'plan'to draw its top-down view instead. Built; reviewed for the elements a Reticulyne catalogue item cross-references.
What belongs in the library
The library holds physical things: anything with a real size that can be placed on a floor plan. An element has no ports and knows nothing about connections.
Reticulyne keeps its own catalogue of the things a network diagram connects: devices with their ports, and virtual things that never appear on a floor plan, such as a cloud service or a VPN link. Ports, media and protocols are defined there, not here.
Many things belong in both: a switch, an access point, a smoke detector. Each
catalogue keeps its own entry, and the two are related by a free-form
cross-reference: an entry names the other catalogue's id, in the same
{ source, ref } shape a scene uses for
links to external tools.
Neither schema depends on it, and a missing or stale reference breaks only
itself. A Reticulyne item that cross-references an element can be drawn with
that element's isometric view (and, in the flat view, its schematic), so a rack is still the same rack, the same size
and colours, on a floor plan and in a network diagram.
Packages
Three packages on npm, all MIT:
| Package | What |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| @accurona/elements | This library, built: manifest.json, models.json, plan/<id>.svg, iso/<id>.svg and schematic/<id>.svg. |
| @accurona/core | The scene format (a Zod schema and its JSON Schema), length units and the shared keymap. |
| @accurona/ui | The shared MUI theme, menus, panels and notifications. |
npm install @accurona/elementsimport manifest from '@accurona/elements/manifest.json' with { type: 'json' };
// Each element's plan drawing, e.g. with Vite:
import sofaUrl from '@accurona/elements/plan/sofa-3.svg?url';Develop
npm install
npm test # rules every element must meet
npm run build # dist/manifest.json, dist/models.json, dist/plan/<id>.svg, dist/iso/<id>.svg (iso experimental), dist/schematic/<id>.svg
npm run sheet # review/<group>.png contact sheets
npm run sheet -- comms --iso --schematicA release (a v* tag) runs the tests and the build and publishes all three
packages to npm (.github/workflows/publish.yml).
Layout
| Path | What |
| -------------------------------- | ----------------------------------------------------- |
| src/groups.mjs | The groups, in display order. |
| src/elements/<group>.mjs | The elements of one group (default export: an array). |
| src/palette.mjs | Colours and line weights. |
| src/render/plan.mjs, iso.mjs, schematic.mjs | The three renderers. |
| ITEMS.md | The item list: the ids, names and sizes built from. |
| packages/core/ | @accurona/core: scene format, units, keymap. |
| packages/ui/ | @accurona/ui, the shared UI (below). |
An element
{
id: 'sofa-3', // kebab-case, unique, never renamed once shipped
name: 'Sofa, 3-Seat', // Title Case; a test enforces it
group: 'living',
tags: ['network'], // optional: network, power, cooling, security, fire, av, safe
size: { w: 210, d: 90, h: 85 }, // cm: width (x), depth (y), height (z)
mount: 210, // optional: cm from mountFrom (wall and ceiling gear)
mountFrom: 'floor', // optional: 'floor' (default; mount = underside above the floor),
// 'ceiling' (mount = gap from the ceiling to the top; 0 = flush,
// negative = recessed, the top that far above the ceiling)
// or 'surface' (mount = gap above the item it stands on,
// on the floor when it stands on nothing)
facing: 'up', // optional: 'up' (default) or 'down', e.g. a dome camera or downlight
surface: 73, // optional: cm from the underside to a top that carries other
// items (desks, tables, benches, base cabinets); <= size.h
parts: [ /* solids, below */ ],
plan: [ /* optional decals drawn on top of the plan, in footprint cm */ ],
symbol: { /* optional: draw on plans as a 40 cm symbol, below */ }
}Axes. x runs left to right, y runs back to front, z runs up. The back of the element (the side against a wall) is at y = 0, the top edge of the plan; the side you use it from faces y = d, the bottom edge. Doors and fronts face the bottom. In the isometric view the front (y = d) and right (x = w) faces show.
Mounting and facing. Parts are always authored facing up, z counting up from the
underside; facing: 'down' mirrors them inside the item's height. mountFrom and
facing are decided in the scene format
and are in scene format version 2. Every element with a mount names its mountFrom:
ceiling gear (detectors, dome cameras, access points, ceiling speakers, the AC
cassette, cable tray, downlights, pendant lights) is 'ceiling', most of it flush
(mount: 0) and facing down, recessed items at a negative mount;
wall gear stays 'floor'. Items put on top of something else stand on it: an element
whose top carries things (tables, desks, benches, base cabinets, sideboards, TV units,
bedside tables, filing cabinets) gives that top's height as surface, and bench-top
gear (monitors, the laptop, the desk phone, the router and desktop switch, the NAS, the
microwave, the table lamp) is mountFrom: 'surface', standing on whatever carrying item an editor places
it over and on the floor when there is none. The renderers draw an element as installed:
- Isometric: the solids mirrored (a dome hangs below its rim), front and side
decals mirrored with them, and
topdecals left out, because they are on the underside the view cannot see. A down arrow on the top marks the item as facing down. - 3D (
models.json): the solids mirrored; a dome that narrows downwards carriesdown: true. - Plan: drawn as authored, because a ceiling item on a plan is read as a reflected
ceiling plan, looking up at its face. An element with
mountFrom: 'ceiling'has its outline dashed, the reflected-ceiling-plan convention (planSvg(el, { reflected: false })turns it off).
Parts
Solids, in cm, all inside size, never overlapping each other (the isometric
renderer orders parts by assuming they are separate; overlapping solids draw in
the wrong order).
| Part | Shape |
| ---------------------------------- | ----------------------------------------------------------------------------------------- |
| { x, y, z, w, d, h, r? } | Box. r rounds its corners on the plan. |
| { cyl: [cx, cy, rx, ry?], z, h } | Vertical cylinder, elliptical if ry is given. |
| { dome: [cx, cy, r], z, h } | Dome (camera domes, lights). |
| { poly: [[x, y], ...], z, h } | Extruded polygon for L-shapes and curves. List points clockwise on the plan (y down). |
Common part options: role (fill, default body), outline (true draws
the part with the heavy plan outline; the first part has it unless it sets
outline: false), dash (a dashed plan outline, e.g. '6 4', for overhead
items such as skylights), and decals per face: top, front, side.
Roles: body, soft (cushions, tops, trays), dark (equipment),
metal, wood, glass (glass and water), plant, ground (rugs, paving).
Decals
Marks on a face, in cm relative to the face's top-left corner. On top that
is the top-left corner of the part's bounding box on the plan (for a cyl,
dome or poly, the box around it, not the plan origin); on front and side, v runs down from the
face's top edge.
| Decal | Draws |
| ------------------------------------------------------ | ------------------------------------------------ |
| { line: [[u, v], ...], closed? } | Polyline or polygon. |
| { rect: [u, v, w, h], r? } | Rectangle. |
| { circle: [u, v, r] }, { ellipse: [u, v, rx, ry] } | Circle, ellipse. |
| { arc: [cx, cy, r, fromDeg, toDeg] } | Arc (0° = +x, 90° = +y, i.e. towards the front). |
Options: fill (a role or accent), stroke (a role, outline or detail;
default detail), accent (a tag colour, filled, for LEDs and status marks),
dash, weight: 'outline' (heavy line), and view: 'plan' or 'iso' to
draw in one view only. A decal on top that only makes sense seen from above
(louvres, seat lines on an item mounted high) takes view: 'plan'.
Symbols
Ceiling and wall devices (cameras, detectors, access points, sensors, call
points) are far too small to see at plan scale. They set symbol, and the plan
draws a 40 × 40 cm frame with a pictogram instead of the model, while
size and parts still describe the real device for the isometric and 3D
views.
symbol: {
frame: 'circle', // or 'square'
glyph: [ /* decals in the 40 × 40 box; default stroke is the outline colour */ ]
}Point directional devices (cameras, sirens, exit signs) towards the bottom of the symbol.
Shared UI
@accurona/ui (packages/ui/) is what Axonometra and Reticulyne share on
screen. Both use MUI; these are the parts that must look and
behave the same in both:
| Export | What |
| -------------------------------------- | ---------------------------------------------------------------------- |
| createAccuronaTheme(mode) | The theme: palette, type, shadows, component defaults. |
| ToolButton, ToolMenu, Surface | Toolbar buttons, a menu that opens on hover or click, their card. |
| SidePanel, FloatingPanel | A panel from the right; a non-modal panel pinned top right (help). |
| AppDialog, CloseButton | A modal with a titled header; every close is named "Close". |
| notify(), NotificationHost | Transient messages, raised from anywhere, including plain classes. |
| ContextMenu | The right-click menu on the drawing. |
| KeyboardShortcutsDialog | The ? list, from the shared keymap, with its Excalidraw differences. |
| Panel, PanelSection, PanelHeader | A properties panel: sticky header, titled sections. |
createAccuronaTheme(mode, { cssVariables }) leaves MUI's CSS variables off
unless asked: MUI writes them to :root, which an embedded editor must not do
to its host page. accuronaThemeOptions(mode) and accuronaVars(mode) give
the same theme as options, for a host that builds its own.
It is built with tsc to packages/ui/dist/ (JavaScript plus types) by
npm run build, and takes React, MUI and Emotion as peer dependencies.
House style
- Recognisable at 1:50, no more. A plan is read from across a room, not inspected. Aim for 5 to 25 marks per element; the plan SVG must stay under 4 KB (a test enforces it).
- Colour carries meaning, sparingly. Bodies are off-white or grey; accent colours mark a device's kind (blue network, orange power, teal cooling, red security and fire, purple AV, green safe: first aid and evacuation) in one small place: a status strip, a lens, a panel. Never a whole body.
- Model real sizes. Take dimensions from common Australian products; the
sizes in
ITEMS.mdare the brief. - Families from functions. When items differ only in size (beds, racks,
wardrobes, desks), write one function and call it per size, as
bed()inbedroom.mjsandrack()incomms.mjsdo.
Contributing
Issues and pull requests are welcome.
- Found a bug, or have an idea or a question? Open an issue. For anything substantial, open the issue before writing code so we can agree on the scope.
- Sending a pull request? Read CONTRIBUTING.md for setup, checks and conventions.
- Found a security problem? Report it privately, as described in SECURITY.md.
Everyone taking part is expected to follow the Code of Conduct.
Author
Created and maintained by Adam Burgess (adamburgess.me). Adam is available for customer implementation work through QANT Pty Ltd.
License
MIT. Every element is drawn here, from scratch; nothing is copied from a third-party icon set.
