draftwright 0.2.0__tar.gz → 0.2.1__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.1}/CHANGELOG.md +40 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/PKG-INFO +1 -1
- {draftwright-0.2.0 → draftwright-0.2.1}/pyproject.toml +1 -1
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/_core.py +43 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/annotations/from_model.py +128 -66
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/annotations/holes.py +41 -2
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/annotations/orchestrator.py +17 -6
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/annotations/sections.py +182 -138
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/builder.py +30 -1
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/linting/coverage.py +69 -36
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_make_drawing.py +278 -2
- {draftwright-0.2.0 → draftwright-0.2.1}/.gitignore +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/LICENSE +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/README.md +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/skills/SKILL.md +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/analysis.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/annotate.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/annotations/_common.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/cli.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/drawing.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/export.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/features.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/model/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/model/detect.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/model/ir.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/repair.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/src/draftwright/sheet.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_layout.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_linting.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_part_model.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_pmi.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_recognition.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_registry.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_render_seam.py +0 -0
- {draftwright-0.2.0 → draftwright-0.2.1}/tests/test_turned_steps.py +0 -0
|
@@ -2,6 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.2.1 — 2026-06-30
|
|
6
|
+
|
|
7
|
+
A patch release focused on **turned-part dimensioning legibility**: crowded
|
|
8
|
+
step-length chains and fine turned heads are now drawn legibly instead of crammed.
|
|
9
|
+
Drawing output changes for affected turned parts.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Automatic enlarged detail view for a crowded turned head** (#304). A turned
|
|
14
|
+
part with a fine cluster of steps near one end and a long shaft (e.g. a thumbwheel
|
|
15
|
+
drive screw) cannot have its head dimensioned legibly in line at any sensible
|
|
16
|
+
scale. The head is now located as one block on the main view and broken out into
|
|
17
|
+
an enlarged **DETAIL A — SCALE n:1** — the textbook treatment — firing
|
|
18
|
+
automatically when a head's shoulders fall below the page legibility floor.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Crowded turned step-length chains stagger across two tiers** (#293) instead of
|
|
23
|
+
cramming or being skipped. When the labels would collide on one line, the ISO
|
|
24
|
+
129-1 staggered convention alternates them between a near and a far tier so every
|
|
25
|
+
step length stays legible at the drawing's own scale — no rescale needed. A roomy
|
|
26
|
+
chain stays on a single tier.
|
|
27
|
+
- **Detail views are now one unified pipeline** (#307). The prismatic step-height
|
|
28
|
+
detail and the new turned-head detail flow through a single
|
|
29
|
+
detect → request → render path; several crowded regions become DETAIL A/B/…
|
|
30
|
+
- **Output changes** for turned parts whose step chains were previously crammed, or
|
|
31
|
+
whose fine heads are now broken out into a detail view.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **A coaxial bore callout no longer overlaps the round view's centreline** (#305):
|
|
36
|
+
its leader is angled off the centre axis so the callout text sits in clear space.
|
|
37
|
+
|
|
38
|
+
### Internal
|
|
39
|
+
|
|
40
|
+
- A new **layout-cleanliness invariant test** asserts that finished drawings have no
|
|
41
|
+
view/annotation collisions across representative part archetypes, and the
|
|
42
|
+
measure-and-repack pass gained a trigger for an annotation growing into a
|
|
43
|
+
neighbouring view's line-work (so the views spread to make room).
|
|
44
|
+
|
|
5
45
|
## v0.2.0 — 2026-06-30
|
|
6
46
|
|
|
7
47
|
A major release. draftwright took ownership of feature recognition and linting
|
|
@@ -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,
|
|
@@ -584,52 +586,30 @@ def render_envelope(dwg, groups, a) -> int:
|
|
|
584
586
|
return n
|
|
585
587
|
|
|
586
588
|
|
|
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))
|
|
589
|
+
def _draw_step_chain(dwg, view, segs, name_prefix, detail_scale=None, allow_collapse=True) -> int:
|
|
590
|
+
"""Place a turned step-length chain in *view* from *segs* — each ``(pa, pb,
|
|
591
|
+
value)`` already projected to *view*'s page coords, in axis order. Orientation is
|
|
592
|
+
data (the projected span direction): horizontal → chain above the view, vertical
|
|
593
|
+
→ chain to the right. A uniform run collapses to one ``N× v`` dim (#230); else a
|
|
594
|
+
per-segment chain, staggered into a near/far tier only when crowded (ISO 129-1,
|
|
595
|
+
#293); skipped if even two tiers can't separate the labels, or if any dim would
|
|
596
|
+
fall off the page. ``detail_scale`` tags the dims for label-vs-measured lint when
|
|
597
|
+
drawing inside a scaled detail view. ``allow_collapse=False`` disables the ``N× v``
|
|
598
|
+
collapse — used when the chain mixes a synthetic head-*block* with real steps, where
|
|
599
|
+
a uniform-staircase representative would be a false claim of N equal steps (#307
|
|
600
|
+
review). Returns the count placed."""
|
|
611
601
|
if not segs:
|
|
612
602
|
return 0
|
|
613
|
-
vb = dwg.view_bounds(
|
|
603
|
+
vb = dwg.view_bounds(view)
|
|
614
604
|
if vb is None:
|
|
615
605
|
return 0
|
|
616
606
|
x0, y0, x1, y1 = vb
|
|
617
607
|
draft = dwg.draft
|
|
618
608
|
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
609
|
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
610
|
vals = [v for *_, v in segs]
|
|
631
611
|
mean_v = sum(vals) / len(vals)
|
|
632
|
-
if len(segs) >= 3 and (max(vals) - min(vals)) <= 0.10 * mean_v:
|
|
612
|
+
if allow_collapse and len(segs) >= 3 and (max(vals) - min(vals)) <= 0.10 * mean_v:
|
|
633
613
|
label = f"{len(segs)}× {_fmt(mean_v)}"
|
|
634
614
|
xs = [p[0] for pa, pb, _ in segs for p in (pa, pb)]
|
|
635
615
|
ys = [p[1] for pa, pb, _ in segs for p in (pa, pb)]
|
|
@@ -637,48 +617,47 @@ def render_step_lengths(dwg, groups) -> int:
|
|
|
637
617
|
dim = _dim((min(xs), y1, 0), (max(xs), y1, 0), "above", gap, draft, label=label)
|
|
638
618
|
else:
|
|
639
619
|
dim = _dim((x1, min(ys), 0), (x1, max(ys), 0), "right", gap, draft, label=label)
|
|
640
|
-
candidates = [("
|
|
620
|
+
candidates = [(f"{name_prefix}_typ", dim)]
|
|
641
621
|
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
|
|
622
|
+
tier_step = draft.font_size + 2 * draft.pad_around_text
|
|
623
|
+
tiers = [0] * len(segs)
|
|
648
624
|
if horizontal:
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
625
|
+
cw = [
|
|
626
|
+
((pa[0] + pb[0]) / 2, len(_fmt(v)) * draft.font_size * 0.62) for pa, pb, v in segs
|
|
627
|
+
]
|
|
628
|
+
|
|
629
|
+
def _clear(items): # (center, width) pairs in x order — labels don't overlap
|
|
630
|
+
return all(
|
|
631
|
+
c2 - c1 >= (w1 + w2) / 2 + draft.pad_around_text
|
|
632
|
+
for (c1, w1), (c2, w2) in zip(items, items[1:])
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
if _clear(cw):
|
|
636
|
+
pass # one tier suffices — no needless zig-zag for a roomy chain
|
|
637
|
+
elif _clear(cw[0::2]) and _clear(cw[1::2]):
|
|
638
|
+
tiers = [i % 2 for i in range(len(segs))] # alternate to make room
|
|
639
|
+
else:
|
|
640
|
+
_log.info("step-length chain skipped: too dense even when staggered")
|
|
658
641
|
return 0
|
|
659
|
-
if solved:
|
|
660
|
-
offsets = [s - c for s, c in zip(solved, centers)]
|
|
661
642
|
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
643
|
shoulder_ys = sorted({c for pa, pb, _ in segs for c in (pa[1], pb[1])})
|
|
665
|
-
if any(b - a <
|
|
644
|
+
if any(b - a < tier_step for a, b in zip(shoulder_ys, shoulder_ys[1:])):
|
|
666
645
|
_log.info("step-length chain skipped: shoulders too close to dimension")
|
|
667
646
|
return 0
|
|
668
647
|
|
|
669
648
|
candidates = []
|
|
670
649
|
for i, (pa, pb, value) in enumerate(segs):
|
|
671
|
-
if horizontal:
|
|
650
|
+
if horizontal:
|
|
672
651
|
p1, p2, side = (pa[0], y1, 0), (pb[0], y1, 0), "above"
|
|
673
|
-
|
|
674
|
-
else:
|
|
652
|
+
dist = gap + tiers[i] * tier_step
|
|
653
|
+
else:
|
|
675
654
|
p1, p2, side = (x1, pa[1], 0), (x1, pb[1], 0), "right"
|
|
676
|
-
|
|
677
|
-
candidates.append(
|
|
655
|
+
dist = gap
|
|
656
|
+
candidates.append(
|
|
657
|
+
(f"{name_prefix}{i}", _dim(p1, p2, side, dist, draft, label=_fmt(value)))
|
|
658
|
+
)
|
|
678
659
|
|
|
679
|
-
# Room guard
|
|
680
|
-
# page, place NONE and let lint report axial_length_missing — never run the
|
|
681
|
-
# chain off the page edge.
|
|
660
|
+
# Room guard: if any dim would fall off the drawable page, place NONE.
|
|
682
661
|
page = (_MARGIN, _MARGIN, dwg.page_w - _MARGIN, dwg.page_h - _MARGIN)
|
|
683
662
|
for _, dim in candidates:
|
|
684
663
|
box = _anno_box(dim)
|
|
@@ -687,10 +666,93 @@ def render_step_lengths(dwg, groups) -> int:
|
|
|
687
666
|
):
|
|
688
667
|
return 0
|
|
689
668
|
for name, dim in candidates:
|
|
690
|
-
|
|
669
|
+
if detail_scale is not None:
|
|
670
|
+
dim._dw_scale = detail_scale
|
|
671
|
+
dwg.add(dim, name, view=view)
|
|
691
672
|
return len(candidates)
|
|
692
673
|
|
|
693
674
|
|
|
675
|
+
def render_step_lengths(dwg, groups) -> int:
|
|
676
|
+
"""Unified turned step-length chain (ADR 0008 #223): each `StepFeature`'s length
|
|
677
|
+
span projects into the front view and joins the chain that tiles the turning axis
|
|
678
|
+
so every shoulder is located. X-turned → horizontal chain above the view;
|
|
679
|
+
Z-turned → vertical chain to the right.
|
|
680
|
+
|
|
681
|
+
A crowded **X-turned head** — a contiguous run of steps too short to dimension
|
|
682
|
+
legibly even staggered (shoulders below the page arrowhead floor) — is not crammed
|
|
683
|
+
in line: the main view locates that run as one *block* dim and an enlarged
|
|
684
|
+
`DetailRequest` (#304/#307) is queued to break it down. If the detail later can't
|
|
685
|
+
place, the block still locates the head extent and lint reports the un-located
|
|
686
|
+
interior shoulders — never worse than the prior skip. Returns the count placed on
|
|
687
|
+
the front view."""
|
|
688
|
+
rows = [] # (a_world, b_world, value) in axis order
|
|
689
|
+
for g in groups:
|
|
690
|
+
if g.feature_kind != "step":
|
|
691
|
+
continue
|
|
692
|
+
length = next(
|
|
693
|
+
(pd.param for pd in g.dims if pd.param.kind == "length" and pd.param.span is not None),
|
|
694
|
+
None,
|
|
695
|
+
)
|
|
696
|
+
if length is None or length.span is None:
|
|
697
|
+
continue
|
|
698
|
+
rows.append((length.span[0], length.span[1], length.value))
|
|
699
|
+
if not rows:
|
|
700
|
+
return 0
|
|
701
|
+
draft = dwg.draft
|
|
702
|
+
fsegs = [(dwg.at("front", *a), dwg.at("front", *b), v) for a, b, v in rows]
|
|
703
|
+
horizontal = abs(fsegs[0][1][0] - fsegs[0][0][0]) >= abs(fsegs[0][1][1] - fsegs[0][0][1])
|
|
704
|
+
|
|
705
|
+
# X-turned crowded-head detour (#307): split off each contiguous *run of ≥2*
|
|
706
|
+
# sub-floor steps (segment narrower than two arrowheads on the page), locate it as
|
|
707
|
+
# a block, and queue an enlarged detail. A single isolated thin step is left in the
|
|
708
|
+
# main chain — a one-step block would just be that step at its sub-floor width
|
|
709
|
+
# (#307 review). The legible steps + blocks stay as the main chain.
|
|
710
|
+
if horizontal:
|
|
711
|
+
floor_pg = 2 * draft.arrow_length
|
|
712
|
+
sub = [i for i, (pa, pb, _) in enumerate(fsegs) if abs(pb[0] - pa[0]) < floor_pg]
|
|
713
|
+
runs: list[list[int]] = []
|
|
714
|
+
for j in sub:
|
|
715
|
+
(runs[-1].append(j) if runs and j == runs[-1][-1] + 1 else runs.append([j]))
|
|
716
|
+
heads = [run for run in runs if len(run) >= 2]
|
|
717
|
+
if heads:
|
|
718
|
+
blocks = []
|
|
719
|
+
for run in heads:
|
|
720
|
+
ra = [rows[i] for i in run]
|
|
721
|
+
hlo = min(min(a[0], b[0]) for a, b, _ in ra)
|
|
722
|
+
hhi = max(max(a[0], b[0]) for a, b, _ in ra)
|
|
723
|
+
minlen = min(v for *_, v in ra)
|
|
724
|
+
# World→page scale for the detail (no sheet factor — detail_scale is an
|
|
725
|
+
# absolute world→page scale). (#307 review)
|
|
726
|
+
scale_needed = _MIN_STEP_SEP_MM / minlen if minlen > 0 else float("inf")
|
|
727
|
+
blocks.append((dwg.at("front", hlo, 0, 0), dwg.at("front", hhi, 0, 0), hhi - hlo))
|
|
728
|
+
|
|
729
|
+
def _redraw(dwg, view, detail_scale, _hw=ra):
|
|
730
|
+
# View-scoped name prefix so two detail views never collide (#307 review).
|
|
731
|
+
hsegs = [(dwg.at(view, *a), dwg.at(view, *b), v) for a, b, v in _hw]
|
|
732
|
+
return _draw_step_chain(dwg, view, hsegs, f"dim_{view}_steplen", detail_scale)
|
|
733
|
+
|
|
734
|
+
dwg._detail_requests.append(
|
|
735
|
+
DetailRequest(
|
|
736
|
+
axis="x",
|
|
737
|
+
lo=hlo,
|
|
738
|
+
hi=hhi,
|
|
739
|
+
scale_needed=scale_needed,
|
|
740
|
+
redraw=_redraw,
|
|
741
|
+
pad_top=2 * (draft.font_size + 2 * draft.pad_around_text)
|
|
742
|
+
+ draft.arrow_length,
|
|
743
|
+
kind="turned-head",
|
|
744
|
+
)
|
|
745
|
+
)
|
|
746
|
+
head = {i for run in heads for i in run}
|
|
747
|
+
main = [fsegs[i] for i in range(len(fsegs)) if i not in head] + blocks
|
|
748
|
+
main.sort(key=lambda s: s[0][0])
|
|
749
|
+
# The chain now mixes head-block(s) with real steps — never collapse it to a
|
|
750
|
+
# uniform "N× v" representative (a block is not a repeated step, #307 review).
|
|
751
|
+
return _draw_step_chain(dwg, "front", main, "m_steplen", allow_collapse=False)
|
|
752
|
+
|
|
753
|
+
return _draw_step_chain(dwg, "front", fsegs, "m_steplen")
|
|
754
|
+
|
|
755
|
+
|
|
694
756
|
def _detect_step_repeat(step_zs, bb_min_z, bb_max_z, tol_frac=0.10):
|
|
695
757
|
"""Return (n, rise) if *step_zs* form a uniform staircase, else None.
|
|
696
758
|
|
|
@@ -514,6 +514,37 @@ def _annotate_holes(dwg, a: Analysis, view_of_axis, groups, feature_keys):
|
|
|
514
514
|
return centre
|
|
515
515
|
return (centre[0] + dx / norm * r, centre[1] + dy / norm * r)
|
|
516
516
|
|
|
517
|
+
def _coaxial_lift(centre, ny, view_cx, view_cy, y_min, y_max):
|
|
518
|
+
"""Leader row for a hole, lifted clear of the round view's centre axis when
|
|
519
|
+
the hole is a *coaxial bore* (#305); *ny* unchanged otherwise.
|
|
520
|
+
|
|
521
|
+
A bore on the turning axis is led out along the view's horizontal centre
|
|
522
|
+
axis, so the centre mark / centreline runs straight through the "⌀… ↓…"
|
|
523
|
+
callout text. Detect that one bore — a rotational part, hole at the view
|
|
524
|
+
centre — and lift its row a clearance off the axis (an angled leader to a
|
|
525
|
+
central feature is standard practice), toward the roomier side. Off-axis
|
|
526
|
+
holes and every prismatic-part hole are untouched (front-view round parts
|
|
527
|
+
place coaxial bores as vertical shafts below the view, not along an axis,
|
|
528
|
+
so they can't hit this and are exempt by construction).
|
|
529
|
+
|
|
530
|
+
Tactical: the principled fix is to not draw the crossing line at all — a
|
|
531
|
+
centred bore is located by the axis, so its linear location dims are
|
|
532
|
+
redundant (#309) — or to make this a layout-solver separation constraint
|
|
533
|
+
(ADR 0003). This nudge becomes dead code once either lands."""
|
|
534
|
+
tol = draft.font_size # "hole at the view centre" tolerance (page mm)
|
|
535
|
+
if not (
|
|
536
|
+
a.is_rotational and abs(centre[0] - view_cx) < tol and abs(centre[1] - view_cy) < tol
|
|
537
|
+
):
|
|
538
|
+
return ny
|
|
539
|
+
# Lift the row a full text height + padding clear of the axis: enough for
|
|
540
|
+
# the text box (half a font tall) to sit wholly off the centre line with a
|
|
541
|
+
# pad of margin, giving a legible leader angle rather than a near-flat one.
|
|
542
|
+
lift = draft.font_size + 3 * draft.pad_around_text
|
|
543
|
+
# Toward the roomier half-view (geometric, not occupancy-aware — safe here
|
|
544
|
+
# because the round view of a coaxial bore is otherwise near-empty).
|
|
545
|
+
up = (y_max - view_cy) >= (view_cy - y_min)
|
|
546
|
+
return min(view_cy + lift, y_max) if up else max(view_cy - lift, y_min)
|
|
547
|
+
|
|
517
548
|
def _add(view, i, tip, elbow, side, callout):
|
|
518
549
|
dwg.add(
|
|
519
550
|
Leader(
|
|
@@ -598,6 +629,12 @@ def _annotate_holes(dwg, a: Analysis, view_of_axis, groups, feature_keys):
|
|
|
598
629
|
else:
|
|
599
630
|
y_min, y_max = a.SV_Y - a.fv_hh, a.SV_Y + a.fv_hh
|
|
600
631
|
|
|
632
|
+
# Round view's horizontal centre axis — a coaxial bore led out along it has
|
|
633
|
+
# its callout text crossed by the centre mark / centreline (#305); see
|
|
634
|
+
# _coaxial_lift.
|
|
635
|
+
view_cx = a.PV_X if view == "plan" else a.SV_X
|
|
636
|
+
view_cy = a.PV_Y if view == "plan" else a.SV_Y
|
|
637
|
+
|
|
601
638
|
# --- Pass 1: boundary assignment ---
|
|
602
639
|
right_queue = [] # (locs, dia, callout, feat, natural_y, rep)
|
|
603
640
|
left_queue = []
|
|
@@ -632,9 +669,11 @@ def _annotate_holes(dwg, a: Analysis, view_of_axis, groups, feature_keys):
|
|
|
632
669
|
continue
|
|
633
670
|
|
|
634
671
|
if can_right and (not can_left or d_right <= d_left):
|
|
635
|
-
|
|
672
|
+
ny = _coaxial_lift(centre_r, centre_r[1], view_cx, view_cy, y_min, y_max)
|
|
673
|
+
right_queue.append((locs, dia, callout, feat, ny, rep_r))
|
|
636
674
|
else:
|
|
637
|
-
|
|
675
|
+
ny = _coaxial_lift(centre_l, centre_l[1], view_cx, view_cy, y_min, y_max)
|
|
676
|
+
left_queue.append((locs, dia, callout, feat, ny, rep_l))
|
|
638
677
|
|
|
639
678
|
# Sort each queue by natural Y so leaders don't cross.
|
|
640
679
|
right_queue.sort(key=lambda s: s[4])
|
|
@@ -44,7 +44,11 @@ from draftwright.annotations.holes import (
|
|
|
44
44
|
_annotate_holes,
|
|
45
45
|
_locate_off_axis_holes,
|
|
46
46
|
)
|
|
47
|
-
from draftwright.annotations.sections import
|
|
47
|
+
from draftwright.annotations.sections import (
|
|
48
|
+
_add_section_view,
|
|
49
|
+
_request_prismatic_detail,
|
|
50
|
+
_resolve_details,
|
|
51
|
+
)
|
|
48
52
|
from draftwright.model import build_part_model, plan_dimensions, plan_sections
|
|
49
53
|
from draftwright.recognition import (
|
|
50
54
|
full_cylinders,
|
|
@@ -99,6 +103,7 @@ def _auto_annotate(dwg, a: Analysis, *, detail_view: bool = False):
|
|
|
99
103
|
# not accumulate duplicate drop records.
|
|
100
104
|
dwg._reset_build_issues()
|
|
101
105
|
dwg._reset_dropped_callout_diams()
|
|
106
|
+
dwg._detail_requests = [] # renderers queue enlarged-detail requests here (#307)
|
|
102
107
|
|
|
103
108
|
FX = a.proj.front_x
|
|
104
109
|
FZ = a.proj.front_z
|
|
@@ -223,18 +228,19 @@ def _auto_annotate(dwg, a: Analysis, *, detail_view: bool = False):
|
|
|
223
228
|
if section is not None:
|
|
224
229
|
_add_section_view(dwg, a, section)
|
|
225
230
|
|
|
226
|
-
#
|
|
231
|
+
# Prismatic step-height detail: queue it (only when build_drawing(detail_view=True))
|
|
232
|
+
# — resolved with every other detail request below (#307).
|
|
227
233
|
if detail_view:
|
|
228
|
-
|
|
234
|
+
_request_prismatic_detail(dwg, a)
|
|
229
235
|
|
|
230
236
|
# Turned-part dimensions via the IR (ADR 0008 convergence). The model is built
|
|
231
237
|
# once and fed to both renderers (#229 — no per-pass rebuild):
|
|
232
238
|
# - diameters: ø leaders, row below (X) / column left (Z), one path by frame
|
|
233
239
|
# axis. Replaces _annotate_turned_diameters.
|
|
234
240
|
# - step lengths: the chain that locates every shoulder, X and Z from one path
|
|
235
|
-
# (#223).
|
|
236
|
-
#
|
|
237
|
-
#
|
|
241
|
+
# (#223). A crowded X-turned head queues an enlarged detail request (#304/#307)
|
|
242
|
+
# instead of cramming; the envelope dim along the turning axis was suppressed
|
|
243
|
+
# so the chain does not double-dimension the length.
|
|
238
244
|
render_diameters(dwg, _groups)
|
|
239
245
|
if a.prof is not None:
|
|
240
246
|
render_step_lengths(dwg, _groups)
|
|
@@ -249,6 +255,11 @@ def _auto_annotate(dwg, a: Analysis, *, detail_view: bool = False):
|
|
|
249
255
|
# after every hole/diameter pass so it claims strip space last.
|
|
250
256
|
render_slots(dwg, _model, a)
|
|
251
257
|
|
|
258
|
+
# Resolve every queued enlarged-detail request (#307) — prismatic step bands and
|
|
259
|
+
# crowded turned heads alike — through the one generic detailer, now that all
|
|
260
|
+
# views and main-view annotations are placed (so the detail avoids them).
|
|
261
|
+
_resolve_details(dwg, a)
|
|
262
|
+
|
|
252
263
|
# Phase 7 — strip footprint debug logging + post-placement overflow check.
|
|
253
264
|
# Overflow can only occur when outer_limit was tightened after allocations
|
|
254
265
|
# were already committed (e.g. iso-x tightening or iso-y cap guard).
|