draftwright 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {draftwright-0.2.0 → draftwright-0.2.2}/CHANGELOG.md +65 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/PKG-INFO +1 -1
- {draftwright-0.2.0 → draftwright-0.2.2}/pyproject.toml +1 -1
- draftwright-0.2.2/src/draftwright/__init__.py +103 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/_core.py +43 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/annotations/from_model.py +145 -69
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/annotations/holes.py +93 -19
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/annotations/orchestrator.py +48 -9
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/annotations/sections.py +182 -138
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/builder.py +30 -1
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/cli.py +6 -2
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/linting/coverage.py +69 -36
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_make_drawing.py +408 -2
- draftwright-0.2.0/src/draftwright/__init__.py +0 -62
- {draftwright-0.2.0 → draftwright-0.2.2}/.gitignore +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/LICENSE +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/README.md +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/skills/SKILL.md +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/analysis.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/annotate.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/annotations/_common.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/drawing.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/export.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/features.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/model/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/model/detect.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/model/ir.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/repair.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/src/draftwright/sheet.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_layout.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_linting.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_part_model.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_pmi.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_recognition.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_registry.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_render_seam.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.2}/tests/test_turned_steps.py +0 -0
|
@@ -2,6 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.2.2 — 2026-06-30
|
|
6
|
+
|
|
7
|
+
A patch release of turned-part dimension-placement fixes and a CLI start-up
|
|
8
|
+
speed-up. Drawing output changes for the affected turned/cross-drilled parts.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A coaxial bore callout on a *stepped* turned shaft is now lifted off the round
|
|
13
|
+
view's centre axis** (#305). The earlier fix only triggered for a uniform
|
|
14
|
+
(`is_rotational`) cylinder; a stepped shaft (e.g. the gramel GRM-03 drive screw)
|
|
15
|
+
has a turned step profile but isn't classified rotational, so its `⌀… ↓…` bore
|
|
16
|
+
callout was still leadered straight along the centreline, with the centre mark
|
|
17
|
+
running through the text. The lift now also fires for a turned-profile part.
|
|
18
|
+
- **A side-drilled hole's location dimension now stacks *inside* the overall
|
|
19
|
+
envelope dimension** (ISO order — overall dim outermost, feature/location dims
|
|
20
|
+
nearer the view). It was placed *outside* the envelope, which forced the shorter
|
|
21
|
+
location dim's arrowheads to flip outward and clash (seen on GRM-01 and GRM-02).
|
|
22
|
+
The mandatory overall dimension is still guaranteed placement.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **CLI shell completion and `--help` are fast again** (#313). The Typer CLI and
|
|
27
|
+
the heavy CAD engine are now imported lazily, so tab-completion and `--help` no
|
|
28
|
+
longer pay a ~6 s engine-import cost; a real drawing run is unaffected.
|
|
29
|
+
|
|
30
|
+
## v0.2.1 — 2026-06-30
|
|
31
|
+
|
|
32
|
+
A patch release focused on **turned-part dimensioning legibility**: crowded
|
|
33
|
+
step-length chains and fine turned heads are now drawn legibly instead of crammed.
|
|
34
|
+
Drawing output changes for affected turned parts.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **Automatic enlarged detail view for a crowded turned head** (#304). A turned
|
|
39
|
+
part with a fine cluster of steps near one end and a long shaft (e.g. a thumbwheel
|
|
40
|
+
drive screw) cannot have its head dimensioned legibly in line at any sensible
|
|
41
|
+
scale. The head is now located as one block on the main view and broken out into
|
|
42
|
+
an enlarged **DETAIL A — SCALE n:1** — the textbook treatment — firing
|
|
43
|
+
automatically when a head's shoulders fall below the page legibility floor.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- **Crowded turned step-length chains stagger across two tiers** (#293) instead of
|
|
48
|
+
cramming or being skipped. When the labels would collide on one line, the ISO
|
|
49
|
+
129-1 staggered convention alternates them between a near and a far tier so every
|
|
50
|
+
step length stays legible at the drawing's own scale — no rescale needed. A roomy
|
|
51
|
+
chain stays on a single tier.
|
|
52
|
+
- **Detail views are now one unified pipeline** (#307). The prismatic step-height
|
|
53
|
+
detail and the new turned-head detail flow through a single
|
|
54
|
+
detect → request → render path; several crowded regions become DETAIL A/B/…
|
|
55
|
+
- **Output changes** for turned parts whose step chains were previously crammed, or
|
|
56
|
+
whose fine heads are now broken out into a detail view.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **A coaxial bore callout no longer overlaps the round view's centreline** (#305):
|
|
61
|
+
its leader is angled off the centre axis so the callout text sits in clear space.
|
|
62
|
+
|
|
63
|
+
### Internal
|
|
64
|
+
|
|
65
|
+
- A new **layout-cleanliness invariant test** asserts that finished drawings have no
|
|
66
|
+
view/annotation collisions across representative part archetypes, and the
|
|
67
|
+
measure-and-repack pass gained a trigger for an annotation growing into a
|
|
68
|
+
neighbouring view's line-work (so the views spread to make room).
|
|
69
|
+
|
|
5
70
|
## v0.2.0 — 2026-06-30
|
|
6
71
|
|
|
7
72
|
A major release. draftwright took ownership of feature recognition and linting
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""draftwright — automated technical-drawing generation for build123d.
|
|
2
|
+
|
|
3
|
+
Takes a build123d solid and produces a fully-annotated multi-view technical
|
|
4
|
+
drawing (orthographic views, dimensions, section A–A, title block) ready for
|
|
5
|
+
DXF/SVG export::
|
|
6
|
+
|
|
7
|
+
from draftwright import make_drawing
|
|
8
|
+
make_drawing(my_part, out="drawing")
|
|
9
|
+
|
|
10
|
+
Requires build123d-drafting-helpers for annotation primitives.
|
|
11
|
+
Licensed under the GNU Affero General Public License v3 (AGPL-3.0).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import importlib as _importlib
|
|
15
|
+
import sys as _sys
|
|
16
|
+
import types as _types
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
# Public API resolved lazily (PEP 562): `import draftwright` — and, crucially,
|
|
20
|
+
# `import draftwright.cli` (which runs this __init__ first) — must NOT eagerly
|
|
21
|
+
# pull in the engine. The engine drags build123d/OCP, ~5 s of CAD-kernel import.
|
|
22
|
+
# The CLI's shell completion, --help and --version import this package but touch
|
|
23
|
+
# none of these names, so they stay sub-second instead of paying for the kernel
|
|
24
|
+
# on every TAB press (#313). Each name maps to the submodule that provides it.
|
|
25
|
+
_LAZY = {
|
|
26
|
+
"analyse_face_levels": "draftwright.analysis",
|
|
27
|
+
"dedup_diams": "draftwright.analysis",
|
|
28
|
+
"build_drawing": "draftwright.builder",
|
|
29
|
+
"generate_script": "draftwright.builder",
|
|
30
|
+
"make_drawing": "draftwright.builder",
|
|
31
|
+
"Drawing": "draftwright.drawing",
|
|
32
|
+
"FeatureInfo": "draftwright.drawing",
|
|
33
|
+
"fix_svg_page_size": "draftwright.export",
|
|
34
|
+
"lint_feature_coverage": "draftwright.linting",
|
|
35
|
+
"PmiRecord": "draftwright.pmi",
|
|
36
|
+
"extract_pmi": "draftwright.pmi",
|
|
37
|
+
"choose_scale": "draftwright.sheet",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _resolve(name):
|
|
42
|
+
"""Import the providing submodule and cache the public object on the package."""
|
|
43
|
+
value = getattr(_importlib.import_module(_LAZY[name]), name)
|
|
44
|
+
_types.ModuleType.__setattr__(_sys.modules[__name__], name, value)
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _DraftwrightModule(_types.ModuleType):
|
|
49
|
+
def __getattr__(self, name):
|
|
50
|
+
# PEP 562 miss handler: resolve a public name on first access.
|
|
51
|
+
if name in _LAZY:
|
|
52
|
+
return _resolve(name)
|
|
53
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
54
|
+
|
|
55
|
+
def __getattribute__(self, name):
|
|
56
|
+
# `draftwright.make_drawing` must be the FUNCTION even after the compat
|
|
57
|
+
# SUBMODULE of the same name is imported and shadows it as a package
|
|
58
|
+
# attribute (importing draftwright.make_drawing binds the module here).
|
|
59
|
+
if name == "make_drawing":
|
|
60
|
+
namespace = _types.ModuleType.__getattribute__(self, "__dict__")
|
|
61
|
+
value = namespace.get("make_drawing")
|
|
62
|
+
if (
|
|
63
|
+
isinstance(value, _types.ModuleType)
|
|
64
|
+
and value.__name__ == "draftwright.make_drawing"
|
|
65
|
+
):
|
|
66
|
+
return _resolve("make_drawing")
|
|
67
|
+
return _types.ModuleType.__getattribute__(self, name)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_sys.modules[__name__].__class__ = _DraftwrightModule
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if TYPE_CHECKING: # static analysers / IDEs — no runtime import, no kernel cost
|
|
74
|
+
from draftwright.analysis import analyse_face_levels, dedup_diams
|
|
75
|
+
from draftwright.builder import build_drawing, generate_script, make_drawing
|
|
76
|
+
from draftwright.drawing import Drawing, FeatureInfo
|
|
77
|
+
from draftwright.export import fix_svg_page_size
|
|
78
|
+
from draftwright.linting import lint_feature_coverage
|
|
79
|
+
from draftwright.pmi import PmiRecord, extract_pmi
|
|
80
|
+
from draftwright.sheet import choose_scale
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def __dir__():
|
|
84
|
+
# Surface the lazy public names (not in __dict__ until first accessed)
|
|
85
|
+
# *alongside* the normal module contents — dunders, imported submodules — so
|
|
86
|
+
# introspection / REPL completion sees the full surface, not a subset.
|
|
87
|
+
return sorted(set(globals()) | set(__all__))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
"Drawing",
|
|
92
|
+
"FeatureInfo",
|
|
93
|
+
"PmiRecord",
|
|
94
|
+
"analyse_face_levels",
|
|
95
|
+
"build_drawing",
|
|
96
|
+
"choose_scale",
|
|
97
|
+
"dedup_diams",
|
|
98
|
+
"extract_pmi",
|
|
99
|
+
"fix_svg_page_size",
|
|
100
|
+
"generate_script",
|
|
101
|
+
"lint_feature_coverage",
|
|
102
|
+
"make_drawing",
|
|
103
|
+
]
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
import functools
|
|
14
14
|
import logging
|
|
15
15
|
import re
|
|
16
|
+
from collections.abc import Callable
|
|
16
17
|
from dataclasses import dataclass, field
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from types import SimpleNamespace
|
|
@@ -382,6 +383,48 @@ def _largest_empty_rect(drawable, obstacles):
|
|
|
382
383
|
return best
|
|
383
384
|
|
|
384
385
|
|
|
386
|
+
@dataclass
|
|
387
|
+
class DetailRequest:
|
|
388
|
+
"""A renderer's request for an enlarged detail of a region it could not draw
|
|
389
|
+
legibly at sheet scale (#307). Renderers append these to ``dwg._detail_requests``
|
|
390
|
+
instead of building bespoke detail views; ``_resolve_details`` resolves them all
|
|
391
|
+
through one generic detailer (crop → project → place → caption → marker), then
|
|
392
|
+
calls ``redraw`` to draw the feature's own dims inside the placed detail view.
|
|
393
|
+
|
|
394
|
+
The single ``detect → request → generic render`` path that folds the prismatic
|
|
395
|
+
step detail (#42) and the turned-head detail (#304) into one, mirroring the
|
|
396
|
+
section pipeline (``plan_sections``/``SectionPlan``).
|
|
397
|
+
|
|
398
|
+
Fields:
|
|
399
|
+
axis: part axis the band spans / is cropped along ("x"/"y"/"z").
|
|
400
|
+
lo, hi: band bounds along ``axis`` (world mm).
|
|
401
|
+
scale_needed: detail world→page scale that makes the region legible.
|
|
402
|
+
redraw: ``redraw(dwg, view_name, detail_scale) -> int`` — draws the
|
|
403
|
+
detail's dimensions in the placed detail view's coordinate system
|
|
404
|
+
and returns the count placed (0 → the detailer rolls the view
|
|
405
|
+
back rather than leave an empty box). Called once the detail is
|
|
406
|
+
placed; the main view always carries the located head/block
|
|
407
|
+
inline regardless, so a placement failure loses no coverage (lint
|
|
408
|
+
reports the un-located interior instead).
|
|
409
|
+
pad_top: page-mm band reserved above the detail view (a horizontal
|
|
410
|
+
chain); reserved in the fit + placement.
|
|
411
|
+
pads: optional ``pads(detail_scale) -> (pad_right, pad_top)`` for a
|
|
412
|
+
footprint that depends on the chosen scale (the prismatic
|
|
413
|
+
ladder reserves one rung per *legible-at-that-scale* step, so it
|
|
414
|
+
shrinks with the scale during the fit). Overrides ``pad_top``.
|
|
415
|
+
kind: short label for logging.
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
axis: str
|
|
419
|
+
lo: float
|
|
420
|
+
hi: float
|
|
421
|
+
scale_needed: float
|
|
422
|
+
redraw: Callable[..., int]
|
|
423
|
+
pad_top: float = 0.0
|
|
424
|
+
pads: Callable[[float], tuple[float, float]] | None = None
|
|
425
|
+
kind: str = "detail"
|
|
426
|
+
|
|
427
|
+
|
|
385
428
|
@dataclass(frozen=True)
|
|
386
429
|
class _Projector:
|
|
387
430
|
"""Model → page coordinate projection for the orthographic views.
|
|
@@ -27,10 +27,12 @@ from draftwright._core import (
|
|
|
27
27
|
_DIAM_RE,
|
|
28
28
|
_END_ON,
|
|
29
29
|
_MARGIN,
|
|
30
|
+
_MIN_STEP_SEP_MM,
|
|
30
31
|
_SLOT_DIM_DEPTH,
|
|
31
32
|
_SLOT_DIM_HEIGHT,
|
|
32
33
|
_SLOT_DIM_STEP,
|
|
33
34
|
_SLOT_DIM_WIDTH,
|
|
35
|
+
DetailRequest,
|
|
34
36
|
_dim,
|
|
35
37
|
_fmt,
|
|
36
38
|
_greedy_strip_ys,
|
|
@@ -530,6 +532,20 @@ def _env_pd(group, role):
|
|
|
530
532
|
return next((pd for pd in group.dims if pd.param.role == role), None)
|
|
531
533
|
|
|
532
534
|
|
|
535
|
+
def envelope_group(groups):
|
|
536
|
+
"""The envelope `DimensionGroup` in *groups*, or None."""
|
|
537
|
+
return next((g for g in groups if g.feature_kind == "envelope"), None)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def env_dim_placed(pd) -> bool:
|
|
541
|
+
"""Whether :func:`render_envelope` will actually place an envelope dim for the
|
|
542
|
+
PlannedDimension *pd* — present, not suppressed by the planner (square footprint /
|
|
543
|
+
X-turned, #250), and carrying a span. The single source of truth for that
|
|
544
|
+
decision, shared with the orchestrator's side-below tier reservation so the two
|
|
545
|
+
can never drift (#316 review)."""
|
|
546
|
+
return pd is not None and not pd.suppressed and pd.param.span is not None
|
|
547
|
+
|
|
548
|
+
|
|
533
549
|
def render_envelope(dwg, groups, a) -> int:
|
|
534
550
|
"""Overall width (plan, below) + depth (side, below) envelope dims via the IR,
|
|
535
551
|
placed through the engine's below-strip zone allocators (the zone-aware render
|
|
@@ -537,12 +553,12 @@ def render_envelope(dwg, groups, a) -> int:
|
|
|
537
553
|
those strips). The **planner** decides suppression (square footprint / X-turned;
|
|
538
554
|
#250); this renderer just skips suppressed dims and places the rest. Returns the
|
|
539
555
|
count placed."""
|
|
540
|
-
env =
|
|
556
|
+
env = envelope_group(groups)
|
|
541
557
|
if env is None:
|
|
542
558
|
return 0
|
|
543
559
|
n = 0
|
|
544
560
|
width = _env_pd(env, "width")
|
|
545
|
-
if width
|
|
561
|
+
if env_dim_placed(width):
|
|
546
562
|
(x0, y0, z0), (x1, _, _) = width.param.span
|
|
547
563
|
p1, p2 = dwg.at("plan", x0, y0, z0), dwg.at("plan", x1, y0, z0)
|
|
548
564
|
witness = p1[1] - 2
|
|
@@ -562,7 +578,7 @@ def render_envelope(dwg, groups, a) -> int:
|
|
|
562
578
|
)
|
|
563
579
|
n += 1
|
|
564
580
|
depth = _env_pd(env, "depth")
|
|
565
|
-
if depth
|
|
581
|
+
if env_dim_placed(depth):
|
|
566
582
|
(x0, y0, z0), (_, y1, _) = depth.param.span
|
|
567
583
|
p1, p2 = dwg.at("side", x0, y0, z0), dwg.at("side", x0, y1, z0)
|
|
568
584
|
witness = p1[1] - 2
|
|
@@ -584,52 +600,30 @@ def render_envelope(dwg, groups, a) -> int:
|
|
|
584
600
|
return n
|
|
585
601
|
|
|
586
602
|
|
|
587
|
-
def
|
|
588
|
-
"""
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
for g in groups:
|
|
600
|
-
if g.feature_kind != "step":
|
|
601
|
-
continue
|
|
602
|
-
length = next(
|
|
603
|
-
(pd.param for pd in g.dims if pd.param.kind == "length" and pd.param.span is not None),
|
|
604
|
-
None,
|
|
605
|
-
)
|
|
606
|
-
if length is None or length.span is None:
|
|
607
|
-
continue
|
|
608
|
-
a, b = length.span
|
|
609
|
-
pa, pb = dwg.at("front", *a), dwg.at("front", *b)
|
|
610
|
-
segs.append((pa, pb, length.value))
|
|
603
|
+
def _draw_step_chain(dwg, view, segs, name_prefix, detail_scale=None, allow_collapse=True) -> int:
|
|
604
|
+
"""Place a turned step-length chain in *view* from *segs* — each ``(pa, pb,
|
|
605
|
+
value)`` already projected to *view*'s page coords, in axis order. Orientation is
|
|
606
|
+
data (the projected span direction): horizontal → chain above the view, vertical
|
|
607
|
+
→ chain to the right. A uniform run collapses to one ``N× v`` dim (#230); else a
|
|
608
|
+
per-segment chain, staggered into a near/far tier only when crowded (ISO 129-1,
|
|
609
|
+
#293); skipped if even two tiers can't separate the labels, or if any dim would
|
|
610
|
+
fall off the page. ``detail_scale`` tags the dims for label-vs-measured lint when
|
|
611
|
+
drawing inside a scaled detail view. ``allow_collapse=False`` disables the ``N× v``
|
|
612
|
+
collapse — used when the chain mixes a synthetic head-*block* with real steps, where
|
|
613
|
+
a uniform-staircase representative would be a false claim of N equal steps (#307
|
|
614
|
+
review). Returns the count placed."""
|
|
611
615
|
if not segs:
|
|
612
616
|
return 0
|
|
613
|
-
vb = dwg.view_bounds(
|
|
617
|
+
vb = dwg.view_bounds(view)
|
|
614
618
|
if vb is None:
|
|
615
619
|
return 0
|
|
616
620
|
x0, y0, x1, y1 = vb
|
|
617
621
|
draft = dwg.draft
|
|
618
622
|
gap = draft.font_size + 4 * draft.pad_around_text
|
|
619
|
-
# Orientation is data: the projected span direction. Horizontal → X-turned
|
|
620
|
-
# (chain above the view); vertical → Z-turned (chain left of the view).
|
|
621
623
|
horizontal = abs(segs[0][1][0] - segs[0][0][0]) >= abs(segs[0][1][1] - segs[0][0][1])
|
|
622
|
-
|
|
623
|
-
# Spread crowded labels along a horizontal chain (ADR-0003 strip solve), then
|
|
624
|
-
# carry each label back to its segment via label_offset_x (the only along-line
|
|
625
|
-
# offset the Dimension primitive supports). A vertical chain places plain dims.
|
|
626
|
-
# Uniform staircase → one representative "N× length" dim spanning the whole run
|
|
627
|
-
# (#230), instead of N identical segment dims. Mirrors the prismatic ladder's
|
|
628
|
-
# _detect_step_repeat: ≥3 segments, all within 10% of the mean (so a 2-step part
|
|
629
|
-
# or a mixed chain still dimensions each segment).
|
|
630
624
|
vals = [v for *_, v in segs]
|
|
631
625
|
mean_v = sum(vals) / len(vals)
|
|
632
|
-
if len(segs) >= 3 and (max(vals) - min(vals)) <= 0.10 * mean_v:
|
|
626
|
+
if allow_collapse and len(segs) >= 3 and (max(vals) - min(vals)) <= 0.10 * mean_v:
|
|
633
627
|
label = f"{len(segs)}× {_fmt(mean_v)}"
|
|
634
628
|
xs = [p[0] for pa, pb, _ in segs for p in (pa, pb)]
|
|
635
629
|
ys = [p[1] for pa, pb, _ in segs for p in (pa, pb)]
|
|
@@ -637,48 +631,47 @@ def render_step_lengths(dwg, groups) -> int:
|
|
|
637
631
|
dim = _dim((min(xs), y1, 0), (max(xs), y1, 0), "above", gap, draft, label=label)
|
|
638
632
|
else:
|
|
639
633
|
dim = _dim((x1, min(ys), 0), (x1, max(ys), 0), "right", gap, draft, label=label)
|
|
640
|
-
candidates = [("
|
|
634
|
+
candidates = [(f"{name_prefix}_typ", dim)]
|
|
641
635
|
else:
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
# an unreadable wall of overlapping dims is worse than none — lint then reports
|
|
645
|
-
# axial_length_missing, and the user resolves it with a larger scale or a detail
|
|
646
|
-
# view. The room guard's sibling, for crowding rather than page overflow.
|
|
647
|
-
gap_min = draft.font_size + 2 * draft.pad_around_text
|
|
636
|
+
tier_step = draft.font_size + 2 * draft.pad_around_text
|
|
637
|
+
tiers = [0] * len(segs)
|
|
648
638
|
if horizontal:
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
639
|
+
cw = [
|
|
640
|
+
((pa[0] + pb[0]) / 2, len(_fmt(v)) * draft.font_size * 0.62) for pa, pb, v in segs
|
|
641
|
+
]
|
|
642
|
+
|
|
643
|
+
def _clear(items): # (center, width) pairs in x order — labels don't overlap
|
|
644
|
+
return all(
|
|
645
|
+
c2 - c1 >= (w1 + w2) / 2 + draft.pad_around_text
|
|
646
|
+
for (c1, w1), (c2, w2) in zip(items, items[1:])
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
if _clear(cw):
|
|
650
|
+
pass # one tier suffices — no needless zig-zag for a roomy chain
|
|
651
|
+
elif _clear(cw[0::2]) and _clear(cw[1::2]):
|
|
652
|
+
tiers = [i % 2 for i in range(len(segs))] # alternate to make room
|
|
653
|
+
else:
|
|
654
|
+
_log.info("step-length chain skipped: too dense even when staggered")
|
|
658
655
|
return 0
|
|
659
|
-
if solved:
|
|
660
|
-
offsets = [s - c for s, c in zip(solved, centers)]
|
|
661
656
|
else:
|
|
662
|
-
# Z-turned chain places plain dims at the shoulders (no along-line spread is
|
|
663
|
-
# available vertically), so its legibility is the raw shoulder spacing.
|
|
664
657
|
shoulder_ys = sorted({c for pa, pb, _ in segs for c in (pa[1], pb[1])})
|
|
665
|
-
if any(b - a <
|
|
658
|
+
if any(b - a < tier_step for a, b in zip(shoulder_ys, shoulder_ys[1:])):
|
|
666
659
|
_log.info("step-length chain skipped: shoulders too close to dimension")
|
|
667
660
|
return 0
|
|
668
661
|
|
|
669
662
|
candidates = []
|
|
670
663
|
for i, (pa, pb, value) in enumerate(segs):
|
|
671
|
-
if horizontal:
|
|
664
|
+
if horizontal:
|
|
672
665
|
p1, p2, side = (pa[0], y1, 0), (pb[0], y1, 0), "above"
|
|
673
|
-
|
|
674
|
-
else:
|
|
666
|
+
dist = gap + tiers[i] * tier_step
|
|
667
|
+
else:
|
|
675
668
|
p1, p2, side = (x1, pa[1], 0), (x1, pb[1], 0), "right"
|
|
676
|
-
|
|
677
|
-
candidates.append(
|
|
669
|
+
dist = gap
|
|
670
|
+
candidates.append(
|
|
671
|
+
(f"{name_prefix}{i}", _dim(p1, p2, side, dist, draft, label=_fmt(value)))
|
|
672
|
+
)
|
|
678
673
|
|
|
679
|
-
# Room guard
|
|
680
|
-
# page, place NONE and let lint report axial_length_missing — never run the
|
|
681
|
-
# chain off the page edge.
|
|
674
|
+
# Room guard: if any dim would fall off the drawable page, place NONE.
|
|
682
675
|
page = (_MARGIN, _MARGIN, dwg.page_w - _MARGIN, dwg.page_h - _MARGIN)
|
|
683
676
|
for _, dim in candidates:
|
|
684
677
|
box = _anno_box(dim)
|
|
@@ -687,10 +680,93 @@ def render_step_lengths(dwg, groups) -> int:
|
|
|
687
680
|
):
|
|
688
681
|
return 0
|
|
689
682
|
for name, dim in candidates:
|
|
690
|
-
|
|
683
|
+
if detail_scale is not None:
|
|
684
|
+
dim._dw_scale = detail_scale
|
|
685
|
+
dwg.add(dim, name, view=view)
|
|
691
686
|
return len(candidates)
|
|
692
687
|
|
|
693
688
|
|
|
689
|
+
def render_step_lengths(dwg, groups) -> int:
|
|
690
|
+
"""Unified turned step-length chain (ADR 0008 #223): each `StepFeature`'s length
|
|
691
|
+
span projects into the front view and joins the chain that tiles the turning axis
|
|
692
|
+
so every shoulder is located. X-turned → horizontal chain above the view;
|
|
693
|
+
Z-turned → vertical chain to the right.
|
|
694
|
+
|
|
695
|
+
A crowded **X-turned head** — a contiguous run of steps too short to dimension
|
|
696
|
+
legibly even staggered (shoulders below the page arrowhead floor) — is not crammed
|
|
697
|
+
in line: the main view locates that run as one *block* dim and an enlarged
|
|
698
|
+
`DetailRequest` (#304/#307) is queued to break it down. If the detail later can't
|
|
699
|
+
place, the block still locates the head extent and lint reports the un-located
|
|
700
|
+
interior shoulders — never worse than the prior skip. Returns the count placed on
|
|
701
|
+
the front view."""
|
|
702
|
+
rows = [] # (a_world, b_world, value) in axis order
|
|
703
|
+
for g in groups:
|
|
704
|
+
if g.feature_kind != "step":
|
|
705
|
+
continue
|
|
706
|
+
length = next(
|
|
707
|
+
(pd.param for pd in g.dims if pd.param.kind == "length" and pd.param.span is not None),
|
|
708
|
+
None,
|
|
709
|
+
)
|
|
710
|
+
if length is None or length.span is None:
|
|
711
|
+
continue
|
|
712
|
+
rows.append((length.span[0], length.span[1], length.value))
|
|
713
|
+
if not rows:
|
|
714
|
+
return 0
|
|
715
|
+
draft = dwg.draft
|
|
716
|
+
fsegs = [(dwg.at("front", *a), dwg.at("front", *b), v) for a, b, v in rows]
|
|
717
|
+
horizontal = abs(fsegs[0][1][0] - fsegs[0][0][0]) >= abs(fsegs[0][1][1] - fsegs[0][0][1])
|
|
718
|
+
|
|
719
|
+
# X-turned crowded-head detour (#307): split off each contiguous *run of ≥2*
|
|
720
|
+
# sub-floor steps (segment narrower than two arrowheads on the page), locate it as
|
|
721
|
+
# a block, and queue an enlarged detail. A single isolated thin step is left in the
|
|
722
|
+
# main chain — a one-step block would just be that step at its sub-floor width
|
|
723
|
+
# (#307 review). The legible steps + blocks stay as the main chain.
|
|
724
|
+
if horizontal:
|
|
725
|
+
floor_pg = 2 * draft.arrow_length
|
|
726
|
+
sub = [i for i, (pa, pb, _) in enumerate(fsegs) if abs(pb[0] - pa[0]) < floor_pg]
|
|
727
|
+
runs: list[list[int]] = []
|
|
728
|
+
for j in sub:
|
|
729
|
+
(runs[-1].append(j) if runs and j == runs[-1][-1] + 1 else runs.append([j]))
|
|
730
|
+
heads = [run for run in runs if len(run) >= 2]
|
|
731
|
+
if heads:
|
|
732
|
+
blocks = []
|
|
733
|
+
for run in heads:
|
|
734
|
+
ra = [rows[i] for i in run]
|
|
735
|
+
hlo = min(min(a[0], b[0]) for a, b, _ in ra)
|
|
736
|
+
hhi = max(max(a[0], b[0]) for a, b, _ in ra)
|
|
737
|
+
minlen = min(v for *_, v in ra)
|
|
738
|
+
# World→page scale for the detail (no sheet factor — detail_scale is an
|
|
739
|
+
# absolute world→page scale). (#307 review)
|
|
740
|
+
scale_needed = _MIN_STEP_SEP_MM / minlen if minlen > 0 else float("inf")
|
|
741
|
+
blocks.append((dwg.at("front", hlo, 0, 0), dwg.at("front", hhi, 0, 0), hhi - hlo))
|
|
742
|
+
|
|
743
|
+
def _redraw(dwg, view, detail_scale, _hw=ra):
|
|
744
|
+
# View-scoped name prefix so two detail views never collide (#307 review).
|
|
745
|
+
hsegs = [(dwg.at(view, *a), dwg.at(view, *b), v) for a, b, v in _hw]
|
|
746
|
+
return _draw_step_chain(dwg, view, hsegs, f"dim_{view}_steplen", detail_scale)
|
|
747
|
+
|
|
748
|
+
dwg._detail_requests.append(
|
|
749
|
+
DetailRequest(
|
|
750
|
+
axis="x",
|
|
751
|
+
lo=hlo,
|
|
752
|
+
hi=hhi,
|
|
753
|
+
scale_needed=scale_needed,
|
|
754
|
+
redraw=_redraw,
|
|
755
|
+
pad_top=2 * (draft.font_size + 2 * draft.pad_around_text)
|
|
756
|
+
+ draft.arrow_length,
|
|
757
|
+
kind="turned-head",
|
|
758
|
+
)
|
|
759
|
+
)
|
|
760
|
+
head = {i for run in heads for i in run}
|
|
761
|
+
main = [fsegs[i] for i in range(len(fsegs)) if i not in head] + blocks
|
|
762
|
+
main.sort(key=lambda s: s[0][0])
|
|
763
|
+
# The chain now mixes head-block(s) with real steps — never collapse it to a
|
|
764
|
+
# uniform "N× v" representative (a block is not a repeated step, #307 review).
|
|
765
|
+
return _draw_step_chain(dwg, "front", main, "m_steplen", allow_collapse=False)
|
|
766
|
+
|
|
767
|
+
return _draw_step_chain(dwg, "front", fsegs, "m_steplen")
|
|
768
|
+
|
|
769
|
+
|
|
694
770
|
def _detect_step_repeat(step_zs, bb_min_z, bb_max_z, tol_frac=0.10):
|
|
695
771
|
"""Return (n, rise) if *step_zs* form a uniform staircase, else None.
|
|
696
772
|
|