Primeros pasos
En npmEngine
The 2D drafting engine: AutoCAD's commands, prompts, keywords and object snaps, on a canvas or headless.
A 2D drafting engine modelled on AutoCAD: its commands, aliases, prompts, keywords and object
snaps. It has no framework and no DOM outside adapters/: CadEngine binds it to a canvas, and
everything else — the runtime, the tools, the scene — runs headless, in a browser, on a server or
in a test.
It is the package @mellowcad/engine, ESM only:
npm install @mellowcad/engineEntry points
| Import | What |
|---|---|
@mellowcad/engine | the engine; English is built in |
@mellowcad/engine/locales/<tag> | one language each: en, es, ar |
Nothing else is public: they are the package's exports (cad/package.json), no other path under
cad/ is part of the API, and cad/entryPoints.test.ts fails if the app reaches one. What they
export, signature by signature, is cad/api.md. No module
has an import-time side effect ("sideEffects": false, one built module per source file), so a
bundler keeps only what a page uses, and a language costs nothing until it is imported.
On a page
import { CadEngine, createMemoryDrawingStore } from "@mellowcad/engine"
const engine = new CadEngine(canvas, { store: createMemoryDrawingStore() })
engine.subscribe(() => render(engine.getState()))
engine.dispose() // when the canvas goesThe defaults are safe on any page:
store, where the drawing is autosaved and restored from: none unless given. A change still waiting to be written is written ondispose()and as the page is hidden or left.createMemoryDrawingStore()keeps it while the page lives; aDrawingStoreof the host's (read,write) keeps it anywhere.createLocalStorageDrawingStore()is this device's, the editor's drawing: an engine given it shares, and overwrites, what the editor saved.scope, what the engine takes keys, the wheel and the right-click from: its canvas unless given, and only once it is pressed or focused, until something outside is. Pass the element that also holds the host's controls for the canvas, or"document"to take the whole page wherever focus sits, as the editor does.
Without a browser
CadDriver drives the same engine with no canvas, as a user would: commands typed, clicks in world
coordinates, keys. It runs in Node, on a server or in a test:
import { writeFileSync } from "node:fs"
import { CadDriver, SvgScenePresenter } from "@mellowcad/engine"
// The engine with no browser: typed commands and clicks in world
// coordinates, as a user would draw them. SAVE hands the drawing file to the
// host's transport, which here writes it to disk.
const cad = new CadDriver({
files: {
save: (text, fileName) => writeFileSync(fileName, text),
open: async () => null,
},
})
cad.command("RECTANG").click({ x: 0, y: 0 }).click({ x: 200, y: 100 })
cad.command("CIRCLE").click({ x: 100, y: 50 }).type("30").enter()
cad.command("SAVE")
// What the canvas would show, as an SVG document.
writeFileSync("drawing.svg", new SvgScenePresenter().render(cad.scene()))
console.log(cad.commandLog().join("\n"))
cad.dispose()That is examples/engine-node, run with node main.ts. The package's
declarations name DOM types (CadEngine takes a canvas): a TypeScript project that checks them
(skipLibCheck off) has "dom" in its lib, even in Node.
React bindings are @mellowcad/react, and the whole editor is
@mellowcad/editor.
Languages
Two languages in one
As in AutoCAD's language packs, a language has two parts:
- the interface (
interface): what a host shows around the drawing — tool labels, palette rows, the names of snaps, colours, ByLayer, views and the ViewCube's faces; - the command language (
commands): command names and aliases, prompts, keywords, command-line messages, and everything drawn on the canvas.
Spanish has both, from Spanish AutoCAD 2026 (LINEA, DESFASE, [Cerrar/desHacer]). Arabic has
only the interface: there is no Arabic AutoCAD, so its command language is the global one, as for
whoever drafts in Arabic in the English product. In every language _ and a global name or
keyword reaches the global vocabulary (_LINE, _C), as AutoCAD scripts rely on; a keyword id
with a space is typed as one word there (_LINESPACING for MTEXT's Line spacing).
The command language is always drawn left to right; state.commandLine.lang is its BCP 47 tag, for
the lang of whatever shows it.
Choosing one
A language is passed, never registered, so one process can draw in several at once:
import { CadEngine, CadDriver, commandCatalog } from "@mellowcad/engine"
import { ar } from "@mellowcad/engine/locales/ar"
import { es } from "@mellowcad/engine/locales/es"
const engine = new CadEngine(canvas, { locale: es })
engine.setLocale(ar) // rewords the state and an open prompt; the scrollback stays as written
new CadDriver({ locale: es }) // headless
commandCatalog(es) // names, aliases and descriptions, in that languagerecordDemo(script, { locale }) and engineText(locale) take it too. Load the module only where
it is drawn (import("@mellowcad/engine/locales/es")): English needs none.
The catalog
cad/locales/en.json is the reference: its keys are every word the engine says, and its type is
the type of every other language (EngineLocale).
interface EngineLocale {
tag: string // BCP 47, for Intl: plurals, and digits pinned to latin
interface: DeepPartial<InterfaceCatalog>
commands?: DeepPartial<CommandCatalog>
}interfacemay leave out any key: that key is shown in English, and only that key.commandsis complete or absent. Without it, the command language is the global one: English's names, aliases, prompts and keywords. With it, the language should name everything — the engine's own languages are tested for that — because what it leaves out degrades:- a command it does not name is reached by its global name alone (
ERASE, neverE): English's aliases are English'sacad.pgp, and another language gives the same letters to other commands (Ujoins in Spanish); - a keyword it does not name is its global word (
Close); - any other message is English's.
- a command it does not name is reached by its global name alone (
namesmaps a global name to the language's name and then its aliases ("OFFSET": ["DESFASE", "DF", "EQ"]). No two commands may share one.keywordsgives each command's keywords by their global id, the abbreviation in capitals anywhere in the word (desHacer→H), as AutoCAD'sinitgetdoes. It is typed and confirmed with Enter or Space, never run on a letter alone.keywordLabelsgives a keyword the gloss AutoCAD lists it with (Ttr (tan tan radius)), or the form it lists a keyword declared otherwise in (TLfor TEXT'sTLeft), shown in the prompt's list, its<default>and on its option; the word typed and named when AutoCAD asks which was meant stays the keyword. A language labels only what English labels, and lists English's label where it gives none.promptKeywordsspells a keyword otherwise at one prompt, by the prompt's id: each AutoCAD prompt declares its own, so Spanish FILLET lists[RAdio]butDesigne polilínea 2D o [Radio]:. A keyword it leaves out is spelled as inkeywords.- A linetype's or hatch pattern's name is drawing data and may be left out in every language.
Messages are compiled by the host
Messages are ICU MessageFormat — next-intl's and FormatJS's syntax: {count, plural, one {…} other {…}}, {name}, a <keywords> tag. The engine ships only icu-minify's formatter (under 1 KB) and
no parser: a parser is FormatJS's, too heavy to send to a browser for messages that never
change at runtime. So a catalog holds compiled messages, and whatever compiles them runs at build
time:
import compile from "icu-minify/compile" // in the host's build, never in its bundle
const fr: EngineLocale = {
tag: "fr",
interface: { byLayer: "DuCalque" }, // no arguments: a plain string is already compiled
commands: {
names: { LINE: ["LIGNE", "L"] /* …every command */ },
messages: { unknownCommand: compile('Commande "{name}" inconnue.') },
},
}A message takes the arguments English's takes. Lengths and angles arrive already formatted, with a
point and latin digits, as AutoCAD writes them in every language; counts are formatted by the
engine, latin digits and no grouping. In ICU a < followed by a letter is a tag: write '<'.
The engine's own catalogs are compiled the same way (npm run locales writes
cad/locales/<tag>.ts from <tag>.json).
Replacing a few words
extendLocale lays a host's words over a language, key by key, and returns a new one:
import compile from "icu-minify/compile" // in the host's build, as above
import { extendLocale } from "@mellowcad/engine"
import { es } from "@mellowcad/engine/locales/es"
const spanish = extendLocale(es, {
interface: { tool: { line: "Línea recta" } },
commands: {
messages: { unknownCommand: compile('No existe el comando "{name}".') },
},
})What it gives wins; everything else is the base's, and the base is untouched. A list of names is
replaced whole. Giving commands to a language without a command language (Arabic) gives it one,
with the rules above — a command it does not name loses its aliases. Use en from
@mellowcad/engine/locales/en as the base to reword English.
Names a host shows
What the engine names, the host reads from it and never copies: EngineState.names
(EngineNames) has, in the interface language,
| Field | Keyed by | Where a host shows it |
|---|---|---|
snaps | SnapType | snap menus, the status bar |
colors | STANDARD_COLORS[].id | colour pickers |
byLayer | — | colour, linetype and lineweight pickers |
linetypes | LINETYPES[].id | linetype pickers (a missing one is its id) |
views | ViewPreset, and custom | the view menu (state.view.preset) |
viewCubeFaces | ViewCubeFace | the ViewCube (state.viewCube) |
compass | CompassLetter | the ViewCube's compass |
Tool labels and aliases are state.tools; command descriptions are commandCatalog(); property
rows come worded in state.properties.info. The shortcut menu a right-click opens
(state.shortcutMenu) brings the prompt's options worded in the command language
(option.label) and the command Repeat runs (repeat); its fixed entries come as ids (action),
and over dimensions alone the style table's names (dimensionStyle, chosen with
chooseShortcutMenuDimStyle). The host's own chrome — its buttons, its menus' other items — is
the host's to translate.
Versioning
The catalog's shape is public API: EngineLocale is typed from en.json, and a host's catalog is
written against it. Renaming, moving or removing a key, changing a message's arguments or tags, or
changing what a branch holds is a breaking change, a major version. Adding a key is not: a
host's language falls back to English for it, or to the global name for a new command.
License
MIT (LICENSE): the code, not the MellowCAD name or logo.
AutoCAD is a registered trademark of Autodesk, Inc. MellowCAD is an independent project, not affiliated with or endorsed by Autodesk; AutoCAD is named only to describe what it is compatible with.