Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 41 additions & 14 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,36 @@ Space lines one and a half text heights apart or more. Given `at`, `align`, `val
is the box the `text-` warnings and their `bounds` use. A character outside Latin-1 is measured
as a `?`, and the result then has `approximate: true`.

## Drawing to a standard

A standard is a firm's layers, pens, text roles, scales, dimension styles and title blocks as
data. Extend the built-in one with what your drawing needs, and export it beside the drawing:

```ts
import { BUILT_IN_STANDARD, createDrawing, defineStandard } from "tsdraft";

export const standard = defineStandard({
id: "my-firm",
version: "1",
extends: BUILT_IN_STANDARD,
layers: { "A-WALL": { color: 2, pen: "bold" } },
});

const d = createDrawing({ units: "ft", standard });
d.layer("A-WALL", () => d.rect([24, 30], 52, 44));
export default d.build();
```

`d.layer(name, draw)` names a layer of the standard, which gives its color, linetype, pen and
plot flag, and an entity's `pen` names one of the standard's pens. A layer, role or scale the
standard lacks is a compile error, and the builder throws the same message, which says how to add
it to `defineStandard`. Add it there; never draw around the standard.

The command holds the drawing to the exported standard with `lint`: its errors are diagnostics
and give exit code 2, and `drawing.standard` names it.
[docs/standard.md](https://github.com/Surfside-Software/tsdraft/blob/main/docs/standard.md) has
the built-in standard and the rest.

## The file to copy

[`examples/site-plan.ts`](https://github.com/Surfside-Software/tsdraft/blob/main/examples/site-plan.ts)
Expand All @@ -251,20 +281,17 @@ Its parameters come first and every position is worked out from them. Its column
breaks are laid out with `measureText`. Its symbols are blocks.

It is drawn to a standard of its own, which extends `BUILT_IN_STANDARD` with the layer
`V-TREE`. `d.layer(name, draw)` names a layer of the standard, which gives its color, linetype,
pen and plot flag, and an entity's `pen` names one of the standard's pens. A layer the standard
lacks is a compile error that says how to add it to `layers` in `defineStandard`.

A text names a `role` of the standard, which gives its text style and the height it plots at.
The plan is a view at one of the standard's scales, `const plan = d.view("plan", { scale:
"1in-10ft" })`, and what model space draws to be read in it names the view:
`d.text(at, "LOT 27", { role: "label", view: plan })`, `d.insert("PROPERTY CORNER", at,
{ view: plan })` for a symbol drawn in inches, and `d.linearDimension(from, to, line, { view:
plan })` in the standard's dimension style. The viewport shows the view, as
`sheet.viewport(corner, width, height, { view: plan, center })`, and `plan.paper(inches)`
turns a gap on paper into drawing units. On the sheet and in a block, a text with a role is at
the role's height and names no view. Under a standard every text names a role: a `height`, a
`style` and `textStyle()` are refused. `sitePlan("1in-20ft")` draws the sheet at 1" = 20'.
`V-TREE`, as the section above shows. A text names a `role` of the standard, which gives its text
style and the height it plots at. The plan is a view at one of the standard's scales,
`const plan = d.view("plan", { scale: "1in-10ft" })`, and what model space draws to be read in it
names the view: `d.text(at, "LOT 27", { role: "label", view: plan })`,
`d.insert("PROPERTY CORNER", at, { view: plan })` for a symbol drawn in inches, and
`d.linearDimension(from, to, line, { view: plan })` in the standard's dimension style. The
viewport shows the view, as `sheet.viewport(corner, width, height, { view: plan, center })`, and
`plan.paper(inches)` turns a gap on paper into drawing units. On the sheet and in a block, a text
with a role is at the role's height and names no view. Under a standard every text names a
role: a `height`, a `style` and `textStyle()` are refused. `sitePlan("1in-20ft")` draws the
sheet at 1" = 20'.

Its layout is on a sheet format of the standard, `d.layout("C-101", { sheetFormat: "arch-d" },
(sheet) => …)`, which gives the paper, the title block and the zones the sheet is laid out by:
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ lint(drawing, { version: "R12" }); // check a drawing against another version
text plots with `measureText`, the warnings of `inspectText` about text that
overlaps or leaves its border, what tsdraft escapes in text, and what AutoCAD
does with it after a save.
- [Standards](docs/standard.md) covers a firm's standard: the document,
`defineStandard`, the built-in standard, drawing to a standard, what `lint`
holds a drawing to, and the `standard` a drawing file exports.
- [Checks of intent](docs/intent.md) covers `checkIntent`, which warns of a
footprint that crosses a setback, and the `intent` a drawing file exports.
- [The `tsdraft` command](docs/command.md) covers `render`, `dev`, the JSON they
Expand Down
15 changes: 14 additions & 1 deletion cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@
to the exit code, and each warning's own `bounds`. When the copy of tsdraft the drawing
loaded has no `checkIntent`, or its `checkIntent` throws, the build gets one
`intent-unchecked` warning that says so, so an intent is never passed over without a word.
- When the drawing file exports `standard`, the worker passes it to the library's `lint`, and
`drawing.standard` names it. A copy of tsdraft without `defineStandard` is from before
standards and would take the export and check nothing, so the worker does not pass it there,
and the build gets one `standard-unchecked` warning after `lint`'s: a standard is never passed
over without a word. A title block of the export whose border is an xref gives the pictures its
`preview` for that xref, unless the `xrefs` export gives a drawing for it: the file's own
drawing comes first. A preview comes only from a standard `lint` checked and accepted, since
its check refuses a getter at any depth. For `drawing.standard` the worker reads the
export's own data alone, never runs a getter and never asks a proxy at its top: one that
loops would end the build.
- `render --view` draws one rectangle with `toSVG`'s `view`, with no margin, as
`outputs.view`, and writes no other picture. `tsdraft dev` has no view: its server keeps
only SVG strings and a worker ends after one build. A `Builder` passes the view to the
Expand All @@ -70,7 +80,10 @@
That is the folder two above `src/worker.ts` or `dist/worker.js`, by its real path: its
`dist/index.js`, or `src/index.ts` in a checkout, and files in that one folder. No name in
a package.json decides it. `src/warmup.ts` runs that library on a sheet of its own and
reads nothing. Any other copy of tsdraft, such as a second one in a project's
reads nothing. It also calls `defineStandard` once, on a standard that extends the built-in
one, and draws, lints and pictures a sheet to it, so that a build's own `defineStandard` and
its `lint` with a standard find their code compiled. A build waits for the step in progress,
so keep each step short. Any other copy of tsdraft, such as a second one in a project's
`node_modules`, is never loaded or run ahead: it may print, count its calls or loop, and a
build must answer the same whether its process waited or not. Nor is the drawing, a module
of the user's or a kit that passes the library on. For those the build runs in a process
Expand Down
27 changes: 26 additions & 1 deletion cli/assets/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,14 @@
font-weight: 600;
letter-spacing: -0.01em;
}
header .file {
header .file,
header .standard {
color: var(--muted);
}
header .standard b {
color: var(--text);
font-weight: 500;
}
#status {
margin-left: auto;
display: flex;
Expand Down Expand Up @@ -411,6 +416,7 @@
<img src="/mark.svg" alt="" />
<span class="name">tsdraft</span>
<span class="file mono" id="file"></span>
<span class="standard mono" id="standard" hidden></span>
<div id="status" class="mono" aria-live="polite">
<span class="dot" id="dot"></span><span id="status-text">connecting</span>
</div>
Expand Down Expand Up @@ -703,6 +709,22 @@ <h3>Diagnostics</h3>
return missing ? `${xrefs.length}, ${missing} not drawn` : `${xrefs.length}`;
}

// The standard the drawing file exports, beside the file's name.
function renderStandard(state) {
const standard = state?.drawing?.standard;
const line = $("standard");
line.hidden = !standard;
line.replaceChildren();
if (!standard) return;
const id = document.createElement("b");
id.textContent = standard.id;
line.replaceChildren(
"standard ",
id,
` version ${standard.version}${standard.checked ? "" : " · not checked"}`,
);
}

function renderFacts(state) {
const d = state.drawing;
const fmt = (n) => (Math.round(n * 100) / 100).toString();
Expand Down Expand Up @@ -870,6 +892,7 @@ <h3>Diagnostics</h3>
select(null);
renderLayers(state);
renderFacts(state);
renderStandard(state);
} else {
const e = state.error;
holder.classList.toggle("stale", Boolean(svg));
Expand All @@ -888,6 +911,7 @@ <h3>Diagnostics</h3>
if (lastGood) {
renderLayers(lastGood);
renderFacts(lastGood);
renderStandard(lastGood);
} else $("empty").textContent = "No drawing yet";
}
renderDiagnostics(state);
Expand Down Expand Up @@ -920,6 +944,7 @@ <h3>Diagnostics</h3>
view = null;
moved = false;
lastGood = null;
renderStandard(null);
swatches = {};
for (const id of ["facts", "layers", "diagnostics"]) $(id).replaceChildren();
$("error").hidden = true;
Expand Down
44 changes: 38 additions & 6 deletions cli/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export interface BuildState {
/** What the drawing holds. `null` when the build failed. */
readonly drawing: DrawingSummary | null;
/**
* What `lint` reports for the drawing, errors first, and after it what `inspectText` warns of
* What `lint` reports for the drawing, errors first, held to the file's export `standard` when
* it has one, and after it what `inspectText` warns of
* in the text as it plots: a rule that starts with `text-`. Then, when the file has an export
* `intent`, what `checkIntent` warns of where the drawing does not hold to it. When the file
* threw a `DrawingError`, as `toDXF` and the builder do, its diagnostics, without `kind` and
Expand Down Expand Up @@ -179,9 +180,17 @@ export interface PngFont {
* and the footprint nearest each other, larger on every side by the setback, and for
* `footprint-off-property` the footprint's box. `footprint-in-setback` also gives `side`,
* `required` and `provided`.
*
* `standard-unchecked` is a warning of the command's own, among lint's: the file exports a
* standard, and the copy of tsdraft the drawing loaded is from before standards, so `lint` held
* the drawing to none of it.
*/
export interface BuildDiagnostic extends Omit<Diagnostic, "rule"> {
readonly rule: Diagnostic["rule"] | TextWarning["rule"] | IntentWarning["rule"];
readonly rule:
| Diagnostic["rule"]
| TextWarning["rule"]
| IntentWarning["rule"]
| "standard-unchecked";
readonly kind?: Entity["kind"];
/** The box around the entity, in drawing units, worked out as `extents` is. */
readonly bounds?: Box;
Expand Down Expand Up @@ -217,6 +226,8 @@ export interface Timings {
export interface DrawingSummary {
readonly units: Units;
readonly dxf: DxfVersion;
/** The standard the drawing file exports as `standard`, or `null` when it exports none. */
readonly standard: StandardSummary | null;
/**
* The smallest box around the entities on layers that are on, thawed and set to plot, in
* drawing units, as `toSVG` works it out. A build that draws the layers that do not plot
Expand Down Expand Up @@ -261,6 +272,24 @@ export interface DrawingSummary {
readonly layouts: readonly LayoutSummary[];
}

/**
* A firm's standard, as `defineStandard` returns it, that the drawing file exports beside its
* drawing with `export { standard }`. An export that does not hold a string `id`, `version` and
* `hash` is no standard: `drawing.standard` is then `null`, and when `lint` was given it, its
* `standard` error says what is wrong with it.
*/
export interface StandardSummary {
readonly id: string;
readonly version: string;
/** The SHA-256 of what the standard states, as the export holds it. */
readonly hash: string;
/**
* Whether `lint` held the drawing to the standard. False when the copy of tsdraft the drawing
* loaded is from before standards, which a `standard-unchecked` warning says.
*/
readonly checked: boolean;
}

/** A layout: a sheet of paper, with what is drawn on it and the viewports that show the model. */
export interface LayoutSummary {
readonly name: string;
Expand Down Expand Up @@ -344,13 +373,16 @@ export interface XrefSummary {
/**
* Whether the pictures draw a drawing for the xref. The command opens no DWG file: it draws
* the drawing that the drawing file's `xrefs` export gives under the xref's name, when that
* drawing has no lint error. It cannot tell whether that drawing is what the file at `path`
* holds, so `true` says what the pictures drew and not what CAD will load. When this is
* false, the pictures and the extents have nothing of the xref, and CAD will still draw it.
* drawing has no lint error. When it gives none, and a title block of the file's export
* `standard` has the xref as its border and a `preview`, it draws the preview. It cannot tell
* whether that drawing is what the file at `path` holds, so `true` says what the pictures
* drew and not what CAD will load. When this is false, the pictures and the extents have
* nothing of the xref, and CAD will still draw it.
*/
readonly shown: boolean;
/**
* The units of the drawing given for the xref, or `null` when none is given. CAD draws an
* The units of the drawing given for the xref, or of the preview drawn for it, or `null` when
* there is neither. CAD draws an
* insert at the scale it has, whatever the units, so where these differ from
* `drawing.units` each insert needs the scale between the two.
*/
Expand Down
61 changes: 55 additions & 6 deletions cli/src/warmup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,28 +44,77 @@ export const formatSteps: readonly (() => void)[] = [
];

/**
* The steps that run the library: a small sheet through the builder, `lint` and `toSVG`.
* The steps that run the library: a small sheet through the builder, `lint` and `toSVG`, then
* one call of `defineStandard` that extends the built-in standard, as a drawing file's does,
* and a sheet drawn to that standard and held to it.
*/
export function librarySteps(library: Library): readonly (() => void)[] {
let drawing: ReturnType<typeof sheet> | undefined;
let standard: ReturnType<typeof extended> | undefined;
let drawn: ReturnType<typeof sheetToStandard> | undefined;
const sheets = (d: ReturnType<typeof sheet> | undefined) => {
for (const layout of d?.layouts ?? []) library.toSVG?.(d!, { layout: layout.name });
};
return [
() => (drawing = sheet(library)),
() => drawing && library.lint?.(drawing),
() => drawing && library.toSVG?.(drawing, { margin: 0 }),
() => {
for (const layout of drawing?.layouts ?? [])
library.toSVG?.(drawing!, { layout: layout.name });
},
() => sheets(drawing),
() => (standard = extended(library)),
() => standard && (drawn = sheetToStandard(library, standard)),
() => drawn && library.lint?.(drawn, { standard: standard! }),
() => sheets(drawn),
// Once more, since much of the code is compiled better on its second run.
() => {
const again = sheet(library);
library.lint?.(again);
library.toSVG?.(again, { margin: 0 });
for (const layout of again.layouts ?? []) library.toSVG?.(again, { layout: layout.name });
sheets(again);
},
() => {
if (!standard) return;
const again = sheetToStandard(library, standard);
library.lint?.(again, { standard });
sheets(again);
},
];
}

/** A standard that extends the built-in one with a layer. A copy from before standards has none. */
function extended(library: Library) {
return library.defineStandard?.({
id: "warm-up",
version: "1",
extends: library.BUILT_IN_STANDARD!,
layers: { "V-TREE": { color: 3, pen: "fine", discipline: "V" } },
});
}

/** A sheet drawn to a standard: layers, roles, a view, a dimension, a sheet format and its title block. */
function sheetToStandard(library: Library, standard: NonNullable<ReturnType<typeof extended>>) {
const d = library.createDrawing!({
units: "ft",
standard,
fields: { project: "A HOUSE", date: "2026-01-01", drawnBy: "AB", checkedBy: "CD" },
});
const plan = d.view("plan", { scale: "1in-10ft" });
d.block("TREE", {}, () => d.circle([0, 0], 1));
d.layer("C-PROP", () => d.rect([0, 0], 80, 120));
d.layer("V-TREE", () => d.insert("TREE", [12, 100], { scale: 6 }));
d.layer("C-ANNO-TEXT", () => {
d.text([40, 60], "RESIDENCE", { role: "label", view: plan, align: "center" });
});
d.layer("C-ANNO-DIMS", () => d.linearDimension([0, 0], [80, 0], [0, -10], { view: plan }));
d.layout("C-101", { sheetFormat: "arch-d" }, (paperSheet) => {
paperSheet.titleBlock({ title: "SITE PLAN" });
d.layer("G-ANNO-NPLT", () => {
paperSheet.viewport([1, 2.5], 26, 20.5, { view: plan, center: [40, 60] });
});
d.layer("C-ANNO-TEXT", () => d.text([29, 22.5], "GENERAL NOTES", { role: "heading" }));
});
return d.build();
}

function sheet(library: Library) {
const d = library.createDrawing!({ units: "ft", linetypeScale: 1 / 2 });
const ratio = 120;
Expand Down
Loading
Loading