Sheet and fluent handles

Sheet is the declared authoring façade. Feature verbs return fluent handles; those handles are documented here because they are part of normal use even though their Python names begin with an underscore.

Projection convention

Use Sheet(part, projection="first") or build_drawing(part, projection="first") for first-angle layout and its matching projection symbol. The CLI equivalent is --projection first. Explicit "third" selects third-angle layout and its symbol; omitting projection uses third-angle layout and its symbol. Use projection_symbol=False (CLI: --no-projection-symbol) to omit the symbol for an embedded illustration; this does not change the projection convention or view layout. The visibility choice also survives generated-script replay.

View names retain their physical viewing directions in the working part frame. Changing convention changes sheet relationships, not which side of the part a name shows:

View Viewed from Page directions Third-angle position First-angle position
front Negative Y X right, Z up Reference Reference
plan Positive Z X right, Y up Above front Below front
side Positive X Y right, Z up Right of front Left of front

The resolved convention is available as drawing.view_plan.convention. Generated scripts retain the requested convention. Annotation footprints, scale/page selection and measured repacking use the same convention. An authored relation contradicting the principal layout fails before projection; a whole-view pin must preserve the convention's relationships.

Sheet margins and title-block width

Set each physical sheet edge independently with margin_left, margin_right, margin_top, and margin_bottom (millimetres). Omitted edges remain 10 mm. frame=True draws the border at those margins and reserves a further 6 mm inside it for content. zones=True adds the zone ruler and implies a frame; each margin must fit its labels.

Sergio's folded-sheet arrangement is an explicit option on A4, A3, or A2:

from draftwright import build_drawing

drawing = build_drawing(
    "part.step", page="A3", frame=True,
    margin_left=25, margin_right=10, margin_top=10, margin_bottom=10,
    title_block_width=175,
)
drawing.export("sergio", formats=("pdf", "svg"))

The same keywords work on Sheet(...) and make_drawing(...). An explicit title_block_width measures between border centrelines and aligns the block to the right and bottom sheet margins. The 175 mm block therefore fits the 210 mm folded face with 25 mm left and 10 mm right clearance. Its visible border extends half its 0.15 mm stroke beyond each centreline. Without an explicit width, the existing widths remain 120 mm on A4 and 150 mm on larger ISO sheets, with the existing extra 1 mm right/bottom clearance. Revision defaults also remain unchanged.

The standard title block derives two read-only fields from the settled drawing: UNITS is mm, matching Draftwright's drawing and export coordinate convention, and FORMAT is the effective A-series designation (for example A2). A non-standard page states its physical WIDTHxHEIGHT instead. These are derived after automatic page selection, so they cannot drift from the sheet that was actually rendered.

CLI parameters carry the same options into rendered drawings and generated scripts:

draftwright part.step --page A3 --frame \
  --margin-left 25 --margin-right 10 --margin-top 10 --margin-bottom 10 \
  --title-block-width 175 --out sergio --format pdf,svg

Add --script to write a replayable Sheet script. Margins must be finite and nonnegative; the width must be finite and positive. An explicit sheet too small for its margins and block is refused. drawing.drawable_bounds exposes the physical drawable rectangle as (left, bottom, right, top) page coordinates in millimetres.

Dimension text style

Title fields and notes at paper size

Keep title-block values concise and put longer engineering information in a notes() block. title_field_overflow identifies text wider or taller than its title-block cell, even when the text remains inside the page. This is a legibility warning, like overlapping annotations; text outside the drawable page remains an annotation_out_of_bounds error. The full value stays rendered; the diagnostic does not truncate it, shrink the font or invent an abbreviation. lint_summary()["passed"] checks errors only, so inspect warnings and the legibility component as well. A title warning does not prevent scale recovery from restoring missing measurements.

from build123d import Box
from draftwright import Sheet

sheet = Sheet(
    Box(30, 20, 10), page="A4", scale=2,
    title="FRAME - DFM REVIEW", number="REVIEW", material="SEE NOTES",
    detail_view=False,
)
sheet.authored_dimensions()
envelope = sheet.envelope()
for parameter in envelope.dimension_ids():
    sheet.dimension(envelope, parameter)
sheet.notes([
    "QUOTATION / DFM REVIEW - NOT RELEASED",
    "Material: TITANIUM - GRADE TBD",
    "Hinge fit and pin retention: engineering decision required",
], prefer="tr")
drawing = sheet.build()
for issue in drawing.lint():
    print(issue.severity, issue.code, issue.message)

page and scale control the sheet and model size. The current annotation preset has a fixed 3 mm nominal paper font size; changing scale does not enlarge notes. Sheet.notes() and Sheet.table() have no font-size option. text_position and text_orientation below control dimension style, not note size. A smaller model scale makes notes larger relative to the part; a smaller page changes how much of the page they occupy. Neither changes their printed size. For the A2 frame trial, retain A2 and the requested scale unless the full package fits the new constraints with its required measurements intact; the small box example is not proof that the frame fits A4.

prefer="tr" ranks available table positions near the top-right corner. The shared placement solve still measures the whole notes block and checks all occupied regions. If it cannot fit, table_dropped reports the failure. Split long prose into authored lines or choose an appropriate page; do not remove engineering content merely to silence a diagnostic. General notes remain uncredited text: they do not automatically satisfy missing dimensions or unsupported features.

Page selection also measures visible imported general notes before choosing a sheet. Derived sections and details claim their less-flexible space first; notes and tables take remaining regions. The late block placer prefers 6 mm of separation but retries the existing minimum ink clearance when necessary. An automatic section likewise prefers a wider gap from its parent row, and an unreserved detail prefers space around its complete footprint only when that keeps its requested scale. These are readability preferences, not permission to shrink, omit, or overlap required content.

Dimension position and reading direction

Position and reading direction are independent drawing-wide settings:

sheet = Sheet(part, text_position="above", text_orientation="horizontal")
# Declare features and dimensions as usual, then build.
drawing = sheet.build()

text_position="inline" (the default) leaves a gap for the value in the dimension line; "above" keeps the line continuous and offsets the full value and tolerance. For angular dimensions, above means outside the arc. text_orientation="aligned" (the default) follows the dimension line or arc tangent; "horizontal" keeps text horizontal on the sheet. An aligned vertical value reads from the right. Short spans use outside arrows while retaining their complete text.

The same options are accepted by build_drawing(), make_drawing(), emit_sheet_script() and generate_sheet_script(). The CLI flags are --text-position and --text-orientation. Generated scripts retain these settings; SVG, PDF and DXF export the same resolved ink. Unsupported values raise ValueError. The choices affect rendering and its placement footprint, not feature measurements or tolerances. These are explicit rendering choices, not a claim of standards conformity.

Feature-leader region policy

Feature callouts may use proved whitespace inside a projected view without exposing raw page coordinates. The drawing-wide leader_region policy is accepted by Sheet(...), build_drawing(), make_drawing(), and generated scripts:

sheet = Sheet(part, leader_region="exterior")
# or: build_drawing(part, leader_region="interior")
  • "auto" is the default and preserves the normal solve: eligible feature families offer interior candidates while retaining their established exterior fallback.
  • "interior" requires an interior candidate for an eligible, unconstrained feature. It does not invent interior geometry for unsupported families; those remain exterior. If no proved interior candidate fits, the eligible callout drops and lint reports it.
  • "exterior" removes all interior candidates and restores the historical exterior-only feature-leader layout.

The CLI spelling is --leader-region auto|interior|exterior. Generated --script output retains a non-default selection in its Sheet(...) constructor. A feature carrying an explicit side="left"|"right"|"above"|"below" remains exterior because authored placement intent is stronger than the drawing-wide preference. The policy affects feature leaders only—not linear dimensions, free notes, tables, or view placement—and never supplies coordinates.

The demand-guided annotation layout additionally reserves a blank 6 mm gutter between the front/plan annotation bands and between the column and side view, growing toward 12 mm when the selected sheet has spare room. Its facing strips cannot spend that gutter. estimated-strips retains its established shared corridors; this change does not silently alter that layout's sheet selection.

For separate linear hole patterns whose pitch marks have the same projected stations and printed value in one view, the renderer keeps one dimension rather than repeating the same pitch beside parallel rows. That mark retains the measurement identities and feature ownership of every covered pattern. Rotated or nonmatching rows remain separate; an unplaced pitch is still reported rather than silently treated as covered.

Acceptable recovered leaders

A sheet-wide recovery is a last resort, not permission to send a callout around the page. Draftwright accepts a recovered feature leader only when:

  • its tip remains attached to the proved feature in its owning view;
  • its label stays on the sheet and outside projected views and settled annotation text;
  • its shaft stays out of other views and does not cross settled annotation text or non-crossable strokes; ordinary dimension-line crossings retain the existing documented crossing policy;
  • it has at most one routing bend before the normal label shelf, never doubles back, and stays local (each leg no longer than the owning view's diagonal, the complete route no longer than twice that diagonal; a 30 mm floor keeps very small views usable).

If no candidate passes, the callout drops with a lint finding. The length and bend limits are Draftwright readability rules, not ISO- or ASME-prescribed numbers. They prevent a callout such as CTC04's 4×R13 from taking a long U-shaped detour into the next view. Ordinary feature leaders are still solved jointly; these additional limits govern the sheet-wide recovery path.

Through-hole wording

The optional argument to a hole handle's through() controls its printed indicator:

hole.through()                # default THRU
hole.through("THRU")          # explicit wording
hole.through("")              # omit the printed indicator
hole.through("THROUGH ALL")   # alternative wording

Each declares a through hole and clears any blind depth. Diameter, tolerances, fits and measurement identities remain unchanged. The empty string is an explicit display omission; it remains visible in the suppression ledger and does not count as a printed through qualifier. .depth(4) makes the hole blind and clears the override; a later .through() returns to the default.

The override is also accepted as through_indicator= by Sheet.hole() and the IR hole() constructor, including pattern members. Generated scripts preserve the difference between no override, explicit default wording and an empty string. Callouts and hole tables measure the selected wording before placement. Compatible holes can share a callout when their resolved wording, machining specifications and placement constraints agree; original feature owners and measurement identities remain separate. Removing one owner of a shared callout rebuilds the survivors through the placement solver.

Feature schedules

sheet.schedule([(feature, ("bore.diameter", "location"))], name="holes") declares a measured table through the same IR/compiler as ordinary dimensions. Features may be fluent handles or exact owned IR objects. Values, tolerances, units and through/blind wording come from the compiler. location expands the addressable member coordinates; explicit canonical IDs select individual components. Use prefer="tr", "tl", "br" or "bl" to rank the existing table placement candidates.

Schedules establish authored dimension intent. Add sheet.dimension(...) declarations when a measurement should also appear beside a view. Ordinary table(...) text has no measurement authority. See shared drawing documents for an executable recipe, verified cell evidence and the document report contract.

For an automatic pmi="annotate" drawing with multiple long imported thread/knurl requirements, the engine may instead place a manufacturing requirements table. The diameter or hole callout keeps its canonical size and a short SEE MFG n reference; the table carries the complete source-owned manufacturing terms. Its measured footprint participates in page selection, and the references are used only after the table fits. If it cannot be placed, full direct callouts remain. Lint reports a short reference whose source-matched table row is removed or altered. This does not apply to pmi="off" or replace an authored feature schedule.

An imported datum-scheme sentence is omitted from the sheet only when its whole meaning is proven by placed, source-owned datum symbols on the matching geometry. The original AP242 text and the symbol source IDs remain in the model. A missing symbol, unmatched geometry, or extra instruction keeps the note (or produces a source-coverage lint error if a previously proven symbol is later removed).

Grooves on coaxial bodies

When different turned bodies share an axis line, give their steps distinct profile_group values and use the owning body's value on s.groove(..., profile_group="outer"). The token associates features; it does not specify an annotation position or add a measurement. Generated scripts preserve this association using Draftwright-owned tokens instead of provider keys. A groove can still be declared when the authored set omits all of that body's steps.

Build-scoped declaration selectors

An editable script can give a feature a declaration identity and later address that intent without a list position:

hole = sheet.hole(diameter=6, at=(0, 0, 0), axis="z").identify("declaration:1")
sheet.dimension(sheet.by_declaration("declaration:1"), "bore.diameter")

The identity follows fluent replacements such as .depth() and identity-preserving list reorders. Assigning another feature into its public sheet.features slot or deleting it withdraws the identity; it never transfers to the new occupant. Live IDs must be unique. Generated scripts use this build-scoped selector to join assessment evidence back to editable intent. It is not a persistent feature, face or topology ID and makes no correspondence promise across recognition runs. Recognition occurrence IDs, when present, remain local to the exact generation run recorded by the declaration.

Declaration-scoped layout overrides

An agent editing a generated script can append a bounded layout override without changing the original feature declaration or supplying page coordinates:

options = sheet.layout_options("declaration:57")
check = sheet.validate_layout_override("declaration:57", side="above")
if check["supported"]:
    sheet.layout_override("declaration:57", side="above")

lane_options = sheet.layout_options(
    "declaration:9", parameter="slot_width.length"
)
lane_check = sheet.validate_layout_override(
    "declaration:9", parameter="slot_width.length", lane=3
)
if lane_check["supported"]:
    sheet.layout_override(
        "declaration:9", parameter="slot_width.length", lane=3
    )

Without parameter=, layout_options() reports the declaration's current side and supported values. With an exact declared parameter it reports dimension-lane capability. A lane is an integer from 1 through 8: a one-based drafting-spaced rank from that dimension's physical witness, not a distance. The shared measured-candidate solve may resolve it into proven whitespace inside or outside the view and records that candidate's region explicitly. The current lane-capable slice is the linear width/length dimensions of slots. Capability is declared in one compiler-owned registry so another dimension family is added deliberately, not by teaching each renderer a private spelling.

The side surface accepts above, below, left, and right; validate_layout_override() returns structured invalid_declaration, unsupported_declaration, unsupported_control, or unsupported_value refusals, plus invalid_control_combination when side and lane addressing are mixed, without mutating the sheet. Both documents say requires_build_validation: true: support means that the declaration and vocabulary are valid, not that the final sheet has enough space. build() and lint() remain the authority for feasibility and collision-free placement.

layout_override() accepts either keyword-only side, or an exact parameter plus lane, and rejects duplicate overrides for the same target. Side chooses a feature corridor; lane ranks a parallel position for one referential dimension. Neither is pinning or priority, and neither moves an annotation to a caller-supplied coordinate or changes its measurement, tolerance, datum, or other engineering semantics. Generated scripts emit the override as a separate line after the identity-bearing declarations. A declared report records the requested and resolved value under layout.overrides with intent_class: "layout-only"; lane rows also retain parameter_id. An infeasible lane drops honestly with the requested lane in the lint message. Omitting the override retains the existing placement behaviour.

Checking dimension placement rules

Call handle.dimension_ids() to find the measurements a declared feature exposes, then query a measurement before choosing its placement controls:

options = s.dimension_options(hole, "bore.diameter")
# options["placements"] contains pairs accepted by the planner's placement rules.
# None means leave that override unspecified.

check = s.validate_dimension(hole, "bore.diameter", view="plan", side="left")
if check["supported"]:
    s.dimension(hole, "bore.diameter", view="plan", side="left")
else:
    print(check["issues"], check["options"])

Both queries return versioned JSON-ready dictionaries without recording intent, preparing the Sheet, recognising geometry or rendering. Use complete view/side pairs: a side supported in one view may be unavailable in another. The axis argument follows dimension()'s parameter-variant rules; full parameter ids already name their variant. Location intent currently admits no view or side override. Invalid references, unsupported controls and unsupported placement pairs have separate structured refusal codes in validate_dimension(); dimension_options() raises on an invalid reference.

Hole-size callouts in the plan and side views accept left or right. The overall envelope height accepts left or right in the front view. These hints can separate hole sizes from vertical location dimensions without moving annotation coordinates:

sheet.dimension(lower_hole, "bore.diameter", side="left")
sheet.dimension(upper_hole, "bore.diameter", side="left")
sheet.dimension(envelope, "height.length", side="left")
sheet.dimension(lower_hole, "location")
sheet.dimension(upper_hole, "location")

Each side belongs to its named view: the front-view height and side-view hole sizes occupy different boundaries. The layout reserves space for the left height and short step rises together before choosing scale and page. The shared solver places the annotations within those boundaries; generated scripts retain the authored sides. An explicit side is a constraint: an infeasible placement reports the affected measurement instead of silently choosing the opposite boundary. Repair preserves an authored overall-height side, including after a label adjustment.

The explicit single_dimension_placement_rules scope means supported reports acceptance by the current planner placement rules. Both documents set requires_build_validation: true: whole-part classification can select a different renderer, so this query does not prove actual rendered support. For example, a boss on a turned part can use a different diameter renderer from a boss on a prismatic part. Agreement with other authored dimensions, visibility in an authored view set, available space, completeness and collision-free placement also require a build. Build and lint still assess the resulting Sheet. A query never replaces an existing dimension request. Keep the original feature handle for edits; these reports do not introduce persistent feature identities or new lane, route or pin controls.

Angular measurements

Sheet.angle() derives an included angle from a vertex and two witness points in model coordinates. Select its canonical included.angle measurement with dimension(). The engine chooses a true-angle principal view and places the arc, arrows and complete compiler-owned label through the shared corridor solver.

from math import sqrt
from build123d import Polygon, extrude
from draftwright import Sheet

part = extrude(Polygon((0, 0), (30, 0), (15, 15 * sqrt(3)), align=None), amount=3)
sheet = Sheet(part).authored_dimensions()
angle = sheet.angle(
    vertex=(0, 0, 3), first=(15, 0, 3), second=(7.5, 7.5 * sqrt(3), 3),
)
angle.tolerance(0.05, on="included.angle")
sheet.dimension(angle, "included.angle")
drawing = sheet.build()
for finding in drawing.lint():
    print(finding.code, finding.message)

The default sector="minor" is the non-reflex angle towards the two witness points. sector="opposite" selects the vertically opposite sector, extending both supports through the vertex. It preserves the numerical angle and can keep an exterior dimension beside its corner. The engine never silently switches between these sectors or substitutes a supplementary angle. Reversing witness order preserves the selected angle. For extended supports meeting beyond a rounded corner, use virtual_vertex=True; this declares the virtual intersection without asserting that the vertex lies on a physical edge. Zero-length or collinear rays, oblique planes and unsupported sectors are rejected. Insufficient page space produces angular_dimension_dropped against the named measurement. Symmetric tolerances and lower/upper deviation magnitudes retain degree units, including small deviations when the nominal display is coarse. Omitting the dimension request suppresses the angle; generated scripts retain its reference geometry and tolerances.

After building, drawing.dimension(feature, "included.angle", pin=True, priority=2) uses the shared corridor in live and deferred edits. As with other edits, use an exact feature from drawing.model(). Measurement comparison records the angular references, so equal-valued support substitutions cannot appear preserved merely because the labels match.

Sheet.angle_pattern(*references) declares repeated coplanar corners using AngularReference values from draftwright.model. Each corner remains independently addressable as included.angle.member1, included.angle.member2, and so on, in declaration order. Request every member with dimension() to permit one quantity label such as 3× 60°. Different member tolerances or side preferences produce individual labels; omitting one member suppresses that measurement without renumbering the others. The generated script retains the full member geometry and each request.

After the conservative strip solve, curved dimensions have up to three corner-local radius alternatives at the existing tier spacing. The shared placement stage accepts a shorter radius or recovers a dropped candidate only when its actual lines and label clear fixed and same-batch ink and fit the page. Pinned dimensions retain their solved position.

The lower-level measured_dimension(kind="angular", ...) route retains nominal author-supplied text. Its explicit AngularReference supplies the same ray geometry; plain Sheet-authored three-point ref_pts use (first, vertex, second). Imported PMI requires explicit ordering. Structured tolerances on that raw route remain unsupported; use the canonical declaration for new authored angles. Lint compares degree values at their displayed resolution, inspects the visible angular ink and checks complete canonical labels against the compiler. Physical critique checks each claimed corner against finite supports in the cached provider evidence, including members represented by a quantity label. Unprovable correspondence produces angular_support_unverifiable; contradictory supports produce angular_support_mismatch.

Automatic drawings derive outside-profile angular requirements from ordered face supports, available since Quiddity 0.2.6. Angles already defined by a recognised chamfer or regular-polygon callout do not add duplicate automatic requirements. This requires the same run's exact face or shared-edge evidence, not matching numerical angles; explicit angle declarations remain available on those corners. Verified profile repetitions can share a quantity label; equal numerical angles alone do not establish a pattern. The requirement audit retains one outcome per physical corner even when several share one mark.

Selecting a hole location component

Use location alone to request the usual location set. To select one component of a hole group or pattern, pair its declared member index with the measured world axis:

options = sheet.dimension_options(holes, "location")
print(options["location_components"])  # valid member/axis pairs and their parameter ids
sheet.dimension(holes, "location", member=1, axis="y")

Member indices start at zero and refer to the declared members tuple. A singleton hole has member 0. The axis is transverse to the hole's own axis: an X-directed hole can have Y and Z location dimensions. A bolt-circle pattern also accepts member="centre". Both selectors are required for a fine request; invalid members or axes are refused. The existing validate_dimension() query accepts the same selectors.

The coarse pattern request keeps its automatic anchor policy: the member nearest the datum, or the centre of a bolt circle. Adding a fine request selects that extra component without selecting its sibling axis. Repeating a request does not duplicate a measurement. Generated scripts carry the member and axis so removing one location declaration omits that component. Coincident dimensions can share one mark while recording every approved owner.

These indices address one declaration and its generated script. They do not establish correspondence across unrelated recognition runs. The shared solver still chooses where to place the dimension; location selection supplies no page coordinates.

Sheet

Comparing measurements after a declaration edit

compare_measurements() compares named compiled measurements, including their owner, parameter, nominal value, tolerance, directional span and rendered claim text. Linear dimensions also retain their measured path lengths in model units, so exchanging labels between two spans cannot hide behind an unchanged label multiset. Recorded per-annotation spans and location components also distinguish equal-valued axes under a coarse parameter id. These are provenance claims; independent lint must still check physical targets. Reuse the feature objects in a declaration when editing placement intent:

from dataclasses import replace
from draftwright import build_drawing
from draftwright.audit import compare_measurements

original_model = sheet.model()
edited_model = replace(
    original_model,
    authored_dimensions=tuple(
        replace(request, side="left") if request.role == "height.length" else request
        for request in original_model.authored_dimensions
    ),
)
before = build_drawing(part, model=original_model)
after = build_drawing(part, model=edited_model)
result = compare_measurements(before, after)
print(result["status"], result["lost"], result["changed"], result["unknown"])

The status is preserved, changed or unknown. An unresolved owner or unconfirmed claim prevents preserved. For an in-place edit, capture before = drawing.measurement_snapshot() before changing the drawing, then compare that snapshot with the edited drawing. Snapshots retain process-local feature references.

Separately rebuilt declarations do not acquire correspondence from equal geometry or inventory order. If the caller knows the correspondence, pass feature_pairs=((old_feature, new_feature), ...). Each pair must name exact owners in the respective snapshots and the correspondence must be one-to-one. These are caller assertions; the comparison checks measurement meaning under them. Output owner numbers are positions in the before snapshot for presentation, not durable identities.

diff_builds() includes this result as measurement_comparison and detects same-kind substitutions between shared declared owners. A clear annotation diff can still have unknown measurement correspondence. Use independent lint_summary()["quality"] evidence alongside the comparison: named-measurement preservation does not establish physical completeness, pin preservation or release readiness.

Sheet

Bases: _SheetViewMethods

Reference features, declare their drawing aspects, export.

Each declaration method mirrors a :mod:draftwright.model constructor: pass the build123d object to read its geometry, or explicit values. :meth:hole returns a chainable :class:_Hole (.through() / .depth()), :meth:diameter / :meth:step a :class:_Dim, for their own aspects; :meth:pocket / :meth:slot / :meth:rectangular_blind_slot / :meth:envelope a :class:_Params (.tolerance(on=…)) which forwards unknown attributes to the Sheet so those verbs still chain to any further declaration (preserving their prior return-Sheet behaviour); the remaining verbs return the Sheet. :meth:build / :meth:export hand the declared features to the engine with detection skipped.

features property

The declared IR features — mutable: override, drop or reorder before :meth:build.

Each feature carries an identity token (#908), so a reorder via :meth:~_FeatureView.reverse or :meth:~_FeatureView.sort moves every reference with it — a handle, a tolerance, a GD&T origin, a section, an add_dimension intent all follow their feature to its new position.

Assignment is not a move. features[i] = f and slice assignment mint a new identity, because assignment cannot distinguish "move this feature here" from "put a different feature here" — so references to the displaced feature fail loudly rather than silently transferring to whatever replaced it. The same holds for deletion. Use reverse/sort to reorder while keeping references.

view_constraints property

The immutable pre-projection view request authored on this sheet.

from_part(part, **opts) classmethod

Seed the declared set from detection (the hybrid mode, ADR 4 (was 0011 §3)): start from the model the detector recovers, then override specific features (edit the list via :attr:features, or re-declare) before :meth:build.

This states the automatic dimension source (#874), because that is what asking for detection means: you have asked for the engine's reading of the part, features and dimensions alike. It is not the implicit default the breaking change removed — a Sheet(part) still has to say — it is from_part's own meaning.

Because the choice is from_part's rather than a script line's, adding dimension(...) declarations overrides it rather than conflicting: detect the features, then declare exactly which of their measurements to draw. That is the natural way to take over a detected drawing, and requiring the caller to redeclare every feature by hand to reach it would be a poor trade (#921). An explicit auto_dimensions() still conflicts — there the script has said both things.

add(feature)

Register a pre-built IR :class:~draftwright.model.Feature (escape hatch for the constructors this façade does not surface directly, e.g. PMI).

Adding the exact feature object already in :attr:features returns a handle to its existing registration. Equal-valued distinct objects remain separate features. Other inputs raise :class:TypeError before changing the sheet.

Returns a handle, like every declaration verb (#922). It matters here more than it looks: the ENVELOPE is emitted through this escape hatch rather than through :meth:envelope, so while add returned the sheet, sheet.dimension(env, "width") — ADR 4 (was 0016)'s own worked example — could not be written against a generated script. Naming would have been uniform across the verbs and silently absent for one feature in the middle of the file, which is worse than being absent everywhere. A raw ControlFrame or DatumRef may name a handle as its origin; add resolves and token-binds that provenance exactly like the public GD&T verbs.

dimension(feature=_UNSET, role=_UNSET, *, axis=None, member=None, view=None, side=None, **removed)

dimension(feature, role) — the ADR 4 (was 0016) referential verb. See :meth:_authored_dimension for the semantics.

The signature is the real one again (#720): the transitional call-shape dispatch to :meth:measured_dimension was removed at 0.4.0, so dimension means one thing. That restores most of what the @overload pair provided — the parameter names and the DimensionIntent return a caller sees (#963) — without the dual shape.

One thing it does NOT restore: **removed means a type checker accepts any keyword rather than rejecting an unknown one (#720). That is the price of catching the legacy call at runtime to name its replacement; the alternative is a static error whose text is about argument counts. Worth revisiting once the break is old news, at which point **removed can go and the signature becomes exact.

feature/role default to a sentinel rather than being required so that a keyword-only legacy call (dimension(kind=…, value=…)) reaches the message below instead of a bare "missing 2 required positional arguments". The old shape never appeared in a release, so this refusal is the only notice it gets — a documented break (docs/deprecations.md).

view selects front/plan/side/rear and side selects the corresponding above/below/left/right corridor where that dimension renderer supports it. They express authored placement intent, not page coordinates; invalid or unrenderable pairs fail clearly during planning.

dimension_options(feature, role, *, axis=None, member=None)

Discover view/side pairs accepted by planner placement rules, without building.

Uses the same handle/id resolver as :meth:dimension and the planner's placement rules. None means omit that override. Pairs are intentional: a side supported in one view need not be supported in another. The result is JSON-ready.

Scope is one dimension's planner placement rules. Whole-part classification can select a different renderer, so acceptance does not prove actual rendered support. The full authored set, chosen views, compound-callout agreement and collision-free placement also require build validation. Keep the supplied feature handle to address subsequent edits; the result creates no persistent feature identity. Invalid feature/measurement references raise as they do in dimension. The query neither records an intent nor prepares, recognises, or renders the part.

validate_dimension(feature, role, *, axis=None, member=None, view=None, side=None, **unsupported)

Preflight a proposed dimension call without recording or rendering it.

Returns supported, structured issues and the valid options for this target. supported means accepted by the current planner placement rules; actual renderer support and whole-sheet feasibility still require build validation. Existing authored intent is never replaced or modified. Unknown controls are refused rather than ignored or passed on to a renderer.

measured_dimension(*, kind, value, label, dominant_axis, ref_pts, ref_bbox=None, at=None, axis=None, upper_tol=None, lower_tol=None, lower_bound=None, upper_bound=None, source='sheet', source_kind=None, source_id='', lowering_blockers=(), rendering_blockers=(), cylindrical_refs=(), circular_refs=(), view=None, side=None, angular_reference=None, angular_references=(), angular_member_ids=(), angular_reference_item_groups=())

Declare a drafting dimension from explicit measured values.

Named for what it carries (ADR 4 (was 0016) / #873). dimension is referential on Drawing today and becomes so on :class:Sheet in #874 — it names a feature and a role, and the engine reads the value off the geometry. The verb that carries a number of its own needed a name saying so before that name could be reused. A measured dimension is the one place a value does NOT come from the part.

This is the concept-shaped Sheet API used by generated AP242 scripts: the source file may call the record PMI, but the editable script declares a dimension category, value, label, referenced model points, and optional structured tolerances. For ordinary geometry-backed edits prefer feature handles such as sheet.hole(...).tolerance(...). lower_bound and upper_bound are the mutually exclusive alternative to upper_tol/lower_tol for a limit range. source_id is the external record identity retained by generated imported-PMI scripts; hand-authored dimensions normally leave it blank. lowering_blockers carries the explicit reason a supported imported requirement could not safely enrich a canonical feature parameter; rendering_blockers carries the source-geometry reason it cannot be drawn truthfully. view/side select a supported semantic corridor while leaving its actual position to the normal placement solve. angular_reference names a model-space vertex, first and second ray witness, with optional virtual_vertex=True for extended supports. Its only supported sector is the non-reflex "minor" sector. Supply it as an AngularReference or mapping; empty ref_pts are filled from that reference. A Sheet-authored angle with exactly three ref_pts uses the order (first, vertex, second). Generic imported PMI stations require an explicit angular reference; their order cannot be inferred from the point count. Geometric intent is preserved even when angular rendering is unavailable. Delegates to :func:draftwright.model.declare.measured_dimension (#704), so build_drawing(model=…) callers can author the same feature without the façade.

of(ref)

A decoratable handle onto an existing feature — the hybrid seam (#463).

ref is a fluent handle this sheet issued, a feature index, a :class:Feature already in :attr:features (e.g. seeded by :meth:from_part), or the build123d object you built (matched by ⌀ + in-plane position). Returns the same fluent handle the declaration verbs do, so you can .fit(...) / .tolerance(...) — and, for a hole, .cbore(...) — a feature you did not declare from scratch. Raises if the object matches no feature or is ambiguous.

by_declaration(declaration_id)

Return the live handle carrying declaration_id, or fail if it was withdrawn.

layout_options(declaration_id, *, parameter=None)

Describe the bounded layout-only controls supported by one declaration.

This is a pre-build capability query, not a feasibility promise: the shared layout solve still decides whether the requested corridor can be used on the final sheet.

validate_layout_override(declaration_id, *, side=None, parameter=None, lane=None, **unsupported_controls)

Preflight a layout override without mutating the sheet.

A supported result means the declaration and bounded vocabulary are valid. It still requires :meth:build to establish geometric feasibility on the composed sheet.

layout_override(declaration_id, *, side=None, parameter=None, lane=None)

Append one bounded declaration-scoped layout override.

side selects a feature corridor. parameter + lane selects a one-based parallel lane for one declared referential dimension. Neither form accepts page coordinates; the shared placement solve resolves and validates the physical offset.

hole(obj=None, **kw)

Declare a hole from the tool cylinder you subtracted (or explicit values). Returns a fluent handle: .through() (default) / .depth(d).

double_d_bore(obj=None, **kw)

Declare a double-D bore from its cutter or explicit major ⌀ and A/F values.

diameter(obj=None, **kw)

Declare an external cylindrical diameter (a boss / OD) — the ⌀ is read off the object. Returns a handle: chain .tolerance(...) for a ± on the ⌀ (P2a).

boss(obj=None, **kw)

Alias of :meth:diameter — an external cylindrical boss / OD.

polygonal_boss(**kw)

Declare a regular polygonal-prism boss sized across flats and by height.

polygonal_stock(**kw)

Declare whole regular polygonal-prism stock sized A/F and axially.

external_spur_gear(**kw)

Declare one complete metric external spur involute gear requirement.

step(obj=None, **kw)

Declare one axial segment of a turned profile (its OD + length). A model with any step renders as a turned part. Returns a handle: .tolerance(...) tolerances the step length by default, .tolerance(..., on="diameter") its OD (P2a).

slot(obj=None, **kw)

Declare a milled slot (width + length, with optional end_radius).

oriented_slot(**kw)

Declare a free-direction rectangular through slot and its source passage.

pocket(obj=None, **kw)

Declare a blind rectangular recess — a floored slot/pocket (width × length × depth). From an object the depth axis defaults to the shortest bbox span; pass depth_axis= for a recess deeper than it is wide.

rectangular_blind_slot(**kw)

Declare a capped, edge-open rectangular U-section slot.

All arguments are explicit because cutter geometry alone cannot establish its open end, terminal wall, or material-opening direction.

round_bottom_blind_slot(**kw)

Declare a capped, edge-open slot with a flat floor and equal round sides.

All arguments are explicit because detached cutter geometry cannot establish the open end, terminal wall, material-opening direction, or round-bottom ownership.

channel(**kw)

Declare a full-span floored channel (wall-to-wall width only).

pad(obj=None, **kw)

Declare a bounded rectangular raised pad (footprint + in-plane location).

Every pad owns its terminal-to-attachment height; a Z profile level measures a different datum-to-attachment fact and does not replace it. axis= and direction= preserve all six signed principal orientations.

chamfer(obj=None, **kw)

Declare a chamfer (bevelled edge) — sheet.chamfer(bevel_face) reads axis, legs and a point on the bevel off the oblique chamfer face, or explicit sheet.chamfer(axis="z", leg=6, at=(x, y, z)). leg = equal-leg 45° (C{leg}); leg1/leg2 = asymmetric. turned=True declares that axis is the shaft axis and selects its profile view; generated Sheet programs preserve this flag.

fillet(obj=None, **kw)

Declare a fillet (rounded edge) — sheet.fillet(round_face) reads axis, radius and a point on the round off the cylindrical blend face, or explicit sheet.fillet(axis="z", radius=3, at=(x, y, z)). Called out R{radius} (grouped n× R for equal radii). turned=True declares that axis is the shaft axis and selects its profile view; generated Sheet programs preserve this flag.

blend(**kw)

Declare a complete straight or circular rolling-ball blend path.

Explicit-only because one detached face cannot prove whole-path ownership or Fillet precedence. path_kind='circular' additionally requires the centre-line path_radius; non-principal paths retain axis_direction and receive one radius callout.

paired_ramp_step(**kw)

Declare a mirror-symmetric paired-ramp step by its axis, equal acute angle, open-to-terminal run length and shared-ridge midpoint. The form is explicit-only: a detached face or cutter cannot prove the paired material-removal topology.

gusset_rib(**kw)

Declare a triangular reinforcing rib or a proven linear/mirror pattern.

angle(**kw)

Declare an included angle from vertex/first/second model-space points.

The value is derived from those rays. Select included.angle with dimension() and use the usual tolerance, omission and placement controls. No number or annotation radius is authored.

angle_pattern(*members)

Declare repeated AngularReference corners with independent member IDs.

The parameters are included.angle.member1, member2, etc. An authored omission retains the other member identities; tolerances may target one full parameter ID or the whole included-angle family.

hex_pocket(**kw)

Declare a blind regular hex from physical mouth geometry.

circular_channel(**kw)

Declare a cylindrical seat by axis, radius, run and three physical arc points.

circular_blind_step(**kw)

Declare a quarter-cylindrical corner cut by radius, stopped depth, oriented terminal-to-open centreline and canonical transverse quarter-arc section. The form is explicit-only because one detached face cannot prove the blind cut.

through_step(**kw)

Declare a rectangular open-profile step spanning its run axis.

The explicit section is (envelope endpoint, concave corner, envelope endpoint) in the two non-run coordinates. Its two legs are independently dimensioned in the end-on view; the through run is already owned by the envelope.

flat(obj=None, **kw)

Declare a machined flat on round stock (#148b) — sheet.flat(flat_face) reads the leader point off the planar flat face (axis= and across= still required), or fully explicit sheet.flat(axis="z", across=15, at=(x, y, z)). Called out {across} A/F (across flats — flat-to-flat for a double-D / hex, the D height for a lone flat).

groove(obj=None, **kw)

Declare a turned / circlip groove on round stock (#148c) — sheet.groove(floor_face) reads axis, width, diameter and the leader point off the reduced-OD floor face, or fully explicit sheet.groove(axis="z", width=3, diameter=16, at=(x, y, z)). Called out {width} WIDE × ø{diameter} (groove width + floor diameter).

plate(obj=None, **kw)

Declare a thin slab's thickness (a base plate / wall / rib) — sheet.plate(slab_box) reads the thin axis + extent + centre off the slab, or explicit sheet.plate(axis="z", lo=0, hi=4, u=10, v=5). Only a multi-plate part dimensions plates (a single slab is the envelope).

rotational(**kw)

Declare a turned body's axial furniture — OD, rotation axis, concentric bores (#945): sheet.rotational(od=30, bores=(16,), axis="z").

The last recognised kind without a declarative surface, which is why a generated script for a turned part could not declare its dimensions at all (#938).

Keyword-only, unlike its object-capable siblings — see :func:draftwright.model.rotational for why an object form would disagree with detection (#950).

step_level(obj=None, **kw)

Declare a prismatic height ladder + step-position shoulders (a rebated / stepped block) — sheet.step_level(part) reads base / interior levels / the (axis, position) shoulders / datum off the part, or explicit sheet.step_level(base=0, levels=(10,), shoulders=(("x", 30),)). A shoulder locates where a step changes height along a horizontal axis, so a stepped block is fully constrained (#555/#578).

pattern(member, **kw)

Declare a hole pattern (bolt circle / linear array / grid) — build the member with :func:draftwright.model.hole.

pocket_pattern(member, **kw)

Declare a linear/grid array of identical blind pockets (#841) — build the representative member with :func:draftwright.model.pocket. Renders as one grouped N× W × L × D DEEP callout + (n-1)× pitch dim(s), not N competing size dims.

slot_pattern(member, **kw)

Declare a linear/grid array of identical milled slots (#841) — build the representative member with :func:draftwright.model.slot. Renders as one grouped N× SLOT W × L leader + (n-1)× pitch dim(s), not N competing size dims.

envelope(obj=None)

Declare the overall bounding dimensions. Defaults to the whole part.

The no-argument form reuses an equal whole-part envelope already seeded by :meth:from_part; its returned handle therefore decorates and dimensions that detected feature instead of appending a duplicate. An explicit obj remains a distinct declaration, including when another envelope is already present.

general_tolerance(designation, *, statement='', source_id='', part21_id='')

Declare the document-wide general tolerance printed in the title block.

default_surface_finish(ra, *, statement='', source_id='', part21_id='')

Declare the document-wide surface finish shown beside the title block.

document_note(text, *, kind, source_id='', part21_id='', on_drawing=True, represented_by_source_ids=())

Declare a document statement, optionally retained as model-only metadata.

datum(letter, ref, *, view=None, side=None)

Declare a datum feature symbol (ISO 5459). ref is a build123d planar face, or a feature handle / :class:Feature / index for a feature's axis. The target view + strip side are derived from the geometry; view/side override them (ADR 4 (was 0011) P2c).

finish(ra, ref, *, view=None, side=None)

Declare a surface-finish symbol (ISO 1302, Ra) on ref — a build123d planar face or a feature. sheet.finish("3.2", top_face); view/side override the strip.

note(text, ref, *, satisfies=(), view=None, side=None)

Declare a manufacturing note (#488) on a leader to ref — a build123d planar face or a feature. The shop callouts detection can't infer: thread specs (sheet.note("M3x0.5 TAP", bore)), DEBURR, chip-relief, knurl. Placed like the GD&T items, clear of the views/title block; view/side override the derived strip. satisfies may name canonical parameter ids only when ref is a feature; it grants coverage only when this structured note is placed, never by parsing its prose (#1351).

structured_note(text, ref, *, satisfies=(), view=None, side=None)

Declare a manufacturing note and return its own addressable feature handle.

This bindable spelling is for editors and generated scripts that must identify the note declaration itself. Placement remains solver-owned; the handle carries no page coordinates. :meth:note retains its Sheet-returning fluent behavior.

control(ref, *, view=None, side=None)

A GD&T feature-control-frame builder on ref — a feature handle / :class:Feature / index, or a build123d planar face. Chain one method per ISO 1101 characteristic (.position(0.1, to="A B") …); each stacks a frame on the target. The target view + strip are derived from the geometry; view/side override them (ADR 4 (was 0011) P2c.2).

schedule(rows, *, name, prefer='tr')

Declare a measured table from (feature, parameter_ids) rows.

location selects every addressable hole member's transverse distances. Explicit canonical IDs select individual measurements. Values, units and tolerances come from the compiler; the existing table solve owns placement. This declares authored dimension intent, like :meth:dimension.

table(rows, *, prefer='tr', name=None, block_cols=None)

Declare a corner-block data table — positioned at :meth:build by the engine's generic auto-placer, clear of the views, title block, and annotations (the same machinery as the hole table), and lint-checked. rows is a sequence of equal-length row sequences (row 0 the header); cells are stringified. prefer ranks the free candidates by the page corner to sit nearest ("tr"/"tl"/"br"/"bl"); it does not exclude viable interior or opposite-corner space. A table with no fitting free region records an inspectable table_dropped lint — it never overlaps. Revision blocks, BOMs and schedules all use this; :meth:notes is the single-column convenience over it.

notes(lines, *, title='NOTES', number=True, prefer='tr')

Declare a manufacturing NOTES block — a single-column :meth:table of lines with a title header (None to omit) and optional 1 … auto-numbering::

sheet.notes(["BREAK ALL EDGES 0.3", "DEBURR", "M3x0.5 TAP"])

auto_dimensions()

Soft deprecated (#1043) — prefer :meth:authored_dimensions in new code.

Supported, and not scheduled for removal: existing scripts keep working. But on this surface, authored dimensions are the right default, for three reasons:

  • it is what draftwright itself emits — --script writes authored_dimensions() with a line per dimension, so the automatic path is the only form the tool never produces;
  • omission means suppression (ADR 4 (was 0016)) only holds for an authored set. Under an automatic one you cannot express "not that one";
  • an authored list is editable text. That is what makes "generate a script, then refine it" work, for a person or a model.

If you want the planner's choices as a starting point, generate them — draftwright yourmodule:part --script — and edit the result, rather than asking for them at runtime where they cannot be seen or changed.

Still asks for the planner's automatic set (ADR 4 (was 0016)). A build must say where its dimensions come from rather than defaulting silently (#874), and this is one of the two ways to say it. It is also what :meth:add_dimension augments.

build_drawing(part)'s automatic path is unaffected and carries no warning — that is the detected front door, and being automatic is its whole point.

authored_dimensions()

Declare that the :meth:dimension lines in this script ARE the complete set.

The other half of :meth:auto_dimensions, and until #933 it did not exist: the authored source was entered implicitly, as a side effect of calling dimension() at least once. That left one thing unsayable — the complete set is empty — because absence of dimension(...) lines is also what a script with no source at all looks like, which _check_dimension_source must reject. So a valid PartModel carrying authored_dimensions=() built directly but could not be written as a script.

Calling dimension(...) still selects this source on its own; the verb only makes the choice sayable when there is nothing else to say it. Emitted scripts write it unconditionally, so an authored script states its source with a verb rather than with a comment a reader has to trust (ADR 4 (was 0016) / #874).

add_dimension(feature, role, *, axis=None, member=None, view=None, side=None)

Augment the planner's set with one more measurement (ADR 4 (was 0016) / #872).

feature is a declared-feature handle (what :meth:hole, :meth:boss, … return), an index into :attr:features, or the IR feature itself. role names the measurement — "bore.diameter", "grid_pitch", … — and carries no number: the value is read from the geometry, so the size still lives in exactly one place.

Returns a :class:DimensionIntent.

Soft deprecated (#1043) — supported and not scheduled for removal, but it exists only to augment :meth:auto_dimensions, which is itself discouraged here. To add a measurement to an authored set, write a :meth:dimension line: same effect, and the result is visible in the script rather than computed at runtime.

Requesting a measurement the planner already emits is a deliberate no-op, not an error: a script should be able to ask without first knowing the rule set's mind.

axis disambiguates a role a feature carries more than once — today a grid pattern's two pitches. Omitting it there raises rather than picking one, because a silent coin toss between the row and column pitch is the kind of wrong a reader cannot see.

view/side have the same solver-owned placement semantics as on :meth:dimension.

model()

The IR the engine will draw (detection skipped) — for inspection. Wraps the declared features into a :class:PartModel without rendering a drawing (#453): the same wrapping :meth:build hands the engine (part bbox + corner datum + step- inferred orientation + the P2a decorations), so inspection pays no projection/anno cost and can't hit a layout/render failure. Wraps the solids body (as :func:_analyse does), so the bbox/datum match what build() draws even when the part carries bbox-extending non-solid geometry.

Gated on the dimension source like :meth:build (#874): this is the model the engine WOULD draw, so a sheet that cannot be built must not hand one out — otherwise build_drawing(part, model=sheet.model()) is a way around the check, and the two public surfaces disagree about the same sheet (the #707 class of divergence).

build()

Build the :class:~draftwright.drawing.Drawing — detection skipped; only the declared features are drawn. Declared corner-block tables (:meth:table/:meth:notes) are placed last, clear of everything already on the sheet.

export(stem=None, *, formats=('pdf',), dpi=150)

Build the drawing and write the requested formats — a format name or an iterable from ("svg", "dxf", "pdf", "png"), default PDF (matching the CLI); return {format: path}. dpi sets the PNG raster resolution. stem defaults to the drawing number, lower-cased.

formats=None means "unspecified", so it takes this method's PDF default. :meth:Drawing.export requires an explicit format selection.

take_over(*, dimensions, principal_views, derived_views)

Adopt a detected baseline with explicit, independently chosen sources.

This is the public transition from :meth:from_part's detected features and implicit automatic dimensions to an editable Sheet request. Principal and derived views are separate sources (ADR 2 (was 0018)): keeping automatic principal planning while authoring the complete derived set replaces an inferred section instead of augmenting it; an empty authored derived set explicitly suppresses every inferred section/detail.

The declaration is atomic and order-independent. Matching declarations made before this call are accepted, while an explicit contradictory source raises without changing any source. The incoherent ADR 2 (was 0018) combination -- authored views with automatic dimensions -- is always refused.

authored_views()

Declare that subsequent :meth:view lines are the complete principal set.

Calling this with no view(...) lines makes the empty authored set explicit. It remains a request even when a later build finds that no projectable drawing can satisfy it; absence of the verb retains the behaviourally-compatible automatic default.

auto_views()

Select automatic principal and derived views, optionally augmented by add verbs.

view(name)

Add one view to the complete authored principal/orientation set.

add_view(name)

Require one additional principal/orientation view in an automatic set.

section_view(label, through=None, *, at=None)

Author a named section view through a declared feature or explicit Y cut plane.

add_section_view(label, through=None, *, at=None)

Augment automatic derived views with one named section.

detail_view(label, around)

Author a named detail view around a declared feature.

add_detail_view(label, around)

Augment automatic derived views with one named detail around a declared feature.

section(feature=None, *, at=None)

Request a full section A–A (#841) — the part-level verb behind the auto section.

A section fires automatically only when a Z-axis hole/pattern has a counterbore, spotface, or blind bottom; a blind pocket has no such driving hole, so its floor and depth stay hidden-line-only. This forces a cut so that internal profile reads.

The cut plane is normal to Y. feature — a fluent handle / :class:Feature / index — cuts through that feature's centre (the natural "section through this pocket"); at=<y> cuts at an explicit Y; bare section() cuts through the part centre. The section renders last (its room check clears the right-of-side-view band), so declare it after the per-feature verbs. Chainable.

detail()

Ensure enlarged detail-view recovery is enabled (#42/#307/#841).

Automatic builds enable it by default; this verb is useful after constructing a Sheet(..., detail_view=False) or when an emitted declaration should state the choice explicitly. Adds a magnified crop of the step-height region when warranted (a no-op otherwise). Chainable. Not feature-targeted — for a blind pocket's floor/depth prefer :meth:section.

row(*views, gap=None)

Constrain complete view blocks into a left-to-right row.

column(*views, gap=None)

Constrain complete view blocks into a bottom-to-top column.

Authored views and layout

View declarations are semantic constraints. They name projections, relationships and whole-view anchors; the layout solve owns the resulting page positions. Authored views must be paired with authored dimensions so a deliberately omitted view cannot strand planner-selected annotations:

This runnable example includes the part, feature declarations, dimension discovery, three principal views and section A–A. The bore's location dimensions are deliberately omitted, and lint keeps those omissions visible.

from build123d import Box, Cylinder, Pos
from draftwright import Sheet

bore_tool = Pos(12, 0, 0) * Cylinder(3, 12)
part = Box(60, 40, 12) - bore_tool
sheet = Sheet(part, page="A3", scale=1, projection="third")
sheet.authored_dimensions().authored_views()

envelope = sheet.envelope(part)
for parameter_id in envelope.dimension_ids():
    sheet.dimension(envelope, parameter_id)
bore = sheet.hole(bore_tool)
print(bore.dimension_ids())  # discover this handle's Sheet measurement IDs
sheet.dimension(bore, "bore.diameter")

for name in ("front", "plan", "side"):
    sheet.view(name)
sheet.section_view("A", through=bore)  # or at=0 for an explicit model-space Y plane

drawing = sheet.build()
issues = drawing.lint()
for issue in issues:
    print(issue.severity, issue.code, issue.message)
# drawing.export("build/section", formats=("pdf", "svg"))

On a Sheet handle, dimension_ids() lists dotted measurement IDs; use sheet.dimension(bore, "bore.diameter"). On a built Drawing, the second argument to drawing.dimension(envelope_feature, "length", role="width") is a parameter kind, with role= distinguishing measurements of the same kind. Hole diameter and depth are callout content: use drawing.callout(bore_feature) for those, not drawing.dimension(...). These are different vocabularies.

Augmenting automatic views

view() and section_view() specify complete authored sets. add_view() and add_section_view() instead augment an explicitly automatic set. Choose the source before adding views; an authored set cannot be switched to automatic in place.

This separate, runnable example keeps automatic principal and derived views and adds a section at model-space Y=0. Its dimensions are still explicitly authored:

from build123d import Box
from draftwright import Sheet

part = Box(60, 40, 12)
sheet = Sheet(part, page="A3", scale=1).authored_dimensions()
envelope = sheet.envelope(part)
for parameter_id in envelope.dimension_ids():
    sheet.dimension(envelope, parameter_id)
sheet.auto_views()
sheet.add_section_view("A", at=0)
drawing = sheet.build()
issues = drawing.lint()
for issue in issues:
    print(issue.severity, issue.code, issue.message)

auto_views() emits SoftDeprecationWarning: it remains supported and has no removal date. The notice recommends the editable authored surface; it does not mean the augmentation example is invalid. Automatic dimensions remain supported too, but they cannot be paired with a complete authored view set.

Sheet.section() is deprecated with removal targeted at 0.6.0. For an authored workflow replace it with section_view("A", through=feature) or section_view("A", at=y); for automatic augmentation use auto_views() followed by add_section_view(...). Drawing.section() is a separate automatic post-build operation and may add no section when the geometry does not warrant one.

Rear views are explicitly requested. s.view("rear") includes rear in an authored view set; s.auto_views().add_view("rear") adds it to the automatic baseline. Rear looks toward the part from +Y with Z upward, so increasing world X runs left on the page. This physical direction is the same under both projection conventions. When side is also present, rear sits beyond side in the unfolding direction: left for first-angle, right for third-angle.

Y-axis hole and hole-pattern callouts, locations and pitch dimensions can use rear, as can overall width and height. For example:

s = Sheet(part, projection="first").authored_dimensions()
hole = s.hole(diameter=6, at=(10, 20, 5), axis="y").through("THRU")
s.dimension(hole, "bore.diameter")
s.dimension(hole, "location")
s.view("rear")
drawing = s.build()

Dimensions still use the shared placement solve. An unsupported measurement/view combination is refused; a required depth extent, for example, needs side. Rear is never added automatically to improve visibility, coverage or hidden lines. Generated scripts preserve an explicit rear request.

view(...), section_view(...) and detail_view(...) define complete authored sets; omission suppresses a view. add_view(...), add_section_view(...) and add_detail_view(...) augment an explicitly selected auto_views() source. row(...) and column(...) are shorthand for whole-view relations. A principal-view handle's pin((x, y)) anchors its projection origin in page millimetres; it never positions an annotation.

A detail around a turned step with an approved step.length uses a profile view and redraws that length through the shared dimension pass. For example, s.detail_view("A", around=shoulder).scale(3) retains its measurement identity and tolerance at three times the sheet scale. Omitted measurements stay omitted. An authored recovery detail that cannot place its dimension raises an error.

Principal orthographic views share the sheet scale and reject .scale(...). Detail and isometric handles may carry an independent positive factor. Infeasible relations, scales and pins raise with their declaration source; unsupported pins on derived or pictorial views are refused at declaration and constraints are never silently relaxed. The immutable authored snapshot is available as sheet.view_constraints, while drawing.view_plan is the distinct resolved result.

Layout advisories are also available as structured findings from Drawing.lint() and lint(physical=False). legibility_floor_breached reports an explicit scale below the legibility floor when a legible automatic fit exists; page_fit_uncertain reports an unsuccessful layout fit; layout_repack_stalled reports unresolved measured-repack triggers; and scale_fallback_applied reports a computed scale or a completeness-driven scale fallback. These warnings contribute to the legibility component of lint_summary(). A successful measured fit replaces the earlier estimated fit warning.

Taking over a detected baseline

Sheet.from_part(part) retains the detector's feature set and starts with implicit automatic dimensions. Use take_over(...) to adopt that same Sheet as an explicit editable request without copying features into a second Sheet:

s = Sheet.from_part(part)
hole = s.of(next(feature for feature in s.features if feature.kind == "hole"))
s.take_over(
    dimensions="authored",
    principal_views="automatic",
    derived_views="authored",
)
s.dimension(hole, "bore.diameter")
s.section_view("A", through=hole)

The three sources are chosen atomically. In this example the principal planner keeps the automatic front/plan/side/isometric baseline, while the authored derived set replaces inferred sections. Leaving that authored set empty suppresses inferred derived views; selecting derived_views="automatic" accepts them. Matching declarations may appear before or after take_over(...) with the same result. Explicit source contradictions fail without partially changing the Sheet, and automatic dimensions remain incompatible with authored views under ADR 0018.

To emit editable Python from an adopted Sheet, pass both request halves to the script emitter; the model carries the feature/dimension semantics and view_constraints carries the independent view sources and semantic targets:

from draftwright.sheet_emit import emit_sheet_script

source = emit_sheet_script(
    s.model(),
    "part = make_part()",
    "drawing",
    title="BRACKET",
    number="DWG-001",
    view_constraints=s.view_constraints,
)

Refining a staged hole into manufacturing authority

A detected hole remains a live feature handle after takeover. Enrich that handle instead of declaring a second, unrelated annotation. Counterbore/spotface tools can supply their geometry; thread and fit are authored manufacturing intent:

s = Sheet.from_part(part, page="A2", scale=1)
stack = s.of(next(feature for feature in s.features if feature.kind == "hole"))
s.take_over(
    dimensions="authored",
    principal_views="automatic",
    derived_views="authored",
)

stack.cbore(collar_tool)                 # diameter and depth re-read from the tool
stack.thread("M6x1", depth=12).fit("H8")
for parameter_id in stack.dimension_ids():
    intent = s.dimension(stack, parameter_id)
    if parameter_id == "counterbore.diameter":
        intent.format(decimals=2)        # 6.35 remains 6.35, still feature-linked

s.section_view("A", through=stack)       # replaces inferred derived views
drawing = s.build()
assert not [issue for issue in drawing.lint() if issue.severity != "info"]
drawing.export("quote/grm01", formats=("pdf", "svg", "dxf"))

An explicit tap depth is a real thread.depth parameter: it appears in dimension_ids(), participates in authored-set suppression and placement, and round-trips through generated Sheet code. fit("H8") applies only to bore.diameter; it does not leak onto a counterbore diameter. A plain thread("M6x1") remains valid when no independent depth is specified.

This surface models the common bore + recess + tap-depth stack in one solver-participating callout. More general ordered operation stacks remain tracked by issue #1360. Until a physical requirement has a structured parameter, use a feature-linked note(..., satisfies=(...)) only for parameter ids the handle actually exposes; free prose does not satisfy coverage.

Generated scripts and editing agents use structured_note(...) when the note declaration itself needs a stable selector:

note = sheet.structured_note(
    "BORE DIAMETER VERIFIED",
    stack,
    satisfies=("bore.diameter",),
).identify("declaration:note", provenance="structured-note")
same_note = sheet.by_declaration("declaration:note")

It declares the same solver-placed manufacturing note as note(...), but returns the note's own feature handle rather than returning the Sheet for chaining. The handle controls intent, not page coordinates. Final reports can therefore link the declaration to its exact placed ink while separately crediting the physical feature and parameter named by satisfies.

Straight and circular Blend chains

When recognition accepts a complete schema-v3 straight or circular rolling-ball Blend path that is not superseded by a dimension-worthy Fillet, it becomes one radius requirement. Declare the same meaning explicitly with the dedicated word:

blend = sheet.blend(
    axis="z",                         # canonical dominant component (x/y/z tie-break)
    axis_direction=(0.321394, 0.383022, 0.866025),
    radius=0.2,
    at=(12.345, -4.5, 6.789),         # straight anchor / circular centre in part coordinates
    side="convex",
)
sheet.dimension(blend, "blend.radius")

For a straight path, at is a point on its analytic line and axis_direction is the canonical line direction. A circular path uses path_kind="circular", treats at as its centre and axis_direction as its normal, and requires path_radius= for the centre-line circle's major radius. radius remains the rolling-ball radius; side may be "convex" or "concave". axis must match the same first-maximum x/y/z tie-break used by automatic conversion, so an explicit declaration has the same exact occurrence identity and view routing as its released record. Generated Sheet code preserves every field.

Straight-path radius leaders target curved boundaries of the physical cylindrical patch. Their at value locates the analytic axis, not the arrow tip. If a declared support is missing or ambiguous, the engine reports blend_dropped. Physical lint reports radius_leader_target_mismatch when a placed tip misses its trimmed boundary, or radius_leader_target_unverifiable when that boundary cannot be established; these findings lower fidelity. Circular-path toroidal blends retain their separate tangency selection.

These are part-space feature coordinates, never page positions. The annotation placement solve owns the final leader and label coordinates. The word is explicit-only because detached surface geometry cannot prove a complete chain or the provider aggregate's Fillet precedence.

View handle

_View

A fluent handle for one semantic whole-view constraint.

Its layout verbs relate or pin the complete view block. They never address a feature annotation, so ADR 2 (was 0014) remains the sole owner of dimension/callout/GD&T coordinates.

left_of(other, *, gap=None)

Keep this whole view block left of other.

right_of(other, *, gap=None)

Keep this whole view block right of other.

above(other, *, gap=None)

Keep this whole view block above other.

below(other, *, gap=None)

Keep this whole view block below other.

align_x(other)

Align this view's projection origin horizontally with other.

align_y(other)

Align this view's projection origin vertically with other.

pin(at)

Pin a principal view's projection origin at (x, y) page millimetres.

scale(factor)

Set an independent detail/orientation scale; principal views reject it.

Dimension intent handle

dimension(feature, parameter_id) and add_dimension(feature, parameter_id) return a referential DimensionIntent. The handle never carries a replacement nominal and never chooses page coordinates. Use format(decimals=n) to preserve between 0 and 15 decimal places in the printed nominal while reconciliation, tolerance, suppression and provenance continue to read the numeric parameter from the feature. Optional view="front|plan|side|rear" and side="above|below|left|right" arguments select a supported semantic corridor when authored routing must override the derived default; the normal placement solve still chooses coordinates and reports capacity/crossing failures. Trailing zeroes are intentional manufacturing display text:

sheet.authored_dimensions()
sheet.dimension(envelope, "width.length").format(decimals=2)  # 13.5 prints as 13.50
sheet.dimension(tapped_hole, "bore.diameter", view="plan", side="left")
sheet.dimension(tapped_hole, "location").format(decimals=2)  # all selected directions

A location intent's precision applies to all its selected directional values, including side-drilled holes and slots. The policy survives generated-script replay. When several location intents share one coincident mark, give them matching display precision; incompatible printed values raise an actionable error instead of silently choosing one intent's label.

DimensionIntent

The handle :meth:Sheet.add_dimension returns (ADR 4 (was 0016)).

It exposes no coordinate. Optional view= / side= are declared on the verb as semantic corridor selection; :meth:place can add a bounded relative lane. The engine still solves and validates the candidate's physical position — a dimension line references; the engine places.

ADR 2 (was 0012)'s .pin() / .priority() are deliberately absent for now. The engine already has two spellings of "keep this put" at different layers, and adding a third that no renderer consumes would ship a chainable verb doing nothing. This handle is the extension point for them once that concept is converged.

Every other attribute forwards to the owning :class:Sheet, so the declare-then-chain contract holds (sheet.add_dimension(bore, "depth").hole(...)) despite this returning a handle rather than the sheet — the same rule :class:_Params follows.

format(*, decimals)

Preserve decimals places in this dimension's printed nominal (#1349).

This is display policy on a referential intent, not a restated label: reconciliation, tolerance, suppression and provenance continue to use the feature parameter's numeric value and semantic identity. Automatic dimensions keep their existing formatting unless their explicit add_dimension intent opts in. For a location intent, the policy applies to every selected directional value.

place(*, lane)

Prefer a one-based drafting-spaced lane from this dimension's witness.

The value is a relative rank, not a page-space offset. Physical placement and feasibility remain owned by the shared measured-candidate solve, which may admit a proven-clear interior or exterior candidate. Generated scripts normally record the same policy through :meth:Sheet.layout_override, which also retains declaration evidence.

Hole handle

_Hole

Bases: _Nameable

A fluent handle for one declared hole — through vs blind (which changes the callout), and the P2a ± tolerance on its bore ⌀.

through(indicator=None)

Declare through, optionally choosing its printed indicator ('' omits it).

depth(d)

A blind hole d mm deep — adds a depth callout.

tolerance(lo, hi=None, *, source=None, source_ids=(), limit_bounds=None)

A ± tolerance on the bore ⌀: symmetric .tolerance(0.05) (→ ±0.05) or a limit pair .tolerance(0.0, 0.1) (→ +0.1 -0.0). Generated import scripts use source / source_ids to retain external requirement provenance; generated limit-dimension imports also carry their absolute bounds in limit_bounds.

fit(code, *, show='class')

An ISO 286 fit class on the bore ⌀ — .fit("H7") renders ø8 H7 (the class, default) or, with show="deviation", the signed deviations ø8 +0.015/0 resolved for the bore's nominal ⌀. Raises for a class/size outside the built-in table (#29).

requirement(value, *, source, source_ids)

Claim this existing bore diameter for external semantic source identities.

This changes no label: the canonical hole callout already prints the nominal value. Generated AP242 scripts use it to retain ownership without adding a duplicate dim.

cbore(obj=None, *, diameter=None, depth=None)

A counterbore on this hole. .cbore(cbore_cyl) reads its ⌀ + depth off the counterbore tool object (⌀ from the cylindrical face, depth from the part + tool along the hole axis — no numbers restated), or pass explicit .cbore(diameter=…, depth=…). An object supplies defaults; explicit kwargs override (#462).

spotface(obj=None, *, diameter=None, depth=None)

A spotface on this hole — same as :meth:cbore but a shallow facing (#462).

countersink(obj=None, *, major=None, angle=None)

A countersink on this hole (a flat-head screw seat, #575). .countersink(cone_tool) reads the major ⌀ + included angle off the build123d Cone you subtracted, or explicit .countersink(major=14, angle=90). Renders ⌵ Ø.. × ..° on the callout. An object supplies defaults; explicit kwargs override (#451).

thread(spec, *, depth=None)

A thread/tap spec folded onto this hole's callout (#764). .thread("M3x0.5") renders the tap/thread on the bore leader (e.g. ø2.5 THRU M3x0.5) — a structured aspect that round-trips, so .thread(...).finish(...) gives Ra-on-thread. A declaration-only aspect (threads are cosmetic, not modelled geometry — no recogniser). depth= retains an explicit tap depth as the independently addressable thread.depth measurement (#1360).

finish(ra, *, view=None, side=None)

A surface-finish symbol (Ra) on this hole's bore (ADR 4 (was 0011) P2c). .finish("1.6") — the roughness text; view/side override the derived strip.

note(text, *, satisfies=(), view=None, side=None)

A manufacturing note on a leader to this hole (#488).

satisfies explicitly names canonical parameter ids from :meth:dimension_ids that the placed note meets instead of a drawn dimension. Plain prose has no coverage effect; view/side override the derived strip (#1351).

Diameter and step handle

_Dim

Bases: _Nameable

A fluent handle for a declared dimension-bearing feature (a diameter / boss OD, or a turned step), carrying the P2a .tolerance aspect. default_kind is the parameter a bare .tolerance(...) targets — "diameter" for an OD, "length" for a step.

tolerance(lo, hi=None, *, on=None, source=None, source_ids=(), limit_bounds=None)

A ± tolerance on this dimension: symmetric .tolerance(0.05) (→ ±0.05) or a limit pair .tolerance(0.0, 0.1) (→ +0.1 -0.0). on picks the parameter for a multi-dim feature — a step's "length" (default) vs its "diameter" (OD), or its canonical parameter id such as "step.length". source / source_ids retain provenance on generated imported requirements.

fit(code, *, show='class')

An ISO 286 fit class on this feature's ⌀ (always the diameter — a fit is diametral, so a step's fit is on its OD, not its length). .fit("h6") renders ø12 h6 (the class, default) or show="deviation" the signed deviations ø12 0/-0.011 resolved for the nominal ⌀. Raises for a class/size outside the built-in table (#29).

requirement(value, *, on=None, source, source_ids)

Claim a canonical nominal parameter for external source identities.

finish(ra, *, view=None, side=None)

A surface-finish symbol (Ra) on this feature's surface (ADR 4 (was 0011) P2c). diameter(journal).finish("0.8"); view/side override the derived strip.

note(text, *, satisfies=(), view=None, side=None)

A manufacturing note on a leader to this feature (#488).

diameter(knurl).note("KNURL 0.8 STRAIGHT"); satisfies explicitly names canonical parameter ids returned by dimension_ids() that the placed note meets instead of a drawn dimension. Plain prose has no coverage effect; view/side override the strip (#1351).

knurl(pitch, pattern='STRAIGHT', *, view=None, side=None)

A knurl callout on this diameter (#765) — diameter(shaft).knurl("0.8") → KNURL 0.8 STRAIGHT, or .knurl("0.8", "DIAMOND"). Named sugar over :meth:note (canonical formatting + discoverability): knurl is a text callout on a leader, not modelled geometry, so no IR/render — it flows through the same note path. view/ side override the derived strip.

thread(spec)

An EXTERNAL thread spec appended to this OD's ⌀ callout (#859) — the turned analog of :meth:_Hole.thread. step(shaft).thread("M3x0.5") renders ø3 M3x0.5; a structured aspect on the feature, so .thread(...).finish(...) gives Ra-on-thread. Declaration-only (threads are cosmetic, rarely modelled as geometry — no recogniser).

Multi-parameter handle

Circular-blind, paired-ramp and through steps use a multi-parameter handle because their requirements remain separately addressable:

Circular seats open at both run ends use sheet.circular_channel(...). The centreline gives ordered endpoints on the cylinder axis; section gives three physical arc points (start, angular midpoint, end) in ascending transverse world axes. The geometry supports minor, semicircular, and major arcs. The engine anchors its leader on the curved wall and locates the axis from the stock bounding-box minimum. In an authored set, request seat_diameter.diameter, seat_run.length, seat_sweep.angle, and location independently. A location intent covers X, Y, and Z; its format(decimals=...) applies to all three. Generated scripts retain the full geometry and these intents.

ramp = sheet.paired_ramp_step(
    axis="y",
    angle=51.34,
    length=25,
    at=(10, 7.5, 0),  # midpoint of the shared ridge
)
sheet.authored_dimensions()
sheet.dimension(ramp, "ramp_angle.angle")
sheet.dimension(ramp, "ramp_run.length")

circular = sheet.circular_blind_step(
    axis="x",
    radius=4,
    length=25,
    centreline=((-5, 15, 10), (20, 15, 10)),  # blind terminal → open envelope
    section=((11, 10), (15, 10), (15, 6)),   # arc endpoint, centre, endpoint
)
sheet.dimension(circular, "circular_step_radius.radius")
sheet.dimension(circular, "circular_step_depth.length")

step = sheet.through_step(
    axis="z",
    length=20,
    at=(12.5, 7.5, 0),
    section=((5, 15), (5, 0), (20, 0)),
)
sheet.dimension(step, "through_step_leg.length.x")
sheet.dimension(step, "through_step_leg.length.y")

blind = sheet.rectangular_blind_slot(
    axis="z",                 # capped penetration/run direction
    open_sign=-1,             # source-envelope mouth along that run
    length=20,
    width_axis="x",
    depth_axis="y",
    depth_sign=1,             # material-outward U-section opening
    width=10,
    depth=5,
    at=(0, 7.5, 10),
)
sheet.dimension(blind, "rectangular_blind_slot_width.length")
sheet.dimension(blind, "rectangular_blind_slot_length.length")
sheet.dimension(blind, "rectangular_blind_slot_depth.length")

round_bottom = sheet.round_bottom_blind_slot(
    axis="z",
    open_sign=1,
    length=20,
    width_axis="x",
    depth_axis="y",
    depth_sign=1,
    radius=3,
    flat_width=4,
    at=(0, -1.5, 10),
)
sheet.dimension(round_bottom, "round_bottom_blind_slot_length.length")
sheet.dimension(round_bottom, "round_bottom_blind_slot_flat_width.length")
sheet.dimension(round_bottom, "round_bottom_blind_slot_radius.radius")

These declarations are explicit-only. A detached face or cutter cannot prove the aggregate material-removal topology. Placement remains solver-owned; the engine selects the end-on view and positions the compound leader or linear section dimensions.

rectangular_blind_slot(...) is not an alias for slot(...) or pocket(...). Its dedicated feature retains the open source-envelope end, capped terminal wall and flat-bottomed U-section, and the solver places one OPEN SLOT width × length × depth DEEP leader carrying all three compiler-approved measurement identities. In authored-dimension mode, any non-empty subset is valid and is role-labelled (WIDE, LONG, DEEP) so every requested identity remains visible. round_bottom_blind_slot(...) is a separate word, not a flag on the rectangular declaration. Its independent dimensions are capped run length, straight bottom-flat width and equal side radius; the total opening width and profile depth are derived from the latter two and are not duplicated in the dimension plan. The solver places one ROUND-BOTTOM OPEN SLOT … leader. Authored subsets remain role-explicit (LONG, BOTTOM FLAT, R) and never reconstruct an omitted sibling value.

An explicit through-step may use any principal run axis, and automatic detection supports the same three axes. Where an X/Y-run record's two exact physical intervals are already proved by the face-level plus shoulder/plate grammar and the envelope, that established grammar remains the owner. If even one leg is not proved, the aggregate local-leg grammar owns the occurrence and its exact matching lower-level fragments are removed, so one physical requirement reaches the sheet once. Completeness follows the chosen legacy dimensions too: their authored omission, placement drop, or missing ink is not hidden by the ownership choice.

_Params

Bases: _Nameable

A fluent handle for a declared MULTI-parameter feature — a pocket (width/length/depth), slot (width/length) or envelope (width/height/depth) — whose parameters share a KIND but have distinct ROLES. .tolerance(..., on=role) tolerances ONE parameter by role (#746: e.g. a pocket's depth without touching its width/length); a bare .tolerance(...) (no on) folds onto every parameter of the feature (the back-compat kind-keyed form). on accepts the full role ("pocket_depth") or its short tail ("depth").

Every other attribute forwards to the owning :class:Sheet, so these verbs stay chainable (sheet.pocket(...).hole(...).build()) despite returning a handle — the module's declare-then-chain contract holds (#807).

tolerance(lo, hi=None, *, on=None, source=None, source_ids=(), limit_bounds=None)

A ± tolerance: symmetric .tolerance(0.05) (→ ±0.05) or a limit pair .tolerance(0.0, 0.1) (→ +0.1 -0.0). on targets one parameter by its full id or discriminator, or a parameter family by its shared role (on="depth" on a pocket → a role-keyed decoration); omit on to tolerance every parameter of the feature alike (the kind-keyed form). source / source_ids retain provenance on generated imported requirements.

requirement(value, *, on, source, source_ids)

Claim the feature's canonical diameter for external semantic source identities.

note(text, ref=None, *, satisfies=(), view=None, side=None)

A free-text manufacturing note (#841). With no ref the note anchors to THIS feature — sheet.slot(...).note("5X OBROUND SLOT") — mirroring :meth:_Hole.note / :meth:_Dim.note (previously this raised, because the forwarded Sheet.note needs a target). An explicit ref (a face or feature) still forwards to :meth:Sheet.note, preserving the handle's forwarding contract for sheet.slot(...).note("DEBURR", face). Returns the handle (chainable). satisfies explicitly names canonical parameter ids from dimension_ids() that the placed note meets instead of a drawn dimension; plain prose has no coverage effect. view/side override the derived strip (#1351).

Axis-aligned through-slots

Sheet.slot(...) declares the independently addressable slot_width.length and slot_length.length measurements. Pass end_radius= only for a stadium/obround slot; this adds slot_end_radius.radius, rendered as a solver-placed 2× R… leader whose arrow remains normal to either end arc. Leaving it unset preserves the rectangular-slot grammar. The declared radius must equal half the width, and generated Sheet scripts retain the field when recognition proved semicircular ends.

GD&T control builder

_Control

A fluent GD&T feature-control-frame builder (ADR 4 (was 0011) P2c.2). One method per ISO 1101 characteristic — each appends a control frame on the same target, so chained calls stack::

sheet.control(bore).position(0.1, to="A B").perpendicularity(0.05, to="A")

to= names the referenced datum letter(s) ("A" / "A B" / ("A", "B")); diameter= prefixes the zone with ⌀ (the default for position/concentricity); modifier= a material-condition symbol ("M"/"L"/"P"). The target view + strip are derived once (from the feature/face) when :meth:Sheet.control runs; view=/side= there override them.

straightness(tol, *, modifier=None)

Apply straightness tolerance tol; no datum reference is permitted.

flatness(tol, *, modifier=None)

Apply flatness tolerance tol; no datum reference is permitted.

circularity(tol, *, modifier=None)

Apply circularity tolerance tol; no datum reference is permitted.

cylindricity(tol, *, modifier=None)

Apply cylindricity tolerance tol; no datum reference is permitted.

profile_line(tol, *, to=None, modifier=None)

Apply line-profile tolerance tol, optionally relative to datum(s) to.

profile_surface(tol, *, to=None, modifier=None)

Apply surface-profile tolerance tol, optionally relative to datum(s) to.

angularity(tol, *, to=None, modifier=None)

Apply angularity tolerance tol relative to datum(s) to.

perpendicularity(tol, *, to=None, modifier=None)

Apply perpendicularity tolerance tol relative to datum(s) to.

parallelism(tol, *, to=None, modifier=None)

Apply parallelism tolerance tol relative to datum(s) to.

position(tol, *, to=None, diameter=True, modifier=None)

Apply position tolerance tol relative to to; diameter selects the zone.

concentricity(tol, *, to=None, diameter=True, modifier=None)

Apply concentricity tolerance tol relative to to; diameter selects the zone.

symmetry(tol, *, to=None, modifier=None)

Apply symmetry tolerance tol relative to datum(s) to.

circular_runout(tol, *, to=None, modifier=None)

Apply circular-runout tolerance tol relative to datum(s) to.

total_runout(tol, *, to=None, modifier=None)

Apply total-runout tolerance tol relative to datum(s) to.

Imported AP242 nominal-diameter ownership requires absolute agreement within 1e-6 with the canonical feature diameter, using the same rule as planning. A larger mismatch keeps the original source value and identity in a blocked dimension; it does not replace the canonical value or crash planning. Generated Sheet scripts retain the blocker, and authored_dim_source_unresolved reports the withheld source dimension through lint.

Oriented through-slots

Sheet.oriented_slot(...) declares a rectangular through-slot with explicit width, long and run directions and its retained passage geometry. Its two independently addressable measurements are oriented_slot_width.length and oriented_slot_length.length; generated Sheet scripts preserve those directions and the passage. The current drawing grammar requires both run ends to be open and perpendicular to the run. Automatic conversion rejects a source with a nonzero end-plane gradient, because the IR cannot retain that slope and a flat opening would put the leader on the wrong physical rim.