@chanmeng666/archlang 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\r\n/** ArchLang CLI: `arch compile <in.arch> [-o out.svg]`, `arch watch <in.arch>`. */\r\n\r\nimport { readFileSync, writeFileSync, watchFile } from \"node:fs\";\r\nimport { resolve } from \"node:path\";\r\nimport { compile, formatDiagnostic } from \"./index.js\";\r\n\r\nfunction compileFile(input: string, output: string, width?: number): boolean {\r\n let source: string;\r\n try {\r\n source = readFileSync(input, \"utf8\");\r\n } catch {\r\n process.stderr.write(`error: cannot read ${input}\\n`);\r\n return false;\r\n }\r\n const { svg, diagnostics } = compile(source, { width, noCache: true });\r\n for (const d of diagnostics) {\r\n process.stderr.write(`${formatDiagnostic(source, d)}\\n\\n`);\r\n }\r\n const errorCount = diagnostics.filter((d) => d.severity === \"error\").length;\r\n if (errorCount > 0) {\r\n process.stderr.write(`✗ compilation failed (${errorCount} error${errorCount > 1 ? \"s\" : \"\"})\\n`);\r\n return false;\r\n }\r\n writeFileSync(output, svg, \"utf8\");\r\n process.stdout.write(`✓ ${input} → ${output} (${svg.length} bytes)\\n`);\r\n return true;\r\n}\r\n\r\nfunction parseArgs(argv: string[]): { _: string[]; o?: string; width?: number } {\r\n const res: { _: string[]; o?: string; width?: number } = { _: [] };\r\n for (let i = 0; i < argv.length; i++) {\r\n const a = argv[i];\r\n if (a === \"-o\" || a === \"--out\") res.o = argv[++i];\r\n else if (a === \"-w\" || a === \"--width\") res.width = Number(argv[++i]);\r\n else res._.push(a);\r\n }\r\n return res;\r\n}\r\n\r\nfunction defaultOut(input: string): string {\r\n return input.replace(/\\.arch$/i, \"\") + \".svg\";\r\n}\r\n\r\nfunction main(): void {\r\n const [cmd, ...rest] = process.argv.slice(2);\r\n const args = parseArgs(rest);\r\n\r\n if (!cmd || cmd === \"help\" || cmd === \"--help\" || cmd === \"-h\") {\r\n process.stdout.write(\r\n `arch — ArchLang compiler\\n\\n` +\r\n `Usage:\\n` +\r\n ` arch compile <in.arch> [-o out.svg] [-w width]\\n` +\r\n ` arch watch <in.arch> [-o out.svg] [-w width]\\n`,\r\n );\r\n process.exit(cmd ? 0 : 1);\r\n }\r\n\r\n const input = args._[0];\r\n if (!input) {\r\n process.stderr.write(\"error: missing input file\\n\");\r\n process.exit(1);\r\n }\r\n const output = args.o ? resolve(args.o) : defaultOut(resolve(input));\r\n\r\n if (cmd === \"compile\") {\r\n process.exit(compileFile(resolve(input), output, args.width) ? 0 : 1);\r\n } else if (cmd === \"watch\") {\r\n compileFile(resolve(input), output, args.width);\r\n process.stdout.write(`watching ${input} … (Ctrl+C to stop)\\n`);\r\n watchFile(resolve(input), { interval: 300 }, () => compileFile(resolve(input), output, args.width));\r\n } else {\r\n process.stderr.write(`error: unknown command \"${cmd}\"\\n`);\r\n process.exit(1);\r\n }\r\n}\r\n\r\nmain();\r\n"],"mappings":";;;;;;;AAGA,SAAS,cAAc,eAAe,iBAAiB;AACvD,SAAS,eAAe;AAGxB,SAAS,YAAY,OAAe,QAAgB,OAAyB;AAC3E,MAAI;AACJ,MAAI;AACF,aAAS,aAAa,OAAO,MAAM;AAAA,EACrC,QAAQ;AACN,YAAQ,OAAO,MAAM,sBAAsB,KAAK;AAAA,CAAI;AACpD,WAAO;AAAA,EACT;AACA,QAAM,EAAE,KAAK,YAAY,IAAI,QAAQ,QAAQ,EAAE,OAAO,SAAS,KAAK,CAAC;AACrE,aAAW,KAAK,aAAa;AAC3B,YAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,CAAC,CAAC;AAAA;AAAA,CAAM;AAAA,EAC3D;AACA,QAAM,aAAa,YAAY,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO,EAAE;AACrE,MAAI,aAAa,GAAG;AAClB,YAAQ,OAAO,MAAM,8BAAyB,UAAU,SAAS,aAAa,IAAI,MAAM,EAAE;AAAA,CAAK;AAC/F,WAAO;AAAA,EACT;AACA,gBAAc,QAAQ,KAAK,MAAM;AACjC,UAAQ,OAAO,MAAM,UAAK,KAAK,WAAM,MAAM,KAAK,IAAI,MAAM;AAAA,CAAW;AACrE,SAAO;AACT;AAEA,SAAS,UAAU,MAA6D;AAC9E,QAAM,MAAmD,EAAE,GAAG,CAAC,EAAE;AACjE,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,MAAM,QAAQ,MAAM,QAAS,KAAI,IAAI,KAAK,EAAE,CAAC;AAAA,aACxC,MAAM,QAAQ,MAAM,UAAW,KAAI,QAAQ,OAAO,KAAK,EAAE,CAAC,CAAC;AAAA,QAC/D,KAAI,EAAE,KAAK,CAAC;AAAA,EACnB;AACA,SAAO;AACT;AAEA,SAAS,WAAW,OAAuB;AACzC,SAAO,MAAM,QAAQ,YAAY,EAAE,IAAI;AACzC;AAEA,SAAS,OAAa;AACpB,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC;AAC3C,QAAM,OAAO,UAAU,IAAI;AAE3B,MAAI,CAAC,OAAO,QAAQ,UAAU,QAAQ,YAAY,QAAQ,MAAM;AAC9D,YAAQ,OAAO;AAAA,MACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAIF;AACA,YAAQ,KAAK,MAAM,IAAI,CAAC;AAAA,EAC1B;AAEA,QAAM,QAAQ,KAAK,EAAE,CAAC;AACtB,MAAI,CAAC,OAAO;AACV,YAAQ,OAAO,MAAM,6BAA6B;AAClD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,SAAS,KAAK,IAAI,QAAQ,KAAK,CAAC,IAAI,WAAW,QAAQ,KAAK,CAAC;AAEnE,MAAI,QAAQ,WAAW;AACrB,YAAQ,KAAK,YAAY,QAAQ,KAAK,GAAG,QAAQ,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA,EACtE,WAAW,QAAQ,SAAS;AAC1B,gBAAY,QAAQ,KAAK,GAAG,QAAQ,KAAK,KAAK;AAC9C,YAAQ,OAAO,MAAM,YAAY,KAAK;AAAA,CAAuB;AAC7D,cAAU,QAAQ,KAAK,GAAG,EAAE,UAAU,IAAI,GAAG,MAAM,YAAY,QAAQ,KAAK,GAAG,QAAQ,KAAK,KAAK,CAAC;AAAA,EACpG,OAAO;AACL,YAAQ,OAAO,MAAM,2BAA2B,GAAG;AAAA,CAAK;AACxD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\r\n/** ArchLang CLI: `arch compile <in.arch> [-o out.svg]`, `arch watch <in.arch>`. */\r\n\r\nimport { readFileSync, writeFileSync, watchFile } from \"node:fs\";\r\nimport { resolve as resolvePath } from \"node:path\";\r\nimport { compile, formatDiagnostic, toDxf, toPdf } from \"./index.js\";\r\n\r\ntype Format = \"svg\" | \"dxf\" | \"pdf\";\r\n\r\nasync function compileFile(input: string, output: string, format: Format, width?: number): Promise<boolean> {\r\n let source: string;\r\n try {\r\n source = readFileSync(input, \"utf8\");\r\n } catch {\r\n process.stderr.write(`error: cannot read ${input}\\n`);\r\n return false;\r\n }\r\n const { svg, diagnostics, scene } = compile(source, { width, noCache: true });\r\n for (const d of diagnostics) {\r\n process.stderr.write(`${formatDiagnostic(source, d)}\\n\\n`);\r\n }\r\n const errorCount = diagnostics.filter((d) => d.severity === \"error\").length;\r\n if (errorCount > 0 || !scene) {\r\n process.stderr.write(`✗ compilation failed (${errorCount} error${errorCount > 1 ? \"s\" : \"\"})\\n`);\r\n return false;\r\n }\r\n\r\n if (format === \"dxf\") {\r\n const dxf = toDxf(scene);\r\n writeFileSync(output, dxf, \"utf8\");\r\n process.stdout.write(`✓ ${input} → ${output} (${dxf.length} bytes, DXF)\\n`);\r\n return true;\r\n }\r\n if (format === \"pdf\") {\r\n try {\r\n const pdf = await toPdf(scene);\r\n writeFileSync(output, pdf);\r\n process.stdout.write(`✓ ${input} → ${output} (${pdf.length} bytes, PDF)\\n`);\r\n return true;\r\n } catch (e) {\r\n process.stderr.write(`error: ${(e as Error).message}\\n`);\r\n return false;\r\n }\r\n }\r\n writeFileSync(output, svg, \"utf8\");\r\n process.stdout.write(`✓ ${input} → ${output} (${svg.length} bytes)\\n`);\r\n return true;\r\n}\r\n\r\nfunction parseArgs(argv: string[]): { _: string[]; o?: string; width?: number; format?: string } {\r\n const res: { _: string[]; o?: string; width?: number; format?: string } = { _: [] };\r\n for (let i = 0; i < argv.length; i++) {\r\n const a = argv[i];\r\n if (a === \"-o\" || a === \"--out\") res.o = argv[++i];\r\n else if (a === \"-w\" || a === \"--width\") res.width = Number(argv[++i]);\r\n else if (a === \"-f\" || a === \"--format\") res.format = argv[++i];\r\n else res._.push(a);\r\n }\r\n return res;\r\n}\r\n\r\nfunction defaultOut(input: string, format: Format): string {\r\n return input.replace(/\\.arch$/i, \"\") + \".\" + format;\r\n}\r\n\r\nasync function main(): Promise<void> {\r\n const [cmd, ...rest] = process.argv.slice(2);\r\n const args = parseArgs(rest);\r\n\r\n if (!cmd || cmd === \"help\" || cmd === \"--help\" || cmd === \"-h\") {\r\n process.stdout.write(\r\n `arch — ArchLang compiler\\n\\n` +\r\n `Usage:\\n` +\r\n ` arch compile <in.arch> [-o out] [-w width] [-f svg|dxf|pdf]\\n` +\r\n ` arch watch <in.arch> [-o out] [-w width] [-f svg|dxf|pdf]\\n\\n` +\r\n `Formats: svg (default) · dxf (zero-dep) · pdf (needs optional pdfkit + svg-to-pdfkit)\\n`,\r\n );\r\n process.exit(cmd ? 0 : 1);\r\n }\r\n\r\n const fmt = (args.format ?? \"svg\").toLowerCase();\r\n if (fmt !== \"svg\" && fmt !== \"dxf\" && fmt !== \"pdf\") {\r\n process.stderr.write(`error: unknown format \"${args.format}\" (use svg, dxf, or pdf)\\n`);\r\n process.exit(1);\r\n }\r\n const format = fmt as Format;\r\n\r\n const input = args._[0];\r\n if (!input) {\r\n process.stderr.write(\"error: missing input file\\n\");\r\n process.exit(1);\r\n }\r\n const inPath = resolvePath(input);\r\n const output = args.o ? resolvePath(args.o) : defaultOut(inPath, format);\r\n\r\n if (cmd === \"compile\") {\r\n process.exit((await compileFile(inPath, output, format, args.width)) ? 0 : 1);\r\n } else if (cmd === \"watch\") {\r\n await compileFile(inPath, output, format, args.width);\r\n process.stdout.write(`watching ${input} … (Ctrl+C to stop)\\n`);\r\n watchFile(inPath, { interval: 300 }, () => {\r\n void compileFile(inPath, output, format, args.width);\r\n });\r\n } else {\r\n process.stderr.write(`error: unknown command \"${cmd}\"\\n`);\r\n process.exit(1);\r\n }\r\n}\r\n\r\nvoid main();\r\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -243,6 +243,163 @@ interface PlanNode {
243
243
  body: Statement[];
244
244
  }
245
245
 
246
+ /** Pure geometry helpers. All coordinates in millimetres. Deterministic. */
247
+
248
+ interface Bounds {
249
+ minX: number;
250
+ minY: number;
251
+ maxX: number;
252
+ maxY: number;
253
+ }
254
+ interface WallSegment {
255
+ a: Point;
256
+ b: Point;
257
+ thickness: number;
258
+ category: string;
259
+ }
260
+
261
+ /**
262
+ * Backend-neutral **Scene IR** — the keystone drawing intermediate.
263
+ *
264
+ * `resolve()` produces semantic geometry (rooms, walls, openings); this module
265
+ * defines a flat list of *positioned drawing primitives* tagged with a layer and
266
+ * paint. Geometry is computed exactly **once** here (by the elements, lowered via
267
+ * `scene-build.ts`); every backend (SVG, DXF, PDF, …) is then a thin, pure
268
+ * serializer of the same `Scene`. This kills the geometry duplication the
269
+ * string-based `RenderOp` forced (DXF re-deriving door arcs, PDF rasterizing SVG).
270
+ *
271
+ * Prior art: Typst's `Frame`/`FrameItem` (crates/typst-library/src/layout/frame.rs)
272
+ * — a positioned list of drawable items — and D2's `d2target` (a flat, pointer-free
273
+ * render target consumed by independent backends). ArchLang has no nested
274
+ * transforms, so unlike Typst's `Frame` the node list is flat (no sub-frames).
275
+ *
276
+ * Phase v0.7 keeps this deliberately small: line-weight/line-type/named-layer
277
+ * metadata, hatch primitives, and circles are intentionally deferred to Phase v0.9
278
+ * (roadmap §6). Poché stays an SVG `<pattern>` fill string; page chrome (north
279
+ * arrow, scale bar, title block) stays in the backends for now.
280
+ */
281
+
282
+ /**
283
+ * Ordered draw layers. Nodes are bucketed by `layer` and emitted in this order,
284
+ * preserving collection order within a layer — this exactly reproduces the v0.1
285
+ * global draw order (all wall fills, then all wall faces, doors before windows,
286
+ * labels above fills, …). Doubles as the discriminant of {@link SceneNode.layer}.
287
+ */
288
+ declare const RENDER_PASSES: readonly ["floor", "furniture", "wallFill", "wallFace", "doors", "windows", "labels", "dims", "annotations"];
289
+ type RenderPass = (typeof RENDER_PASSES)[number];
290
+ /** Render-derived sizes (in mm), scaled from the drawing's reference dimension. */
291
+ interface RenderSizes {
292
+ refDim: number;
293
+ wallStroke: number;
294
+ thin: number;
295
+ roomFont: number;
296
+ areaFont: number;
297
+ dimFont: number;
298
+ furnFont: number;
299
+ margin: number;
300
+ hatchGap: number;
301
+ }
302
+ /**
303
+ * How a primitive is painted. Strokes/fills carry colours (already theme-resolved
304
+ * and escaped at the serialization boundary); `width`/`dash` carry *raw* numbers
305
+ * that each backend formats. The optional `linecap`/`linejoin`/`fillRule` cover
306
+ * the exact SVG attributes the original element emitters used, so the SVG
307
+ * serializer reproduces today's output byte-for-byte.
308
+ */
309
+ interface Paint {
310
+ /** A colour, `"none"`, or an SVG pattern ref like `"url(#poche)"`. */
311
+ fill?: string;
312
+ stroke?: string;
313
+ /** Raw stroke width in mm (backend applies its own number formatting). */
314
+ width?: number;
315
+ /** `stroke-dasharray` pair in mm (e.g. door swing arc). */
316
+ dash?: [number, number];
317
+ linecap?: "square";
318
+ linejoin?: "miter";
319
+ fillRule?: "nonzero";
320
+ }
321
+ /**
322
+ * A positioned drawing primitive. Coordinates are absolute millimetres in the
323
+ * plan's space (origin top-left, +x right, +y down — SVG convention); backends
324
+ * apply their own transforms (e.g. DXF's Y-flip).
325
+ */
326
+ type ScenePrim =
327
+ /** A filled/stroked closed polygon (room, furniture, column, opening cover, per-segment wall fill). */
328
+ {
329
+ t: "polygon";
330
+ pts: Point[];
331
+ }
332
+ /** A single straight segment (wall face, door leaf, window pane, dimension lines/ticks). */
333
+ | {
334
+ t: "line";
335
+ a: Point;
336
+ b: Point;
337
+ }
338
+ /**
339
+ * A multi-loop closed region drawn as one path (`fill-rule` nonzero), used for
340
+ * unioned orthogonal walls so the poché fills with proper holes and the outline
341
+ * has no internal seams.
342
+ */
343
+ | {
344
+ t: "region";
345
+ loops: Point[][];
346
+ }
347
+ /**
348
+ * A circular arc (door swing). Carries the `center`/`r` a CAD backend needs to
349
+ * emit a native arc, plus the explicit `start`/`end` points + `sweep` flag an
350
+ * SVG `A` command needs — so neither backend re-derives endpoints from trig.
351
+ */
352
+ | {
353
+ t: "arc";
354
+ center: Point;
355
+ r: number;
356
+ start: Point;
357
+ end: Point;
358
+ sweep: 0 | 1;
359
+ }
360
+ /** A text label. `value` is the raw (unescaped) string; backends escape on emit. */
361
+ | {
362
+ t: "text";
363
+ at: Point;
364
+ value: string;
365
+ size: number;
366
+ anchor: "start" | "middle" | "end";
367
+ baseline: "central";
368
+ /** SVG `font-weight` (e.g. 600 for a room name). */
369
+ weight?: number;
370
+ /** Rotation in degrees about `at` (e.g. dimension text along its line). */
371
+ rotate?: number;
372
+ };
373
+ /** One drawable: a primitive on a layer, with paint and an optional source span. */
374
+ interface SceneNode {
375
+ layer: RenderPass;
376
+ prim: ScenePrim;
377
+ paint: Paint;
378
+ span?: Span;
379
+ }
380
+ /**
381
+ * A complete, backend-neutral drawing. `nodes` is the geometry; the remaining
382
+ * fields are the page-level context backends need (viewBox sizing, theme colours
383
+ * for chrome, north/scale/title block, hatch materials in use). Theme is baked
384
+ * into node paint already — it is carried here only for the page chrome.
385
+ */
386
+ interface Scene {
387
+ /** Padded page width/height in mm (drawing extent + annotation margin). */
388
+ width: number;
389
+ height: number;
390
+ /** Tight drawing bounds (before margin), for chrome placement. */
391
+ bounds: Bounds;
392
+ nodes: SceneNode[];
393
+ theme: Theme;
394
+ sizes: RenderSizes;
395
+ north: NorthDir;
396
+ scale?: string;
397
+ title?: TitleNode;
398
+ name: string;
399
+ /** Distinct wall materials in use (stable order), so the SVG backend can emit hatch `<pattern>`s. */
400
+ materials: string[];
401
+ }
402
+
246
403
  interface CompileError {
247
404
  /** Human-readable message. */
248
405
  message: string;
@@ -280,7 +437,159 @@ interface CompileResult {
280
437
  diagnostics: Diagnostic[];
281
438
  /** The validated AST, present whenever parsing succeeded. */
282
439
  ast?: PlanNode;
440
+ /**
441
+ * The backend-neutral Scene IR (positioned drawing primitives), present
442
+ * whenever rendering succeeded (i.e. no fatal errors). Feed it to alternate
443
+ * backends: `toDxf(scene)`, `toPdf(scene)`.
444
+ */
445
+ scene?: Scene;
446
+ }
447
+
448
+ /**
449
+ * Intermediate representation + `resolve(ast)`.
450
+ *
451
+ * `resolve` is the single place semantics live: it grid-snaps coordinates,
452
+ * assigns ids, hosts openings, and runs semantic checks — producing a NEW
453
+ * immutable IR (the input AST is never mutated). `render` consumes IR only.
454
+ */
455
+
456
+ interface RBase {
457
+ kind: ElementKind;
458
+ id: string;
459
+ span?: Span;
460
+ }
461
+ interface RWall extends RBase {
462
+ kind: "wall";
463
+ category: string;
464
+ thickness: number;
465
+ /** Resolved hatch material (always a known material; defaults to "poche"). */
466
+ material: string;
467
+ points: Point[];
468
+ closed: boolean;
283
469
  }
470
+ interface RRoom extends RBase {
471
+ kind: "room";
472
+ at: Point;
473
+ size: {
474
+ w: number;
475
+ h: number;
476
+ };
477
+ label?: string;
478
+ }
479
+ interface RDoor extends RBase {
480
+ kind: "door";
481
+ at: Point;
482
+ width: number;
483
+ hinge: "left" | "right";
484
+ swing: "in" | "out";
485
+ host: WallSegment | null;
486
+ }
487
+ interface RWindow extends RBase {
488
+ kind: "window";
489
+ at: Point;
490
+ width: number;
491
+ host: WallSegment | null;
492
+ }
493
+ interface RFurniture extends RBase {
494
+ kind: "furniture";
495
+ category: string;
496
+ at: Point;
497
+ size: {
498
+ w: number;
499
+ h: number;
500
+ };
501
+ label?: string;
502
+ }
503
+ interface RDim extends RBase {
504
+ kind: "dim";
505
+ from: Point;
506
+ to: Point;
507
+ offset: number;
508
+ text?: string;
509
+ }
510
+ interface RColumn extends RBase {
511
+ kind: "column";
512
+ at: Point;
513
+ size: {
514
+ w: number;
515
+ h: number;
516
+ };
517
+ }
518
+ type ResolvedElement = RWall | RRoom | RDoor | RWindow | RFurniture | RDim | RColumn;
519
+ interface ResolvedPlan {
520
+ name: string;
521
+ units: "mm";
522
+ grid: number;
523
+ scale?: string;
524
+ north: NorthDir;
525
+ title?: TitleNode;
526
+ theme?: Partial<Theme>;
527
+ /** Resolved elements, in source order (for rendering). */
528
+ elements: ResolvedElement[];
529
+ /** Resolved walls (for bounds/hosting), in source order. */
530
+ walls: RWall[];
531
+ }
532
+ declare function resolve(ast: PlanNode): {
533
+ ir: ResolvedPlan;
534
+ diagnostics: Diagnostic[];
535
+ };
536
+
537
+ /**
538
+ * Lowers a resolved plan (IR) to the backend-neutral {@link Scene}.
539
+ *
540
+ * This is the single place geometry is assembled: each element contributes
541
+ * positioned primitives via its registry `render`, walls are unioned/offset here
542
+ * (the only element needing cross-segment treatment), and the page-level sizing
543
+ * (reference dimension, derived font/stroke sizes, bounds) is computed once and
544
+ * carried on the Scene for the backends. Pure & deterministic — no I/O, no time.
545
+ */
546
+
547
+ /**
548
+ * Build the {@link Scene} for a resolved plan. The theme is merged + sanitized
549
+ * once here and baked into node paint; it is also carried on the Scene for the
550
+ * page chrome (north/scale/title). `opts.width` does not affect the Scene (it is
551
+ * an SVG-only attribute) — only `opts.theme` participates.
552
+ */
553
+ declare function toScene(ir: ResolvedPlan, opts?: CompileOptions): Scene;
554
+
555
+ /**
556
+ * DXF export backend — a pure serializer of the {@link Scene}. Emits ASCII DXF
557
+ * (R12 / AC1009, the most broadly importable flavor). Pure, synchronous,
558
+ * zero-dep: DXF is plain text, so this needs no external library and ships in the
559
+ * core. Build a Scene with `toScene(resolve(ast).ir)` (or `compile().scene`).
560
+ *
561
+ * As of v0.7 the geometry is NOT re-derived here: door arcs, window panes, and
562
+ * dimension ticks are the very `ScenePrim`s the elements produced. Each primitive
563
+ * maps generically to a DXF entity; the only element-aware step is mapping a draw
564
+ * layer to a DXF layer name. DXF's Y axis points up while ArchLang's points down,
565
+ * so every Y is negated to keep plans right-side-up in CAD.
566
+ */
567
+
568
+ /** Render a {@link Scene} as an ASCII DXF document string. */
569
+ declare function toDxf(scene: Scene): string;
570
+
571
+ /**
572
+ * PDF export backend — a **true vector** serializer of the {@link Scene}.
573
+ *
574
+ * Walks the Scene's positioned primitives into pdfkit drawing ops, so strokes
575
+ * are real vector paths and text is selectable (no SVG rasterization round-trip).
576
+ * `pdfkit` is an OPTIONAL dependency, lazy-`import()`ed so the zero-dep core never
577
+ * hard-requires it; a clear error is thrown if it is absent. Async + Node-oriented
578
+ * — NOT part of `compile()`. Build a Scene with `toScene(ir)` or `compile().scene`.
579
+ *
580
+ * Coordinates: ArchLang is mm, top-left origin, +y down — pdfkit's user space is
581
+ * the same orientation, so we map the viewBox by translating by its top-left and
582
+ * treat 1mm as 1pt (as the previous SVG-based exporter did with `assumePt`).
583
+ *
584
+ * Page chrome (north arrow, scale bar, title block) is drawn with PDF-native
585
+ * helpers to keep parity with the SVG output. This duplicates the chrome geometry
586
+ * (also in `backends/svg.ts`) — a deliberate, bounded cost until chrome itself
587
+ * moves into the Scene in a later phase. Hatch patterns are SVG-specific, so
588
+ * poché regions fill with the solid poché base colour in PDF.
589
+ */
590
+
591
+ /** Convert a {@link Scene} to a vector PDF (Uint8Array). Requires optional `pdfkit`. */
592
+ declare function toPdf(scene: Scene): Promise<Uint8Array>;
284
593
 
285
594
  /**
286
595
  * ArchLang — compile declarative floor-plan source to a professional SVG.
@@ -294,4 +603,4 @@ declare function compile(source: string, opts?: CompileOptions): CompileResult;
294
603
  /** Clear the internal compile cache (useful in long-lived processes/tests). */
295
604
  declare function clearCache(): void;
296
605
 
297
- export { type AstElement, type ColumnNode, type CompileError, type CompileOptions, type CompileResult, type CompileWarning, type ComponentDef, type Diagnostic, type DimNode, type DoorNode, type ElementKind, type ExprPoint, type FurnitureNode, type InstanceNode, type LetNode, type NodeBase, type NorthDir, type PlanNode, type Point, type RoomNode, type Severity, type Span, type Statement, type TitleNode, type WallNode, type WindowNode, clearCache, compile, formatDiagnostic, offsetToLineCol };
606
+ export { type AstElement, type ColumnNode, type CompileError, type CompileOptions, type CompileResult, type CompileWarning, type ComponentDef, type Diagnostic, type DimNode, type DoorNode, type ElementKind, type ExprPoint, type FurnitureNode, type InstanceNode, type LetNode, type NodeBase, type NorthDir, type Paint, type PlanNode, type Point, type RColumn, type RDim, type RDoor, type RFurniture, type RRoom, type RWall, type RWindow, type RenderPass, type RenderSizes, type ResolvedElement, type ResolvedPlan, type RoomNode, type Scene, type SceneNode, type ScenePrim, type Severity, type Span, type Statement, type TitleNode, type WallNode, type WindowNode, clearCache, compile, formatDiagnostic, offsetToLineCol, resolve, toDxf, toPdf, toScene };
package/dist/index.js CHANGED
@@ -2,12 +2,20 @@ import {
2
2
  clearCache,
3
3
  compile,
4
4
  formatDiagnostic,
5
- offsetToLineCol
6
- } from "./chunk-GUNWYUR2.js";
5
+ offsetToLineCol,
6
+ resolve,
7
+ toDxf,
8
+ toPdf,
9
+ toScene
10
+ } from "./chunk-CPK5CI5Y.js";
7
11
  export {
8
12
  clearCache,
9
13
  compile,
10
14
  formatDiagnostic,
11
- offsetToLineCol
15
+ offsetToLineCol,
16
+ resolve,
17
+ toDxf,
18
+ toPdf,
19
+ toScene
12
20
  };
13
21
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chanmeng666/archlang",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "A small declarative language that compiles to professional SVG floor plans — like Typst/LaTeX, but for architecture.",
5
5
  "keywords": [
6
6
  "architecture",
@@ -52,6 +52,7 @@
52
52
  "test": "vitest run",
53
53
  "test:watch": "vitest",
54
54
  "cli": "tsx src/cli.ts",
55
+ "bench": "tsx bench/run.ts",
55
56
  "prepublishOnly": "npm run build && npm run test"
56
57
  },
57
58
  "devDependencies": {
@@ -60,6 +61,11 @@
60
61
  "tsup": "^8.3.5",
61
62
  "tsx": "^4.19.2",
62
63
  "typescript": "^5.7.2",
63
- "vitest": "^2.1.8"
64
+ "vitest": "^2.1.8",
65
+ "vscode-oniguruma": "^2.0.1",
66
+ "vscode-textmate": "^9.3.2"
67
+ },
68
+ "optionalDependencies": {
69
+ "pdfkit": "^0.15.2"
64
70
  }
65
71
  }