Planagonia

How Planagonia works

This page is written for people using the app, not for an AI generating plans from scratch — an assistant needs the precise language specification instead, which isn't covered here.

Getting started

Every plan is written in a small language, and rendered live next to the code that defines it. There's no "compile" step and no separate preview mode — the code pane on the left and the drawing on the right are two views of the exact same thing, always.

The smallest possible plan is a single shape:

element room {
  shape: "rect"
  size: [4m, 3m]
  position: [0m, 0m]
}

Paste that into the code pane in the app and you'll see a 4×3 meter rectangle appear. Drag it around in the viewer — the position values in the code update as you move it. Edit the numbers in the code instead, and the rectangle moves to match.

Elements

An element is the one building block the whole language is made of — a room, a wall, a piece of furniture, or just a reference point. Every element has an id (the word right after element) and a block of properties.

Shape and size

ShapeNeedsLooks like
"rect"size: [w, h]a rectangle
"circle"radius: ra circle
"polygon"points: [[x,y], ...]a closed shape
"polyline"points: [[x,y], ...]an open line (walls)

Leave shape off entirely and the element is just an invisible reference point — useful for marking a corner other shapes can point to (see Connections below).

Position and units

Every element has a position: [x, y] in real-world units — meters by default, or write 90cm for centimeters. Position is always relative to whatever the element is nested inside, not the whole plan — more on that below.

Style

style: { fill: "#cfe0f5", stroke: "#4a76a8", strokeWidth: 0.02 }

Plain color/stroke properties, same idea as CSS. strokeWidth is in meters too, like everything else geometric. Instead of an inline object, style can also be a plain name pointing at a preset declared once in a plan-wide settings {} block (see Settings & modules) — handy once the same colors are reused across many elements.

Labels

label: "Bett" plus show: "hover" or show: "always" puts a text label on the shape — hidden until you hover over it, or always visible.

Nesting means containment

An element written inside another element's braces becomes its child — and its own position is then relative to that parent, not the whole plan:

element room {
  shape: "rect"
  size: [5m, 4m]

  // bed sits 0.3m, 0.4m from the room's own corner — not the plan's
  element bed {
    shape: "rect"
    size: [1.6m, 2m]
    position: [0.3m, 0.4m]
  }
}

Add childPlacement: "inside" to a parent and its rect children are automatically kept from being dragged outside its own boundary — they slide along the wall instead of leaving the room. A single child can set the same thing itself with placement: "inside" (or "outside", for something attached to a container's outer edge), and flush: true pins it exactly to the boundary it's touching instead of just somewhere inside it. In the app, the right-click menu's Placement submenu sets all of this for you.

Hiding things

Any element can be hidden: true — it, and everything nested inside it, is left out of the drawing entirely rather than just covered up. The app's Layers panel is the easiest way to toggle this without editing the property by hand, and reorder which elements paint in front of which.

Connections

A connection links two elements so dragging one carries the other along with it — useful for a piece of furniture that should always stay flush against another:

connection stove counter

In the app, you don't need to type this by hand: hold Ctrl (or Cmd on a Mac) and drag from one element onto another, anywhere on the canvas, and a small menu offers to connect them or, for a bare point dropped onto a rectangle, attach it to that rectangle's outside edge. Right-click an existing connection's own element for a Disconnect option. Connection lines stay hidden by default — hovering either end shows one temporarily, or set settings { showConnections: true } to keep every connection visible permanently.

Shared corners

Walls that actually meet at a point should share that point exactly, rather than two separate coordinates that happen to match. Give a shape's points a sibling element's id instead of a literal [x, y] pair, and it always resolves to that element's own current position:

Three corner elements, referenced by two wall segments — drag any one corner and both connected walls follow it.

element corner_1 { position: [0m, 0m] }
element corner_2 { position: [3m, 0m] }

element wall_a {
  shape: "polyline"
  points: [corner_1, corner_2]
  style: { stroke: "#444", strokeWidth: 0.1 }
}

Working in the app

Selecting, moving, and resizing

  • Click a shape to select it — its span in the code pane highlights. If it's completely covered by something else, click again at the same spot to reach it; hovering shows a small label listing everything stacked there.
  • Drag a selected shape to move it, or one of its corner handles to resize it (rectangles and circles — a shape built from corner references resizes by dragging its own corners instead).
  • Arrow keys nudge the selection by a small step; hold Shift and the same keys resize it.

The right-click menu

Delete, Duplicate (copies the whole shape, including anything nested inside it), Bring to Front/Send to Back, a Placement submenu (inside its container, flush to the edge, or free of both), and — once connected to something — Disconnect.

The rest of the toolbar

  • New Element (Edit tab) drops a fresh line, rectangle, circle, or polygon into the selected element, or the plan's own root if nothing's selected.
  • Undo/Redo (or Ctrl+Z / Ctrl+Shift+Z) cover every drag, edit, and menu action.
  • Grid and Snap are independent, each with its own flyout and its own spacing — a fine visual grid with coarse snapping, or the reverse, are both one click away.
  • Units switches the scale bar and dimension labels between metric and feet/inches — display only, a plan's own code always stays metric.
  • Layers lists every element in front-to-back order; hide one or reorder it with the ▲/▼ buttons — a hidden element is saved as hidden: true in the plan itself, not just remembered by the app.
  • Export gets the current plan out as a plan file, a PNG, or an SVG.
  • Scroll (or pinch, on a touchscreen) to zoom, drag empty space to pan, and use Fit to reset the view. Drag the thin divider between the code and the drawing to resize either pane.
  • On a phone, the editor splits into Code/Viewer tabs, and holding a shape opens the same right-click menu a long press away.
Collisions and overlaps Two shapes are allowed to overlap by default — a rug under a table, say. Set allowCollisions: true on an element (or in a plan-wide settings {} block) to have it slide along whatever it bumps into instead of overlapping it.

Settings & modules

A plan can start with a settings {} block for plan-wide defaults:

SettingDefaultWhat it does
allowCollisionstruewhether siblings may overlap
allowSelfIntersectingPolygonsfalsewhether a polygon may cross itself
edgeLengthsfalseshow each edge's length by default
styles—named style presets, referenced by name from any element's own style
gridoffbackground grid — size, layer, and opacity, also settable from the header
snapoffsnap-to-grid distance while dragging or resizing, independent of the grid above
keyboardStep0.1how far the arrow keys move or resize the selected element
showConnectionsfalsekeep every connection's line permanently visible, not just on hover
settings {
  styles: {
    wall: { stroke: "#444", strokeWidth: 0.1 }
  }
}

element wall_a {
  shape: "polyline"
  points: [corner_1, corner_2]
  style: "wall"
}

module "name.js" loads extra rendering or interactivity from a file — nothing loads unless a plan's own text declares it, including the editor's own drag-and-drop, pan/zoom, code-highlighting, and Layers panel. Every plan you start from inside the app (New, or any example below) already declares those four for you, so you'll rarely need to write them yourself; turning on Grid or Labels/Dimensions from the header writes that module's own line automatically too, the first time you use it.

Custom (external) modules

A module doesn't have to be one of this app's own built-in ones — module also accepts a full URL, fetched and run alongside whatever else a plan declares:

module "https://example.com/campervan-kit.js"

A module loaded this way can add new rendering (a shape kind the core language doesn't have), new interactivity, or a reusable higher-level building block composed from ordinary elements and connections — a "wall with a door" that expands from a few parameters instead of being written out corner by corner, for example (see the Studio Apartment example below). Browse what other authors have published at planagonia.com/modules, or sign in and write your own from the Account page's "Your modules" dashboard — paste in the JavaScript and you get a stable URL back to declare with module "...", the same as any other module.

You'll be asked before it loads The first time a plan declares a module that isn't one of this app's own built-in ones, the app asks whether to trust the URL before fetching and running it — module code runs with full access to the page, the same as any script you'd choose to add yourself, so only accept one from somewhere you actually trust.

Writing your own module means working directly against the app's own JavaScript API — that's developer territory, not covered here.

Saving & the cloud

Every plan you touch is saved automatically in your browser — no account needed. The header's Open and New buttons each open a full picker with a live preview of every plan or example, so switching or starting one means seeing it first, not just a name in a list. Use Import… to bring in a plan file, and Export to get the current one back out — as a plan file, a PNG, or an SVG.

Sign in (top of the app, or on the Account page) to also explicitly push the plan you're working on to the cloud, or pull one you saved earlier back in — useful for picking up the same plan on a different device. This is optional and separate from the automatic local save, which always happens regardless of whether you're signed in. Don't have an account yet? Create one directly on the Account page — you'll get an email to confirm it before you can sign in.

Examples

Every example below is also built into the app itself — open the plan switcher and pick it from "New from example…".

Blank

A genuinely empty canvas — a single, shapeless root element and nothing else, so there's nothing to undo or delete before you start drawing or typing your own plan.

Studio Apartment

A small room with a bed, a wardrobe flush against the wall, a desk angled into a corner, and a rug — containment keeping the furniture inside the room, a connection linking the stove to the counter beside it (kept permanently visible via showConnections), arrow-tipped dimension lines around the room's own outer edges, and an entry partition wall built with door-module.js instead of being drawn corner by corner.

Show the code
module "annotations-module.js"
module "interactivity-module.js"
module "view-module.js"
module "code-highlight-module.js"
module "hierarchy-module.js"
module "door-module.js"

settings {
  version: 1
  allowSelfIntersectingPolygons: false
  dimensionStyle: "arrows"
  showConnections: true
}

element room {
  shape: "rect"
  size: [5m, 4m]
  position: [0m, 0m]
  style: { fill: "#f7f5f0", stroke: "#333", strokeWidth: 0.04 }
  childPlacement: "inside"
  edgeLengths: true
  show: "always"

  element bed {
    shape: "rect"
    size: [1.6m, 2m]
    position: [0.3m, 0.4m]
    style: { fill: "#cfe0f5", stroke: "#4a76a8", strokeWidth: 0.02 }
    label: "Bett"
    show: "hover"
  }

  element wardrobe {
    shape: "rect"
    size: [0.6m, 1.4m]
    position: [4.4m, 0.3m]
    placement: "inside"
    flush: true
    style: { fill: "#d8c7e8", stroke: "#7a5a9a", strokeWidth: 0.02 }
    label: "Schrank"
    show: "hover"
  }

  element desk {
    shape: "rect"
    size: [1.1m, 0.55m]
    position: [2.6m, 2.7m]
    rotation: 18
    style: { fill: "#fbe3b0", stroke: "#a87a2b", strokeWidth: 0.02 }
    label: "Schreibtisch"
    show: "hover"
  }

  element stove {
    shape: "rect"
    size: [0.6m, 0.6m]
    position: [4.1m, 2.0m]
    style: { fill: "#d9534f", stroke: "#8a2f2a", strokeWidth: 0.02 }
    label: "Herd"
    show: "hover"
  }

  element counter {
    shape: "rect"
    size: [0.5m, 1.2m]
    position: [4.1m, 2.7m]
    style: { fill: "#e8c896", stroke: "#a9895c", strokeWidth: 0.02 }
    label: "Ablage"
    show: "hover"
  }

  element rug {
    shape: "polygon"
    points: [[0.9m, 2.6m], [1.8m, 2.4m], [2.2m, 3.3m], [1.5m, 3.9m], [0.7m, 3.4m]]
    style: { fill: "#e0d5c0", stroke: "#b0a080", strokeWidth: 0.02 }
    label: "Teppich"
    show: "hover"
  }

  element side_table {
    shape: "circle"
    radius: 0.35m
    position: [1.5m, 3.2m]
    style: { fill: "#bfe3f5", stroke: "#3a7a9a", strokeWidth: 0.03 }
    label: "Tisch"
    show: "always"
    dimensions: true
  }

  element entry_wall {
    compose: "wallWithDoor"
    from: [2.5m, 0m]
    to: [2.5m, 1.6m]
    doorAt: 0.8m
    doorWidth: 0.9m
  }
}

connection stove counter

Electrical Grid

Three directly adjacent property parcels — each a polygon sharing its boundary corners with its neighbor — all wired to one shared distribution cabinet, over a declared background grid. Each house's own connection point declares placement: "outside", and the cabinet's internal fuse panel stays hidden until you need it.

Show the code
module "annotations-module.js"
module "interactivity-module.js"
module "view-module.js"
module "code-highlight-module.js"
module "hierarchy-module.js"
module "grid-module.js"

settings {
  grid: { size: 2 }
  styles: {
    parcel: { fill: "#e7ecdf", stroke: "#7c8f6a", strokeWidth: 0.05 }
    haus: { fill: "#dcdcd7", stroke: "#55554f", strokeWidth: 0.07 }
    kabel: { stroke: "#c98a1c", strokeWidth: 0.05, strokeDash: "0.3,0.15" }
    kabine: { fill: "#3a3a38", stroke: "#1c1c1a", strokeWidth: 0.06 }
  }
}

element stromnetz {
  element c0 { position: [0m, 0m] }
  element c1 { position: [8m, 0m] }
  element c2 { position: [16m, 0m] }
  element c3 { position: [24m, 0m] }
  element c4 { position: [0m, 9m] }
  element c5 { position: [8m, 9m] }
  element c6 { position: [16m, 9m] }
  element c7 { position: [24m, 9m] }

  element parzelle_1 {
    shape: "polygon"
    points: [c0, c1, c5, c4]
    position: [0m, 0m]
    style: "parcel"
    label: "Parzelle 1"
    show: "hover"
    edgeLengths: true
    childPlacement: "inside"

    element haus_1 {
      shape: "rect"
      size: [4m, 4m]
      position: [2m, 3m]
      style: "haus"
      label: "Haus 1"
      show: "always"
      dimensions: true
    }

    element hausanschluss_1 {
      position: [4m, 3m]
      placement: "outside"
    }
  }

  element parzelle_2 {
    shape: "polygon"
    points: [c1, c2, c6, c5]
    position: [8m, 0m]
    style: "parcel"
    label: "Parzelle 2"
    show: "hover"
    edgeLengths: true
    childPlacement: "inside"

    element haus_2 {
      shape: "rect"
      size: [4m, 4m]
      position: [2m, 3m]
      style: "haus"
      label: "Haus 2"
      show: "always"
      dimensions: true
    }

    element hausanschluss_2 {
      position: [4m, 3m]
      placement: "outside"
    }
  }

  element parzelle_3 {
    shape: "polygon"
    points: [c2, c3, c7, c6]
    position: [16m, 0m]
    style: "parcel"
    label: "Parzelle 3"
    show: "hover"
    edgeLengths: true
    childPlacement: "inside"

    element haus_3 {
      shape: "rect"
      size: [4m, 4m]
      position: [2m, 3m]
      style: "haus"
      label: "Haus 3"
      show: "always"
      dimensions: true
    }

    element hausanschluss_3 {
      position: [4m, 3m]
      placement: "outside"
    }
  }

  element verteilkabine {
    shape: "rect"
    size: [1m, 1m]
    position: [11.5m, -1.5m]
    style: "kabine"
    label: "Verteilkabine"
    show: "always"
    dimensions: true

    element kabine_anschluss { position: [0.5m, 1m] }

    element sicherung_intern {
      shape: "rect"
      size: [0.6m, 0.6m]
      position: [0.2m, 0.2m]
      style: { fill: "#555555", stroke: "#222222", strokeWidth: 0.02 }
      label: "Sicherungen"
      hidden: true
    }
  }

  element kabel_1 {
    shape: "polyline"
    points: [kabine_anschluss, [3m, -0.5m], hausanschluss_1]
    style: "kabel"
    label: "Hausanschluss 1"
    show: "hover"
  }

  element kabel_2 {
    shape: "polyline"
    points: [kabine_anschluss, hausanschluss_2]
    style: "kabel"
    label: "Hausanschluss 2"
    show: "hover"
  }

  element kabel_3 {
    shape: "polyline"
    points: [kabine_anschluss, [21m, -0.5m], hausanschluss_3]
    style: "kabel"
    label: "Hausanschluss 3"
    show: "hover"
  }
}

connection parzelle_1 parzelle_2
connection parzelle_2 parzelle_3
connection hausanschluss_1 haus_1
connection hausanschluss_2 haus_2
connection hausanschluss_3 haus_3
connection hausanschluss_1 verteilkabine
connection hausanschluss_2 verteilkabine
connection hausanschluss_3 verteilkabine

Campervan Build

A camper's interior split into three layer groups — furniture, electrics, plumbing — with the water tank tucked directly under the bench seat, a real packaging trick small campervans actually use. A fold-out table sits angled off the bench (rotation), a cabinet stays pinned flush against the van's own wall, and hiding the "Einrichtung" layer in the app reveals the tank underneath, exactly where an installer would need to see it. Collision checking is turned on plan-wide (allowCollisions: false) — the fold-out table itself opts back out with its own allowCollisions: true, since it's meant to overlap the bench it folds against.

Show the code
module "annotations-module.js"
module "interactivity-module.js"
module "view-module.js"
module "code-highlight-module.js"
module "hierarchy-module.js"

settings {
  allowCollisions: false
}

element camper {
  shape: "rect"
  size: [3.6m, 1.8m]
  position: [0m, 0m]
  style: { fill: "#f2f0ec", stroke: "#333", strokeWidth: 0.04 }
  label: "Camper"

  element einrichtung {
    label: "Einrichtung"

    element kueche {
      shape: "rect"
      size: [0.6m, 1.2m]
      position: [0m, 0m]
      style: { fill: "#e8c896", stroke: "#a9895c", strokeWidth: 0.02 }
      label: "Küche"
      show: "hover"
    }

    element sitzbank {
      shape: "rect"
      size: [1m, 0.6m]
      position: [0.8m, 0m]
      style: { fill: "#d8c7e8", stroke: "#7a5a9a", strokeWidth: 0.02 }
      label: "Sitzbank"
      show: "hover"
    }

    element klapptisch {
      shape: "rect"
      size: [0.5m, 0.35m]
      position: [1.55m, 0.55m]
      rotation: 20
      style: { fill: "#c9a876", stroke: "#8a6a42", strokeWidth: 0.02 }
      label: "Klapptisch"
      show: "hover"
      allowCollisions: true
    }

    element haengeschrank {
      shape: "rect"
      size: [0.5m, 0.25m]
      position: [0.9m, 1.55m]
      placement: "inside"
      flush: true
      style: { fill: "#cbb994", stroke: "#8a7850", strokeWidth: 0.02 }
      label: "Hängeschrank"
      show: "hover"
    }

    element bett {
      shape: "rect"
      size: [1.7m, 1.8m]
      position: [1.9m, 0m]
      style: { fill: "#cfe0f5", stroke: "#4a76a8", strokeWidth: 0.02 }
      label: "Bett"
      show: "always"
      dimensions: true
    }
  }

  element elektrik {
    label: "Elektrik"

    element batterie {
      shape: "rect"
      size: [0.3m, 0.25m]
      position: [0.05m, 1.5m]
      style: { fill: "#3a3a38", stroke: "#1c1c1a", strokeWidth: 0.02 }
      label: "Batterie"
      show: "hover"

      element batterie_anschluss { position: [0.3m, 0.12m] }
    }

    element sicherungskasten {
      shape: "rect"
      size: [0.25m, 0.2m]
      position: [1.55m, 1.55m]
      style: { fill: "#555555", stroke: "#222222", strokeWidth: 0.02 }
      label: "Sicherungskasten"
      show: "hover"

      element sicherung_anschluss { position: [0m, 0.1m] }
    }

    element leselampe {
      shape: "circle"
      radius: 0.06m
      position: [2.7m, 0.2m]
      style: { fill: "#fce97a", stroke: "#a89a2a", strokeWidth: 0.02 }
      label: "Leselampe"
      show: "hover"
    }

    element kabel_batterie_sicherung {
      shape: "polyline"
      points: [batterie_anschluss, sicherung_anschluss]
      style: { stroke: "#c98a1c", strokeWidth: 0.03, strokeDash: "0.1,0.06" }
      label: "Kabel"
      show: "hover"
    }

    element kabel_sicherung_lampe {
      shape: "polyline"
      points: [sicherung_anschluss, [2.7m, 1.5m], leselampe]
      style: { stroke: "#c98a1c", strokeWidth: 0.03, strokeDash: "0.1,0.06" }
      label: "Kabel"
      show: "hover"
    }
  }

  element sanitaer {
    label: "Sanitär"

    element wassertank {
      shape: "rect"
      size: [0.9m, 0.5m]
      position: [0.85m, 0.05m]
      style: { fill: "#8fc5e0", stroke: "#3a7a9a", strokeWidth: 0.02 }
      label: "Wassertank"
      show: "hover"
    }

    element pumpe {
      shape: "circle"
      radius: 0.08m
      position: [0.7m, 0.4m]
      style: { fill: "#5aa0c0", stroke: "#2a5a7a", strokeWidth: 0.02 }
      label: "Pumpe"
      show: "hover"
    }

    element abfluss { position: [1.75m, 0.3m] }

    element rohr {
      shape: "polyline"
      points: [pumpe, abfluss]
      style: { stroke: "#3a7a9a", strokeWidth: 0.025 }
      label: "Rohr"
      show: "hover"
    }
  }
}

connection klapptisch sitzbank
Try it in the app →