Drawing results

build_drawing(...) and Sheet.build() return a Drawing. Its public methods cover inspection, semantic editing, linting, and export.

Drawing

A composable technical drawing — the editable form of :func:make_drawing.

A Drawing holds the projected views, the annotation list, and per-view coordinate helpers. :func:build_drawing returns one pre-populated with the automatically selected principal/pictorial views and dimensions; you then add or remove annotations, add section/auxiliary views, and finally :meth:export.

Attributes:

Name Type Description
scale

drawing scale factor (e.g. 2.0 for 2:1).

scale_decision

JSON-friendly resolution of an automatic or explicit scale request, including the requested/effective scales and any required placement blockers.

annotation_scheme_decision dict[str, object]

JSON-friendly corridor comparison and a versioned, observational profile recommendation from pre-render demand. With annotation_layout="compare" it also records verified layout trials and the selected result plus provisional safety evidence; influenced_layout says whether a candidate won. Provisional evidence is not an admission gate.

view_decision dict[str, object]

JSON-friendly resolution of automatic principal-view selection. chosen is the final principal set and attempts records a reduced candidate and why it was accepted or rejected.

arrangement_decision

JSON-friendly record of how the sheet arrangement was resolved (#1130) — chosen names the arrangement the sheet was composed under, and attempts lists each one built, in order, with the required placement blockers that rejected it. One entry means one compile.

section_decision

JSON-friendly record of the section A-A outcome (#1190) — status is "placed", "skipped", "not_warranted", or "not_evaluated" when the section pass never ran (auto_dims=False), with a stable reason code and human-readable detail when skipped.

detail_decisions list[dict[str, object]]

JSON-friendly outcomes for requested detail views, including model-space crop bounds, physical support evidence, resolved scale, and a named refusal reason. Observational only; requirement lint remains authoritative.

page_w, page_h

sheet size in mm.

tb_w

title-block width in mm.

draft

the shared Draft preset used by the automatic annotations.

look_at

scaled centroid (x, y, z) — the default target for internal view projection (see :meth:_add_view).

dist

orthographic camera distance in scaled space.

centroid

unscaled centroid (x, y, z).

views dict

{name: (visible_compound, hidden_compound_or_None)}.

items list

ordered list of annotation objects (mutable).

part

the source solid, when known — enables the feature-coverage lint.

assembly

feature-coverage severity control — None auto-detects a multi-solid part as an assembly (per-part bores at info), True/False forces it (#69).

reproducible

default for :meth:export's reproducible= — when true, two exports of this drawing are byte-identical. True by default; pass False to trade that for export speed on a part-heavy sheet (see :func:draftwright.export._elements).

The constructor also accepts cyls, a precomputed analyse_cylinders(part) result (cached privately; computed lazily on first :meth:lint otherwise).

view_plan property

The resolved view plan (ADR 2 (was 0018)), or None before the views are created.

READ ONLY, and there is no setter: a resolved plan that a caller can rebind is indistinguishable from an authored request, which is the confusion ADR 2 (was 0018 §1) exists to prevent. Editing means converting it into constraints explicitly and resolving again.

working_part property

The coordinate-coherent compiler/projection solid (read-only).

iso_projection_scale property

The scale of the final projected isometric view, if one was projected.

recognition_frame property

The provider caller-to-working frame, or None outside a framed build.

leader_region property

The resolved feature-leader region policy for this drawing.

pmi_mode property

The imported PMI presentation policy resolved for this drawing.

general_tolerance_source property

The document default attached to the title-block tolerance, if any.

default_surface_finish_source property

The document-wide finish attached to sheet furniture, if any.

document_member property

Whether this drawing belongs to an imported document.

document_source_annotation_ids property

Source PMI identifiers already owned by the imported document.

recognition_frame_decision property

A copy of the explicit framed/raw/refusal selection outcome.

drawable_bounds property

Physical margins as (left, bottom, right, top) page coordinates in mm.

A frame uses this rectangle; view placement reserves additional clearance inside it.

registry property

The AnnotationRegistry (identity/build-issue store) — the build handle the passes' PlacementContext references (#639).

coverage property

The CoverageState — referenced by the run's PlacementContext (#639).

box_cache property

The ONE annotation bounding-box memo for this build (#1138).

Placement and lint both measure annotations, and an optimal bounding_box() tessellates (~16 ms for a Leader), so anything measured on both paths is worth measuring once. Sharing one memo makes that hold by construction.

Keep its measured scope in mind before attributing a build's cost to it: on a plate build it holds two entries, on a flange three, all leaders and their callouts. Dimensions are not among them and cannot be — strip_obstacles decomposes anything exposing .segments instead of boxing it whole, and corridor_blockers skips Dimension/SafeDimension outright. The memo is worth tens of milliseconds, not a phase; the leader work in #1138 is what moved the number.

Exposed publicly (not as _ann_box_cache) because the annotations layer is duck-typed against dwg and, per ADR 1 (was 0005), reads no private Drawing state. Sharing the dict rather than adding a second memo also means lint()'s existing liveness prune — which drops entries for objects no longer on the sheet — covers placement-seeded entries for free; a separate placement cache would have to re-implement that pruning, and a missed prune keeps OCC geometry alive for the drawing's lifetime.

solve_trace property

The opt-in solve-trace recorder threaded through this build (#736), or None (the default — tracing off). See build_drawing(trace=...).

model_declared property

Whether this drawing's model was declared by the caller (ADR 4 (was 0011)) rather than detected — the public read the annotation pass threads onto its PlacementContext (#639).

set_iso_projection_scale(scale)

Record an isometric projection or reprojection from the projection stage.

attach_document_context(member, source_annotation_ids)

Attach imported-document policy once, before annotation passes run.

coords(view)

Return the :class:ViewCoordinates for a named view.

at(view, x, y, z)

Map a world point to a page point (px, py, 0) in view.

Raises :class:ViewNotPlanned when view is not on the sheet. A bare KeyError from inside whichever render pass happened to ask first is not a usable answer to "this drawing does not have that view" — ADR 2 (was 0018 §6) wants an absent view to be a named result, because view-set selection makes asking for one the normal case rather than a bug (#1130).

view_bounds(view)

Return (x_min, y_min, x_max, y_max) of the projected geometry in view, or None if the view is unknown (#28).

The box is the tight bounding box of the placed silhouette — visible plus hidden lines — in page coordinates (mm from the sheet origin), the same space :meth:at returns. Use it to place free-form notes, leader elbows and the like just outside a view without guessing offsets::

x0, y0, x1, y1 = dwg.view_bounds("front")
dwg.note("SEE NOTE 1", (x1 + 5, (y0 + y1) / 2))

features(view='front')

Return detected geometric features in page coordinates for view.

Holes are grouped by machining spec (diameter + depth + cbore) and returned as :class:FeatureInfo objects with count set to the number of identical holes at that spec. Each group's page_pos is the page position of the first hole in the group.

The view determines which holes appear as circles (and are therefore annotatable from that view):

  • "plan" → Z-axis holes
  • "front" / "rear" → Y-axis holes
  • "side" → X-axis holes

Returns an empty list when no analysis is available or the view name is unrecognised.

model()

The detected PartModel this drawing was built from (ADR 1 (was 0008) IR) — the read surface for semantic edits (#397, ADR 4 (was 0001 Amendment 1)).

Both input scenarios converge here: a STEP file and a build123d solid both normalise to a solid, are detected once, and produce the same feature model (.features — holes/slots/steps/patterns, .datums, .orientation, .bbox). This is the provenance-agnostic "what is in this drawing and why" — richer than :meth:features (grouped holes, per view) and the read surface for feature-referenced edits.

Read-only — a view of what was built; mutating it does not change the drawing. Experimental: exposes raw IR dataclasses that may still evolve.

Populated for every built drawing, including a manual-mode (auto_dims=False) build — detection runs in the pipeline, not the annotation pass (#398), so a script can dimension detected features even when it suppressed the automatic ones. None only on a bare, unbuilt Drawing.

recognition()

The ADR 3 (was 0017) recognition inventory used to build this drawing.

This is the geometry-only evidence below the detected/declared :meth:model and drafting policy. It is an experimental, read-only result.

None for a bare Drawing that did not pass through :func:build_drawing, and for a declared build that has not yet been critiqued — that path recognises nothing (ADR 4 (was 0011) / #1022) and only builds an aggregate when something asks for physical critique. So None here means "nothing has needed recognition yet", never "this part has no features".

recognition_evidence()

The run-scoped provider evidence paired with :meth:recognition.

This experimental, read-only view is available for raw automatic recognition and after the first physical critique of a declared drawing. It is None before that lazy critique, for a bare drawing, and for the framed path, which has not adopted the provider's framed-evidence contract. Draftwright never reruns recognition merely to fill this value. The returned evidence borrows exact faces from the source part, so callers must not mutate that part while using the evidence view.

recognition_ownership()

Run-local accepted-occurrence ownership captured during detected conversion.

This experimental, read-only ledger is available only when raw automatic recognition supplied :meth:recognition_evidence. It currently classifies unconditional one-to-one adapters; singleton/grouped/pattern holes, slots, and pockets; nested countersinks; and settled ownerless unsupported, deferred, and evidence-only policy. Remaining nested and classification-only families stay explicitly unclassified. It carries opaque provider references and therefore cannot be serialized or used as persistent feature identity.

record_section_decision(status, *, reason=None, detail='')

Record what happened to section A–A (#1190).

A public verb rather than an attribute the render pass assigns, so the annotations layer stays off Drawing internals (ADR 1 (was 0005)) and every outcome lands in one shape. status is "placed", "skipped" or "not_warranted"; reason is a stable code for the skipped case.

material_fields()

The per-view filled projected material of this drawing, keyed by id(shape).

The ADR 2 (was 0014) leader routing and the leader_crosses_silhouette critique must agree on what counts as travelling through the part, so both read this ONE lowering rather than each deriving the answer from the projected outline. An empty dict means the part could not be meshed, which callers must read as "no material known" rather than "clear" — inventing clearance from a failed lowering is how a silent geometry failure becomes a confident wrong answer.

The tessellation behind it is memoised (including its failure, so an unmeshable part is not retried on every lint), but the per-view fields are reconciled against the CURRENT views on each call, for two reasons:

  • This is first called mid-build, by the feature-leader stage — which runs before the section and detail stages. Fields built once would permanently omit every view added afterwards, and leaders in a detail view would silently never be checked for cutting.
  • Entries are identity-checked, like _view_edge_entries' (#143), because a replaced view shape can be collected and its id reused — which would hand back another view's material.

pending_title_block_box()

The title block's page-space footprint before it has been drawn, or None.

"title_block" sits near the end of _PASS_SEQUENCE, so it is absent from iter_annotations while strips place — which is why a below/right strip ran into its region and only the paths asking _title_block_box directly ever avoided it (#481 did that for GD&T; #1593 was the same gap for dimensions). Its footprint is deterministic before it exists, so the builder measures it once and hands it over here rather than annotations/ probing the drawing (ADR 1 (was 0005 §2): the drawing is not the state bus).

title_block_for(key, factory)

Return the build-owned title block for its page, fields and typography.

suppressions()

Every measurement the compiler considered and did not approve, and why.

The audit read (#996). A finished drawing shows what was drawn; this shows what was not, separated into the two cases that mean opposite things:

  • authored — the script's own omission, under ADR 4 (was 0016)'s rule that an authored set means omission is suppression. Recoverable by adding a dimension(...) line.
  • otherwise — a planner rule decided it, and reason names which.

conveyed_by distinguishes a measurement that was WITHHELD from one that was CONSOLIDATED (#1154): when it is set, the fact is still on the sheet, stated by the dimension it names. "This is not drawn" and "this is drawn over there" are different answers, and an audit that flattened them would read a de-duplication as a gap. It rides the same stable kind@(x,y,z)/axis key as feature, so the two halves of one consolidation can be matched up without importing IR types. None wherever nothing takes the fact over — which is NOT the same question as authored: an authored omission carries a conveyed_by whenever the author's set keeps the owner, since the author chooses which dimensions are drawn and not where the geometry states a fact.

The second is the one worth auditing. A rule that fires where it should not produces a drawing that is silently under-defined and lints clean, which is how #997's square rule generated four separate issue reports without any of them naming the cause. An absent dimension is only defensible if something can say which rule removed it; this is that something.

Returns plain dicts so a harness, a script or an LLM can diff two builds without importing IR types. feature is a stable key, not just the type name: a bare "HoleFeature" made two holes indistinguishable, so a diff could not say which one lost its location, or whether a suppression moved between instances. The key is kind@(x,y,z)/axis, which survives a rebuild because it is derived from the geometry rather than from list position.

measurement_keys(name)

Which measurements the annotation name draws — possibly none (#1002).

The mirror of :meth:suppressions and deliberately the SAME row shape — {"feature": <stable key>, "parameter_id": ...} — so a drawn measurement and a suppressed one are directly comparable. Without it the two halves of the audit could only be joined by matching an engine-assigned annotation name against a parameter id by substring, which attributed losses to unrelated suppressions.

A list, because one annotation can draw several independently suppressible measurements — a compound hole callout renders bore diameter, depth and counterbore together (ADR 4 (was 0016) / #886). Empty means the renderer recorded nothing, not that the annotation measures nothing. Which renderers record it is enforced by the ratchet in tests/test_audit_differential.py; treat presence as exact and absence as unknown.

These keys describe geometry and have the collision limits of feature_key. For declaration-local ownership and meaning comparisons, capture :meth:measurement_snapshot; geometry descriptions alone do not prove correspondence.

measurement_snapshot()

Capture named measurement claims for a declaration-local edit comparison.

Use :func:draftwright.audit.compare_measurements to compare snapshots. Capture before mutating a drawing. Owner references stay local to this process; separately rebuilt declarations need an explicit correspondence, never a geometry-key guess. This read compiles the existing model and reads recorded claim text, without recognition.

place_dim(p1, p2, side, view, draft, *, name=None, slot=8.0, feature=None, **kwargs)

Deprecated low-level page-coordinate dimension escape hatch.

Add a :class:~build123d_drafting.helpers.Dimension that stacks cleanly with the auto-generated dimensions by delegating to the same strip-allocation system (:class:Strip) that :func:build_drawing uses internally.

Parameters:

Name Type Description Default
p1

first page-coordinate tuple (px, py, 0) — use :meth:at to convert world coordinates.

required
p2

second page-coordinate tuple (px, py, 0).

required
side

"above", "below", "left", or "right".

required
view

"front", "plan", "side", or "rear".

required
draft

the drawing's :attr:draft preset.

required
name

optional annotation name for later :meth:remove / replace.

None
slot

strip slot depth (mm); the perpendicular space reserved per dim.

8.0
feature

optional source IR feature to attribute this dim to, so :meth:drop / :meth:annotations_of can find it (#398).

None
**kwargs

forwarded to Dimension (e.g. label=). tolerance= is folded into the label rather than forwarded — your own label= included — because helpers do rendered = label if label is not None else …, so an explicit label DISCARDS a forwarded tolerance and a label is always present here (#1234).

{}

Deprecated for normal editable scripts: prefer :meth:dimension for feature-backed linear dimensions and :meth:locate for feature-backed location dimensions. Both support pin=True in deferred/finalize mode and can participate in the shared layout solve.

Uses the single-position strip carve, not the ADR 2 (was 0009) collect-then-solve path the automatic placers use — fine for adding a dimension into free space, but it does not re-solve the strip or dedup against existing dims (#396). Prefer :meth:dimension for a feature-referenced edit.

Falls back to a fixed slot offset when the strip is full or when no layout analysis is available (e.g. when auto_dims=False was not used with :func:build_drawing).

The @deprecated (PEP 702) decorator both emits the runtime DeprecationWarning and lets type checkers/IDEs flag call sites statically (#817).

remove(name)

Remove a previously named annotation. Raises KeyError if absent.

annotations_of(feature)

{name: object} for every annotation rendered for feature (#398).

feature is an IR feature from :meth:model (dwg.model().features[i]). Matched by value, so the exact object is not required. Empty if the feature has no annotations (or its render pass does not yet tag provenance — coverage grows as passes are migrated).

drop(feature)

Remove every annotation rendered for feature (#398) — the semantic curation verb: "stop dimensioning this feature". Returns the removed names.

Use a feature from :meth:model: dwg.drop(dwg.model().features[0]). Removing a feature's callout/centre-mark/size-dims is a page-level edit; call :func:finalize_drawing afterwards (when available) to recompose the sheet. A measured schedule shared by other features requires editing its declared rows and rebuilding; partial removal refuses before changing any ink.

dimension(feature, param, *, role=None, side=None, view=None, name=None, pin=False, priority=0.0, **kwargs)

Add a dimension for feature's param, attributed to the feature (#398e).

The feature-referenced add verb: pair to :meth:drop. feature is an IR feature from :meth:model; param is a linear parameter kind, exact parameter id, or discriminator it exposes — a turned step's "length" or a through step's "through_step_leg.length.x"/"x" (value-only slot geometry is derived here via :meth:_derive_span). The dimension is placed into free strip space and tagged with feature, so :meth:drop / :meth:annotations_of find it. Returns the annotation name.

A feature may expose several params of one kind (an envelope's width/height/depth, or a slot's slot_width/slot_length, are all "length"); pass role= or an exact parameter id/discriminator to pick one — an ambiguous kind raises rather than guessing.

view is chosen from the selected principal views ("front"/"plan"/ "side"/"rear") where the span projects non-degenerate — a length along the turning axis vanishes in its end-on view, so the view follows the geometry. Through-step legs share their semantic axis end view and natural outside-corner sides. Pass view= to select a principal explicitly (a non-orthographic view foreshortens the span and is rejected). An implicit side is "above" except for through-step legs, whose missing corner selects the natural outside corridor. kwargs forward to the dimension — except tolerance=, which is folded into the label (see :meth:place_dim), because helpers discard a forwarded tolerance whenever a label is present. In deferred mode, pin=True anchors the dimension at its natural slot coordinate inside the shared corridor solve, and priority= controls over-capacity survival. Live placement still uses the single-position escape hatch and pins only the placed annotation name.

Raises ValueError if the feature has no such param, the kind is ambiguous, or view is not orthographic. A hole's "diameter"/"depth" are leader callouts, not linear dimensions, so they raise here — a callout add verb is a separate mechanism, tracked apart from this one.

callout(feature, *, view=None, name=None)

Add a ø leader callout for feature (#414/#419) — the callout half of the feature-referenced add surface, symmetric with :meth:drop.

Where :meth:dimension draws a linear dim, callout draws a leader: for a hole/pattern, the ø / n× / through-or-depth / counterbore callout (the same text the auto-pass builds), placed beside the feature's end-on view (view defaults to it); for a turned step/boss, the ø… diameter leader in the row below (X-turned) or column left of (Z-turned) the front view. Tagged with feature so :meth:drop / :meth:annotations_of find it. Returns the annotation name.

Raises ValueError if feature exposes no callout (use :meth:dimension for a linear param). A machined-feature callout (pocket/pad-height/circular-blind-step/fillet/blend/paired-ramp/flat/chamfer/groove) is auto-named and placed in its characteristic view by the kind's renderer, so view=/name= are unsupported for those kinds and raise ValueError rather than being silently ignored. Placed reasonably, not via the auto-pass's whole-set solve (byte-identity is not a goal, #400 Ph2) — :meth:repair tidies the rest. A step/boss diameter that finds no room returns "" (a warning-level drop, like the auto-pass), rather than raising, so a reconstruction script never aborts.

overall_height()

Add the part's overall height — the one dimension with no feature to name.

Every other add verb takes a feature, because every other dimension belongs to one. The overall height usually does too: a model with an EnvelopeFeature carries a height parameter, and dimension(env, "length", role="height") is the verb for it.

A model WITHOUT one still gets an overall height — the compiler falls back to the bounding box, which is a decision only the compiler may make (_compile_overall_height). There is then no feature to record an intent against, so an intent-level script had no way to say "and the 46 mm overall height", and a generated script replayed without it, silently and lint-clean (#889).

This verb is that line. It is deliberately NOT "draw it whenever the compiler approves one": auto_dims=False means the verbs are the whole drawing, so a dimension nobody recorded must not appear — record-then-finalize has to equal placing live.

Returns the placed names (empty when the compiler withholds the height — a Z-turned part whose step chain already tiles it, or an X/Y rotational OD that conveys it).

furniture(feature, *, view=None)

Add a hole/pattern's non-dimensional sheet furniture (#419) — centre marks (every member) plus a pattern's centre-cross (bolt circle) or pitch/grid dims.

The geometric marks a feature carries that no other verb emits: where :meth:callout draws the ø leader and :meth:locate the position dims, furniture draws the centre marks and pattern furniture. feature is a hole/pattern from :meth:model; view defaults to its end-on view. Each mark is tagged with feature so :meth:drop / :meth:annotations_of find it. Returns the placed names (varies by pattern kind — a bolt circle emits a centre-cross, a linear/grid array a pitch dim).

Raises ValueError if feature is not a hole/pattern (use :meth:dimension).

rotational(feature)

Add a rotational part's turned furniture (#424/#426) — the overall OD dimension, the axis centrelines, and any concentric-bore leaders.

The editable handle for the whole-model rotational renderer: where the per-feature verbs place callouts/locations, rotational draws the furniture the auto-pass synthesises for a part's RotationalFeature (a turned / cylindrical body). feature is the rotational feature from :meth:model. Placed by the shared :func:render_rotational — the same whole-model renderer the auto-pass runs, so a script-reconstructed drawing is byte-identical to the direct build (no only= subset, no positional-naming seam: the renderer names its own outputs dim_od / centerline_* / ldr_*). Returns [].

section()

Add the automatic full section A–A (#420) — the section half of the editable surface.

Part-level, unlike the per-feature verbs: a section fires when a Z-axis hole/pattern has a counterbore, spotface, or blind bottom (its internal profile is hidden-line-only in every ortho view), cutting through the densest qualifying row. Takes no argument (the auto A–A) and is not feature-tagged or :meth:drop-compatible — a section is atomic, so it is dropped by commenting the call. Returns the placed annotation names, or [] when no section is warranted or there is no room. Call it after the per-feature verbs — the room check carves the view row around whatever is already placed and takes the leftmost gap that fits, so it needs the occupancy to be complete. The outcome is recorded on :attr:section_decision either way (#1190).

locate(feature, *, axes=None, pin=False)

Add datum-referenced X/Y position dimensions for a Z-axis hole/pattern (#418) — the location half of the feature-referenced add surface.

Distinct from :meth:dimension (a feature's own intrinsic linear params): a location dim measures the datum → feature-centre offset, which no feature exposes as a parameter. feature is a hole/pattern from :meth:model; axes selects the in-plane axes (default both — "x" above the plan view, "y" above the side view). pin=True marks the placed dimensions as deliberate user edits: in deferred mode they still flow through the shared corridor solve, but survive/dedup as high-priority candidates and pin themselves once placed (#511). Each dim is tagged with feature so :meth:drop / :meth:annotations_of find it. In live mode, returns one placed name per distinct requested in-plane ordinate with a real offset. In deferred mode, records the intent and returns []; the names are created when the context finalizes.

Circular channels also accept this verb: their X/Y/Z offsets locate the seat axis from the stock bounding-box minimum, and axes may select any subset of those three coordinates. They use the shared profile corridor solve.

Raises ValueError for an unsupported feature (side-drilled bores are placed by the auto-pass). A feature with no datum-referenced ref (a datum-less model or a concentric/on-datum bore) returns []. Live placement handles this feature alone; automatic/deferred rendering may coalesce truly coincident ordinates while retaining every semantic owner. Placed reasonably, not via the auto-pass's corridor solve (byte-identity is not a goal, #400 Ph2).

deferred()

Record add-verb calls as placement intents, then batch-solve on exit (#426).

Inside the with block the add verbs (:meth:callout/:meth:locate/ :meth:furniture/:meth:dimension/:meth:section) record their intent instead of placing it live; on normal exit :meth:finalize drains them through the auto-pass's own solvers, so a reconstruction reaches auto-pass placement quality (crossing-free locations, the priority-drop callout solve, the turned diameter/step-length set-solves) rather than greedy live placement. This is the record-then-finalize surface the generated --script builds on.

finalize() runs on normal exit only — if the block raises, the recorded intents are left intact (finalize is skipped) so the error surfaces cleanly and a retry can re-drain. Restores the prior _defer_intents on exit. Idempotent: a later :meth:export (which also finalizes) no-ops once the intents are drained.

Do not nest deferred() blocks: finalize() drains the whole recorded list on every exit, so an inner block would place the outer block's still-pending intents early. One block per reconstruction (what the --script emitter does).

finalize()

Drain the recorded placement intents (#426).

When the drawing was built in deferred mode (_defer_intents), the add verbs recorded :class:~draftwright.intents.Intent\s instead of placing. This drains them, routing what it can through the auto-pass's own solvers — in the auto-pass's own ORDER: the drain stages are keyed by the orchestrator's canonical _PASS_SEQUENCE and executed by the shared run_stages (#699 slice b), so the two build paths cannot silently diverge in sequencing. The routed stages:

  • reserve_section — a recorded section's cutting-plane row is reserved first so the callout carve sees it as an obstacle (Coupling A);
  • live_replay — furniture, non-routed dimensions, and axes-restricted locates replay in recorded order (pop-after-success);
  • hole_callouts — hole/pattern ø callouts through _annotate_holes — the real priority-drop / central-bore-anchoring solve;
  • locations / height_ladder / step_positions / slots / user_dims — the register-only stages queue into the SHARED corridor (a slot position coincident with a hole location collapses to one dim, #345; pin/priority user dims join as first-class candidates, ADR 4 (was 0012));
  • detail_request — when detail recovery is enabled (the automatic default, persisted on BuildState) and the ladder stage recorded a "step"/"illegible" escalation, the prismatic step-height detail is queued, exactly as the auto pass gates it (#661);
  • diameters / step_lengths — the X/Z-turned set-solves place immediately, before the drain, exactly as the auto-pass runs them (a crowded X-turned head queues its enlarged DetailRequest here, #304/#307);
  • drain — one drain_and_reconcile places every queued candidate (crossing-free, deduped, monotone ladder) + the #690 label reconciliation;
  • section — renders after the drained furniture exists (its room check clears the side view's right);
  • details — every queued detail request resolves through the one generic detailer, after the drain + section so it avoids everything placed (#661 — pre-fix the finalize path never resolved the queue, so the edit path produced no detail views);
  • tabulate — dense-scattered plan holes escalate to the hole table + balloon ring via _maybe_tabulate_holes — last, so it sees the section + title block as obstacles. The density gate counts all analysis holes, so this is a full-reconstruction escalation (a partial hand-edit still tabulates the full count, #434); the escalations live only on the per-run ctx, so a repeat batch starts clean (#639).

A slot routes width, length, obround end radius and its model-derived datum position, even if fewer intents were recorded. Unsupported-axis turned callouts live-replay and raise the same ValueError as the live verb. Routing uses only-set mode; the auto-pass path is untouched.

Draining empties the list, so repeated calls and exports are no-ops until more intents are recorded. A live-replayed intent is removed only after placement; an error leaves the remaining batch available for a corrected retry.

annotations()

Return {name: type_name} for every named annotation (#27).

Lets a script introspect what is already on the drawing before adding more — e.g. if "dim_width" not in dwg.annotations() — so it can do incremental edits without risking a silent name-collision replace. Unnamed annotations are omitted; iterate :attr:items for those.

iter_annotations()

Iterate (name, annotation object) for every named annotation.

The encapsulated read path for production code (lint, sheet, sections, renderers): use this instead of reaching into dwg._named directly so the registry stays the single owner of annotation identity (#241).

view_of(name)

The owning orthographic view for name ("front"/"plan"/"side"/"rear"), or None — instead of reading dwg._anno_view directly (#241).

annotations_in_view(view)

Yield (name, annotation object) for the named annotations owned by view — the common filter-by-view read (#241).

get_annotation(name)

Return the named annotation object, or None if no such name (#27).

note(text, at, *, view=None, rotation=0.0, name=None, align=None)

Add user-positioned free text and return its annotation name.

add_table(rows, *, prefer='tr', name='table', block_cols=None, _source_id=None, _source_ids=(), _features=(), _drop_code='table_dropped', _drop_severity='warning', _cells=(), _left_align_cols=())

Fit a data table in available sheet space and report any placement drop.

add_balloons(view, specs)

Place leadered balloons in the view's reserved halo.

add_hole_table(view='plan', *, prefer='tr', name=None, balloons=True)

Add a hole table and optional matching balloons for a view.

pin(name)

Pin a named annotation so the engine never moves it (#89).

A deliberate placement — by you or an AI — must win over automatic layout. :meth:repair will not re-place a pinned annotation, and the constraint solver (ADR 2 (was 0003)) treats it as fixed. Pinning fixes the position, not existence: :meth:remove and :meth:_clear_annotations still apply. Raises KeyError if name is not a known annotation. Returns self for chaining.

unpin(name)

Release a pin so the engine may move name again (#89). Returns self; a no-op if name was not pinned.

repair(max_iter=3, *, _initial_issues=None, _on_settled=None)

Close the lint→repair loop: act on violations, don't only report them.

After the greedy initial placement, re-place the dimensions behind the mechanically-clear violations and re-lint, bounded to max_iter passes:

  • dim_inside_part — the offset is on the wrong side; flip it once.
  • annotation_ink_overlap / annotation_overlap — try the shared dimension candidate solver once, with along-span choices and at most one existing stacking tier. Pins, authored sides, annotation membership and confirmed measurements survive; every lint-code/severity component must stay the same or improve, and at least one must improve. An infeasible candidate leaves the findings.

Only engine-built dimensions (carrying placement_spec) are re-placeable; leaders, callouts and standards-judgement issues (e.g. missing_principal_dimension) are left for the caller. Each side flip is attempted at most once; a clean drawing is returned unchanged.

Wrong-side flips retain their issue-count rollback guard. Rejected ink candidates restore the original items and registry; an exception during candidate critique also restores them before propagating the error.

Returns self for chaining.

lint(*, physical=True)

Lint all annotations against all views; returns the list of issues.

When :attr:part is set, also runs :func:lint_feature_coverage. Build-time drops recorded via :meth:_record_build_issue are included.

physical=False asks for the placement critique only — geometry/standards checks over what is on the sheet — and skips the feature-coverage half that needs a recognition inventory of the solid. That is what the repair loop wants (it acts on the allowlisted placement codes in ADR 5), and on a declared build it is the difference between exporting a drawing and recognising the part to no purpose (#1022). The default stays the full critique: a caller asking "is this drawing right?" means both halves.