Skip to content
ispyisail edited this page Oct 7, 2026 · 15 revisions

JavaScript Scripting

Read a project's model, export it, and edit its geometry from a script β€” headless for CI, or interactively against the diagram you have open.


Why this exists, and why JavaScript

Issue #162 asked for scripted automation: export derived files (PDF, BOM, cable lists) after every revision, and drive that from CI. The headless export flags (see CLI Reference) already covered the fixed, one-shot cases. This covers the rest: loops, conditionals, reading the model to decide what to do next, and β€” new in this PR β€” editing the diagram itself.

The engine is Qt's own QJSEngine, not an embedded Python interpreter. That was a deliberate choice, not the obvious one:

  • No new toolchain to package. QJSEngine ships in the Qml module of every Qt SDK QET already targets. Nothing new to install on Windows/macOS packaging, no interpreter version to pin, no GIL.
  • The core classes reflect into scripts almost for free, since they are already QObjects β€” no hand-written binding layer to maintain.
  • It is optional at build time, probed the same non-fatal way QtPdf already is. A QET build without the Qml module compiles identically; scripting is simply absent.

Two ways to run a script

Scripting is off until you switch it on. A script runs with your rights: it can read and change the project and write files. Tick Autoriser l'exΓ©cution de scripts JavaScript (Allow running JavaScript scripts) in Configurer QElectroTech > GΓ©nΓ©ral > Projets (Settings > General > Projects), or, for a run with nobody to tick a box (CI, a batch job), set the environment variable QET_ENABLE_SCRIPTING=1. With neither, running a script from the editor first asks whether to turn scripting on, and --run stops with exit code 3 and says how to turn it on.

Headless β€” --run

qelectrotech --run script.js project.qet

Opens the project, runs the script, exits. Exit codes match the rest of the CLI: 0 success, 1 the script threw or the project failed to open, 2 called wrongly (missing arguments, file not found), 3 scripting is off.

#!/bin/bash
set -e
qelectrotech --run export_after_revision.js "$1"

Interactive β€” "Run Script..."

Projet β†’ Scripts β†’ ExΓ©cuter un script... (Project β†’ Scripts β†’ Run script) opens a file picker (filtered to *.js) and runs the chosen script against the currently open project. Useful for one-off macros you don't want to wire into CI. The same menu holds stored scripts with their own button, icon and shortcut β€” see Script buttons. A run from the editor is one undo step, named Script : <name>, so one Ctrl+Z undoes the whole script (PR #1219); headless --run keeps one step per call.


What a script can do

Every script sees one global, qet. Four groups of capability, each with a different scope β€” read the boundary of each before assuming more:

Read the model

qet.projectTitle()             // -> "My Panel"
qet.filePath()                 // -> "/path/to/project.qet"
qet.folioCount()                // -> 3
qet.folioTitle(0)               // -> "Power"
qet.elementCount(folioIndex)    // -> 42
qet.conductorCount(folioIndex)  // -> 58

Same numbers --info reports, available programmatically instead of parsed from JSON.

Export and save

Thin wrappers around the same --export-* machinery documented in CLI Reference β€” same logic, same output, called from a script instead of a flag:

qet.exportPdf(output, showTerminals = false)
qet.exportPng(outDir, showTerminals = false)
qet.exportSvg(outDir, showTerminals = false)
qet.exportDxf(outDir, showTerminals = false)
qet.exportCables(output)
qet.exportWires(output)
qet.exportBom(output, noSlaves = false, noJunctions = false)
qet.exportWiring(output)
qet.exportNets(output)
qet.exportLinks(output)
qet.exportInfo(output)                       // output may be "" for stdout
qet.setTitleBlock(output, ["revision=B", "date=today"])
qet.save(output)                             // output may be "" to save in place

All return true/false.

Important, and worth reading twice: every export method above re-opens the project fresh from its file on disk. They never see edits a script made with the methods in the next section β€” only save() writes the project's live, in-memory state. Editing then exporting means calling save() first:

qet.addElement(0, "embed://…/relay.elmt", 100, 100);
qet.save("");                    // write the edit to disk
qet.exportPdf("out.pdf");        // now this sees it

Edit geometry, with real undo

qet.addElement(folioIndex, locationPath, x, y)   // -> uuid string, or "" on failure
qet.setElementPosition(folioIndex, elementUuid, x, y)
qet.moveElement(folioIndex, elementUuid, dx, dy)
qet.deleteElement(folioIndex, elementUuid)

qet.undo()      // -> false if the stack is empty
qet.redo()
qet.canUndo()
qet.canRedo()

These go through the exact same undo commands the GUI itself uses (AddGraphicsObjectCommand, QPropertyUndoCommand, DeleteQGraphicsItemCommand). Ctrl+Z in the editor undoes a script's edits exactly as it would the equivalent manual ones β€” they are, mechanically, the same commands on the same stack, not a side channel.

One consequence worth knowing before it surprises you: Qt's undo stack merges consecutive commands on the same object and property when their label also matches. setElementPosition() and moveElement() on the same element produce the same label, so calling them back-to-back collapses into one undo step β€” the same way dragging an item repeatedly does, not one step per call.

locationPath is an element collection path β€” embed://… for something already embedded in this project, common://… / custom://… for the shared collections. Same paths you see in a .qet file's <element type="…"> attribute.

deleteElement() removes the symbol and the wires on its terminals, in one undo step, as the Delete key does. (Before PR #1270, merged 2026-10-03, the wires stayed in the project, attached to nothing.) It refuses to remove an element with a non-deletable terminal (a linked master/slave, for instance) β€” same rule the Delete key follows.

Find a wire by its uuid

The conductor calls name a wire by one of its ends β€” an element uuid and a terminal index β€” which cannot tell apart two wires meeting at one terminal. A wire's uuid names it for good:

qet.conductorUuids(folioIndex)        // -> ["{…}", "{…}", …], same order as qet.conductors()
qet.conductorEnds(folioIndex, uuid)   // -> ["{element uuid} terminal 0", "{element uuid} terminal 2"]
                                      //    or [] if the folio has no such wire

Each end comes back in the form qet.conductors() prints and the conductor calls take, so a script can go from a uuid to a call:

var ends = qet.conductorEnds(0, uuid);
var e = ends[0].split(" terminal ");
qet.setConductorProperty(0, e[0], parseInt(e[1], 10), "num", "W12");

Wires in a project saved by an older QElectroTech get a uuid worked out from what they connect when the project opens, the same every time, and saved from the next save on (see the project database, section 4).

Find a terminal by its uuid

qet.addConductor() and the conductor calls take a terminal as an element uuid and a terminal index. The index is the terminal's place in a list sorted top to bottom, then left to right β€” not the order the symbol file lists them in, and undefined between two terminals at the same point. qet.elementTerminals() shows that numbering, and ends each line with the terminal's uuid:

qet.elementTerminals(0, elementUuid)
// -> ["0: A1 (1 conductor(s)) {5c1e…}", "1: A2 (0 conductor(s)) {9d04…}"]
qet.terminalIndex(0, elementUuid, "{9d04…}")   // -> 1, or -1 if not found

A terminal's uuid comes from its symbol, so every placed copy of one symbol has the same ones: it names a terminal only together with its element. Hold the uuid, and turn it into the index when calling:

var a2 = qet.terminalIndex(0, coil, "{9d04…}");
qet.addConductor(0, coil, a2, lamp, qet.terminalIndex(0, lamp, "{77aa…}"));

qet_element_info in the MCP server lists the uuids of a symbol file's terminals. Every terminal of an opened project has one (see the project database, section 4).

Find a sheet, text, shape, picture, table or symbol text by its uuid

Most calls name things by position: sheet 2, text 0, table 1. Positions shift when something before them is added or removed, so a script that deletes table 0 and then moves "table 1" moves the wrong one. Hold the uuid instead, and turn it into the current position when you need it:

qet.folioUuid(index)                           // -> "{…}", or ""
qet.folioIndex(uuid)                           // -> current index, or -1

qet.textIndex(folioIndex, uuid)                // a free text, in qet.texts(folioIndex)
qet.shapeIndex(folioIndex, uuid)               // a shape, in qet.shapes(folioIndex)
qet.imageIndex(folioIndex, uuid)               // a picture, in qet.images(folioIndex)
qet.tableIndex(folioIndex, uuid)               // a table, in qet.tables(folioIndex)
qet.elementTextIndex(folioIndex, elementUuid, textUuid)
                                               // a symbol's text field, in qet.elementTexts(folioIndex, elementUuid)

Each returns -1 when there is no such item.

  • Symbol text fields take the symbol too, because copies of a symbol keep their fields' uuids β€” in 2612_ats_singlephase.qet one field uuid appears on 20 copies.
  • Older files work too. Most shipped examples save no sheet uuid; a sheet gets one on load, the same on every load, written on the next save.

Since PR #1098 (tables, symbol text fields) and PR #1114 (sheets), both merged 2026-09-28. The MCP server's qet_items lists every drawn item with its uuid.

A sheet's wire defaults

qet.setConductorDefault(folioIndex, property, value)   // -> false if refused
qet.conductorDefault(folioIndex, property)             // -> "" if unknown

The defaults in PropriΓ©tΓ©s du folio (Sheet properties): what wires drawn later on the sheet start from, and the sheet-wide option activer l'option un texte par potentiel, named onetextperfolio ("true" / "false"). The other property names are the ones setConductorProperty() takes. A folioIndex of -1 means the project's defaults, which each sheet added later copies.

for (var i = 0; i < qet.folioCount(); i++)
    qet.setConductorDefault(i, "onetextperfolio", "true");   // one number per potential
qet.setConductorDefault(-1, "onetextperfolio", "true");      // and on folios added later

Changing onetextperfolio shows or hides the wire numbers at once, so an export later in the same script draws them right. There is no undo for these, the same as in the dialogs.

Where a symbol, a terminal or a wire is

qet.elementGeometry(folioIndex, elementUuid)
// -> {x, y, rotation, left, top, right, bottom}
qet.terminalPosition(folioIndex, elementUuid, terminalIndex)
// -> {x, y, facing}   facing is "n", "e", "s" or "w"
qet.conductorPath(folioIndex, conductorUuid)
// -> [{x, y}, {x, y}, …]

All three return folio coordinates (the same pixels setElementPosition() takes), and an empty result when there is no such item.

  • elementGeometry(): x, y are the symbol's origin, what setElementPosition() sets. left/top/right/bottom are the box it covers on the sheet, rotation included. Use it to lay one symbol out next to another, or to check a move landed.
  • terminalPosition(): the point where a wire meets the terminal, and which way the wire leaves, the symbol's rotation included. A wire between two terminals that face each other is straight exactly when their x are equal (terminals facing n/s) or their y are equal (e/w). So a script can line a symbol up before it draws the wire.
  • conductorPath(): a wire's drawn path, by the wire's uuid (from qet.conductorUuids()). The first point is at the wire's first end (conductorEnds()[0]), the last at its second; the points between are its corners.

Example: put a second coil 40 px under a first one, its top terminal (A1, index 0) in line with the first coil's bottom one (A2, index 1), so the wire between them is straight:

var f = qet.currentFolio();
var path = "common://10_electric/10_allpole/310_relays_contactors_contacts/01_coils/bobine3.elmt";
var k1 = qet.addElement(f, path, 200, 100);
var a = qet.terminalPosition(f, k1, 1);              // {x: 200, y: 120, facing: "s"}
var k2 = qet.addElement(f, path, 0, 0);
var b = qet.terminalPosition(f, k2, 0);              // facing "n"
qet.moveElement(f, k2, a.x - b.x, a.y + 40 - b.y);   // same x, 40 px lower
qet.addConductor(f, k1, 1, k2, 0);                   // a straight wire

Check which way a terminal faces before relying on it: some symbols have both terminals on one side.

Added in PR #1264 (merged 2026-10-03). The MCP server's place_element and align_terminal operations do this for an assistant.

A wire's path: segments, and routing around symbols

addConductor() draws a wire the way the editor does when you join two terminals: two or three straight segments, which can run straight through another symbol. These calls change the path of one wire, not the whole potential:

qet.conductorSegments(folioIndex, elementUuid, terminalIndex)
// -> ["0: (120,160)-(120,170) vertical static", "1: (120,170)-(300,170) horizontal movable", …]
qet.moveConductorSegment(folioIndex, elementUuid, terminalIndex, segmentIndex, dx, dy)

qet.routeConductor(folioIndex, elementUuid, terminalIndex)
qet.routeConductorBetween(folioIndex, elementUuidA, terminalIndexA,
                                      elementUuidB, terminalIndexB)
// -> "routed", "no-route", or "" if there is no such wire
  • conductorSegments() lists the wire's segments. A static segment is the short one fixed to a terminal; it has no handle in the editor and cannot be moved.
  • moveConductorSegment() moves one segment, like dragging its handle: only across its own direction, so dx moves a vertical segment and dy a horizontal one; the other value is ignored.
  • routeConductor() redraws the wire so it goes around the symbols in its way, in horizontal and vertical runs on the 10 px grid, inside the sheet's frame. Every symbol's box counts as an obstacle (its texts do not), except a symbol drawn around one of the wire's own ends, such as a frame. Other wires are not obstacles, but the route avoids running along or across them where it can. The new path is one undo step and is saved with the project.
  • routeConductorBetween() does the same for the wire joining two given terminals. Use it right after addConductor() onto a terminal that already has a wire: the other calls name a wire by one terminal, and refuse a terminal carrying two.
  • "no-route" is not a failure: there was no way round, the wire keeps the path it had (still joining the right terminals), and the reason goes to the log.

Route after everything is placed: moving a symbol later stretches the routed path, it does not route it again.

var f = qet.currentFolio();
qet.addConductor(f, k1, 0, k2, 1);
var r = qet.routeConductorBetween(f, k1, 0, k2, 1);
if (r !== "routed") qet.log("left as drawn: " + r);

Added in PR #1245, with fixes in #1256 and #1258 (all merged 2026-10-02).

The sheet's frame, and paper sizes

The frame round a sheet is a grid of columns and rows (the PropriΓ©tΓ©s du folio, Sheet properties, dialog sets it):

qet.folioBorder(folioIndex, property)          // -> the value as text
qet.setFolioBorder(folioIndex, property, value) // one undo step
qet.folioPresets()                              // -> ["a0-portrait", "a0-landscape", …]
Property Value
columns, rows how many, 1 to 99
column-width, row-height size in pixels, 1 to 1000
display-columns, display-rows "true" / "false": show the headers
preset write only: a sheet of paper, such as "a3-landscape"
width, height read only: the frame and title block together, in pixels

preset takes a0 to a5, letter, legal, tabloid or ledger (the same 11 Γ— 17 in sheet), each followed by -portrait or -landscape. It chooses whole-number column and row counts and sizes that fill that sheet without going over it, allowing for the title block, and keeps the cells as close to their current size as it can. A PDF export of the sheet then comes out on that paper size.

for (var i = 0; i < qet.folioCount(); i++)
    qet.setFolioBorder(i, "preset", "a3-landscape");
qet.log(qet.folioBorder(0, "width") + " x " + qet.folioBorder(0, "height"));

Added in PR #1250 (merged 2026-10-02).

House style

qet.houseStyle()          // -> the text, or "" if none was set
qet.setHouseStyle(text)   // always true

Your own drawing rules, written once in plain words: grid, which way the circuit flows, how wires are routed, where tags go. QElectroTech keeps the text in its settings, the same for every project, and every AI assistant that connects reads it (see Connecting an AI assistant). QElectroTech itself does nothing else with it.

Added in PR #1315 (merged 2026-10-05).

Navigate and message

qet.selectElement(elementUuid)   // scene state; works headless, no view needed
qet.deselectAll(folioIndex)
qet.zoomFit()                    // -> false headless (no view to act on)
qet.zoomToContent()
qet.zoomReset()
qet.showMessage("Done.")         // a modal info box; safe headless (auto-dismissed)

Deliberately not here: triggering an arbitrary menu action by name. A script that could invoke any QAction could just as easily open a modal dialog with nobody there to dismiss it β€” a real, previously-hit hang class in this codebase (see the discussion on #882). Every method above is either non-blocking by construction, or β€” for showMessage β€” safe under QET's existing non-interactive mode, which is already on for the whole process before any headless script runs.

Which sheet is on screen, and what calls exist

var f = qet.currentFolio()   // index of the sheet on screen; 0 headless; -1 if the project has none
qet.apiSignatures()          // every call this build offers, with its arguments

currentFolio() lets a script act where the user is looking, which a script button needs. apiSignatures() comes from the running build itself, so a list made from it (as the MCP server's qet_script_api does) cannot drift from what the build really has.

Logging

qet.log("anything you want in stderr")

A script has no console of its own; this is how you see output, including under --run.


Every call, at a glance

The sections above explain the calls most scripts need. This is the whole list in current development builds, by what they act on. For the arguments of any of them, run qet.log(qet.apiSignatures().join("\n")): the list comes from your own build, so it is always right for it.

Acts on Calls
the project and its sheets projectTitle, setProjectTitle, filePath, folioCount, currentFolio, folioTitle, setFolioTitle, folioUuid, folioIndex, addFolio, insertFolio, removeFolio, folioProperty, setFolioProperty, folioBorder, setFolioBorder, folioPresets, titleBlockTemplates, embedTitleBlockTemplate
export and save exportPdf, exportPng, exportSvg, exportDxf, exportCables, exportWires, exportBom, exportWiring, exportNets, exportLinks, exportInfo, setTitleBlock, save
symbols elementCount, elementUuids, elementName, addElement, setElementPosition, moveElement, rotateElement, deleteElement, duplicateElements, elementGeometry, elementInfo, setElementInfo, elementLabel, setElementLabel
terminals elementTerminals, terminalIndex, terminalPosition
a symbol's text fields elementTexts, elementTextIndex, addElementText, elementTextProperty, setElementTextProperty, elementTextGeometry, deleteElementText
wires conductorCount, conductors, conductorUuids, conductorEnds, addConductor, deleteConductor, conductorProperty, setConductorProperty, conductorDefault, setConductorDefault
a wire's path conductorPath, conductorSegments, moveConductorSegment, routeConductor, routeConductorBetween
cross-references and PLC elementLinkType, linkedElements, linkElements, unlinkElement, elementLinkGroupIndex, plcIOs, addPlcIO, setPlcIO, removePlcIO
free texts texts, textIndex, addText, textContent, setTextContent, setTextColor, setTextRotation, deleteText
shapes shapes, shapeIndex, addShape, shapeProperty, setShapeProperty, deleteShape, addPolygon, shapePolygon, setShapePolygon, addPath, shapePathNodes, setShapePathNodes, setShapeClosed
pictures images, imageIndex, addImage, addPdfPage, setImageScale, setImageRotation, cropImage, imageCrop, deleteImage
tables on a sheet tables(folioIndex), tableIndex, addTable, setTablePosition, deleteTable
terminal strips terminalStrips, addTerminalStrip, removeTerminalStrip, addTerminalToStrip, stripRealTerminals, groupTerminals, bridgeTerminals, sortTerminalStrip
auto-numbering autoNums, addAutoNum, removeAutoNum, renameAutoNum, useConductorAutoNum, useElementAutoNum, numberElement, renumberElementAutoNum, freeElementNumbers, assignElementNumber, assignElementAutoNum
the project database tables(), query, queryError (read-only SELECT; see the project database)
checks, search checkContinuity, searchAndReplace
undo undo, redo, canUndo, canRedo
the window selectElement, deselectAll, selectedElements, zoomFit, zoomToContent, zoomReset, showMessage
this installation houseStyle, setHouseStyle, log, apiSignatures

tables has two forms: with no argument it lists the database's tables and views, with a sheet index the tables drawn on that sheet.


Worked examples

Export everything after a revision (the issue's original ask)

// bump_and_export.js
qet.setTitleBlock("", ["revision=" + qet.projectTitle(), "date=today"]);
qet.save("");
qet.exportPdf(qet.filePath().replace(".qet", ".pdf"));
qet.exportBom(qet.filePath().replace(".qet", "_bom.csv"));
qet.log("Exported revision for " + qet.projectTitle());
qelectrotech --run bump_and_export.js panel.qet

CI: fail the build if a project won't open or export

#!/bin/bash
set -e
for f in projects/*.qet; do
  qelectrotech --run ci_check.js "$f"
done
// ci_check.js
if (qet.folioCount() === 0) {
  qet.log("ERROR: no folios in " + qet.filePath());
  throw new Error("empty project");
}
var ok = qet.exportPdf("/tmp/check.pdf");
if (!ok) throw new Error("PDF export failed");

A thrown error exits 1 and CI fails the step β€” no separate exit-code plumbing needed.

Place several elements from a list, then save

var placements = [
  ["embed://…/relay.elmt",    100, 100],
  ["embed://…/contactor.elmt", 200, 100],
  ["embed://…/breaker.elmt",   300, 100],
];
for (var i = 0; i < placements.length; i++) {
  var p = placements[i];
  var uuid = qet.addElement(0, p[0], p[1], p[2]);
  if (!uuid) qet.log("failed to place: " + p[0]);
}
qet.save("");

Errors and exit codes (headless)

Situation Exit code
Script ran to completion 0
Script threw an uncaught exception 1
Project failed to open 1
Missing script.js or project.qet argument 2
Script or project file not found 2
Scripting is off (no setting, no QET_ENABLE_SCRIPTING=1) 3

An uncaught exception is reported as Script error: <path>:<line>: <message> on stderr.


Limitations, on purpose

Same three lines as the class-level scope in the source, kept here so a change of mind shows up in one obvious place:

  • No arbitrary GUI actions. See the "Navigate and message" section above for exactly why.
  • Export methods don't see unsaved edits without an explicit save() first β€” see the callout above.
  • This is not a plugin system. No script runs automatically, on open or otherwise; every run is explicit, either a --run invocation or a menu click. There is still no loadable-module directory and no way for a project file to carry or trigger a script β€” see Automating QElectroTech for the file-format boundary this respects.

See also

  • CLI Reference β€” the export flags this feature wraps
  • Automating QElectroTech β€” the file-format and headless export ground this builds on
  • MCP server β€” an AI assistant driving this engine through qet_edit/qet_query
  • Script buttons β€” store a script and run it from a button
  • Live mode β€” an assistant running scripts in the open project
  • Macro recorder β€” record a task by hand for an assistant to script
  • PR #891 β€” implementation, with the exact tests run against it
  • Issue #162 β€” the original request and design discussion
Getting Started

Home

🌐 Languages β€” English Β· FranΓ§ais Β· Deutsch

Downloads

Windows without admin rights β€” the portable archive, no installer

Quick Start Guide

User Manual

FAQ

Tips & Tricks

Guides

Conductors β€” wire properties, what feeds which export, cables, and hops where wires cross

Wires per terminal β€” limit the wires on a terminal, chain wiring instead of stars

Printing and exporting β€” paper, PDF, images, and what each path does differently

Linking elements β€” master, slave, terminal

PLC modules β€” I/O tables and linking a wire to a specific point

Using the element editor β€” drawing tools, saving, checks

Grid size and element size β€” why symbols aren't all the same scale, and scaling one without leaving the grid

Preferences reference β€” what each settings page does

Saving and loading settings β€” your whole setup in one file, to copy or keep

Keyboard-only control β€” mouseless QET, and what still needs a mouse

Mouse modifiers β€” what Shift, Ctrl and Alt change while you drag

3D mouse β€” SpaceMouse pan, zoom and buttons

Aligning items β€” snap symbols back to the grid, or line them up

Pictures on a sheet β€” labels, crop, transparency, what they cost in the file

Arcs and curved wires β€” the Arc tool, pulling an arc in or out, rounding a corner with a fillet, dashed arcs for lighting layouts

Grouping items β€” select, move and copy several items as one

Finding your place on a sheet β€” go to a cell like B13 or 4-B7, keep the headers in sight, show the cell limits, zoom and pan

Showing and hiding kinds of items β€” hide texts, wire numbers, shapes, pictures, tables or cross-references on every sheet

Drawing faster β€” place without dragging, the S shortcut bar, command search, gestures

Customising QElectroTech β€” keys, toolbar size and contents, the gesture ring (partly pending)

Managing collections β€” folders, writability, building your own shortlist

Templates β€” reusable multi-element blocks, placed by double-click or drag

Search & Replace β€” bulk property changes

Building a nomenclature query β€” the BOM/summary table builder

Linking wires across pages β€” sheet reports

Variables & formulas β€” %f, %{label}, sequences

Auto-numbering β€” schemes, sequences, freezing

Terminal strips β€” strips, levels, bridges

Title block templates β€” the .titleblock format

Importing EPLAN parts (.edz) β€” EPLAN Data Portal

DXF import & export β€” two unrelated features, one format; command-line export and layers

The project database β€” the in-memory SQLite cache

File formats
Elements XML
Project XML
Development

Building from Source

Contributing Code

Automating QET β€” CLI, XML formats, external tools

CLI Reference β€” command line usage

JavaScript Scripting β€” --run, geometry editing, undo

MCP server β€” let an AI assistant read, verify and edit projects

Connecting an AI assistant β€” setup for Claude, Copilot, Gemini, Codex, Cursor, LM Studio

Script buttons β€” stored scripts with an icon, by hand or by an assistant

Live mode β€” an assistant working in the open project while you watch

Macro recorder β€” record a task by hand, for an assistant to script

Development Roadmap

Vision β€” proposal, under discussion

Developer Tools

About

Features

History

Community

License

Contributing to this Wiki

Clone this wiki locally