draftwright 0.2.4__tar.gz → 0.2.5__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.4 → draftwright-0.2.5}/CHANGELOG.md +9 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/PKG-INFO +1 -1
- {draftwright-0.2.4 → draftwright-0.2.5}/pyproject.toml +1 -1
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/_core.py +16 -5
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/annotate.py +1 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/annotations/_common.py +12 -4
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/annotations/from_model.py +152 -50
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/annotations/holes.py +502 -29
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/annotations/orchestrator.py +41 -43
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/annotations/sections.py +58 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/builder.py +154 -9
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/cli.py +2 -5
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/drawing.py +571 -9
- draftwright-0.2.5/src/draftwright/intents.py +39 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/model/planner.py +15 -9
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/registry.py +36 -2
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/layout_snapshots/box.json +47 -47
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/layout_snapshots/bracket.json +86 -86
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/layout_snapshots/drive_screw_x.json +36 -36
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/layout_snapshots/dshape.json +54 -54
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/layout_snapshots/flange.json +66 -66
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/layout_snapshots/holed_slot.json +71 -71
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/layout_snapshots/plate_holes.json +61 -61
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/layout_snapshots/side_drilled.json +50 -50
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/layout_snapshots/slotted.json +72 -72
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/layout_snapshots/turned_shaft.json +79 -79
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_e2e_standards.py +62 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_make_drawing.py +1350 -16
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_render_seam.py +6 -2
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_strip_layout.py +7 -8
- {draftwright-0.2.4 → draftwright-0.2.5}/.gitignore +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/LICENSE +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/README.md +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/skills/SKILL.md +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/analysis.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/export.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/features.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/model/__init__.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/model/detect.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/model/ir.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/repair.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/src/draftwright/sheet.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_layout.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_layout_snapshot.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_linting.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_part_model.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_pmi.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_recognition.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_registry.py +0 -0
- {draftwright-0.2.4 → draftwright-0.2.5}/tests/test_turned_steps.py +0 -0
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Dimension-line spacing now follows ISO 129-1 / ASME Y14.5 convention** (#347).
|
|
8
|
+
The first dimension line sits further from the view outline (first-line gap
|
|
9
|
+
8 → 10 mm) and successive parallel lines stack tighter and uniform (between-line
|
|
10
|
+
clear gap 4 → 2.5 mm). The inter-view corridor widens in step (`_DIM_PAD`
|
|
11
|
+
18 → 20 mm) so the wider first-line gap does not crowd the between-view
|
|
12
|
+
dimensions. Re-drifts the layout of every drawing.
|
|
13
|
+
|
|
5
14
|
## v0.2.4 — 2026-07-03
|
|
6
15
|
|
|
7
16
|
A follow-up patch on the ADR 0009 placement rebuild in 0.2.3: it finishes unifying
|
|
@@ -162,6 +162,15 @@ def _dim(p1, p2, side, distance, draft, **kwargs):
|
|
|
162
162
|
return d
|
|
163
163
|
|
|
164
164
|
|
|
165
|
+
# Dimension-line spacing (page-mm, scale-independent), the single source of truth for
|
|
166
|
+
# BOTH the ADR 0009 strip carve (via the `Strip` dataclass defaults below) and the
|
|
167
|
+
# sheet.py halo/depth estimates that must reserve the same space. Per ISO 129-1 / ASME
|
|
168
|
+
# Y14.5, the FIRST dimension line sits furthest from the outline (clears the outline +
|
|
169
|
+
# extension-line origins) and subsequent parallel lines stack tighter and uniform (#347).
|
|
170
|
+
_STRIP_GAP = 10.0 # clearance between the view outline and the first dimension line
|
|
171
|
+
_STRIP_SPACING = 2.5 # clear gap between successive parallel dimension lines (beyond the label)
|
|
172
|
+
|
|
173
|
+
|
|
165
174
|
@dataclass
|
|
166
175
|
class Strip:
|
|
167
176
|
"""A one-dimensional annotation band adjacent to an orthographic view.
|
|
@@ -184,8 +193,8 @@ class Strip:
|
|
|
184
193
|
anchor: float
|
|
185
194
|
outer_limit: float
|
|
186
195
|
direction: float = 1.0
|
|
187
|
-
gap: float =
|
|
188
|
-
spacing: float =
|
|
196
|
+
gap: float = _STRIP_GAP
|
|
197
|
+
spacing: float = _STRIP_SPACING
|
|
189
198
|
|
|
190
199
|
@property
|
|
191
200
|
def available(self) -> float:
|
|
@@ -220,7 +229,6 @@ _SLOT_DIM_DEPTH = 2 * _FONT_SIZE + _PAD # sv_zones.below: overall depth dimensi
|
|
|
220
229
|
_SLOT_DIM_HEIGHT = 2 * _FONT_SIZE + 2 * _PAD # fv_zones.right: overall height dim
|
|
221
230
|
|
|
222
231
|
|
|
223
|
-
_STRIP_SPACING = 4.0 # page-mm between successive annotations in a strip
|
|
224
232
|
_MIN_VIEW_MM = (
|
|
225
233
|
10.0 # min projected view dimension; below it annotation geometry degenerates (#129)
|
|
226
234
|
)
|
|
@@ -572,8 +580,11 @@ def _iso_bbox(dwg):
|
|
|
572
580
|
# Relocated from make_drawing for the sheet.py split (#162). Shared by sheet.py
|
|
573
581
|
# (choose_scale/_layout_geometry) and make_drawing's repack pass, so they live
|
|
574
582
|
# here in the shared base to keep the DAG acyclic.
|
|
575
|
-
|
|
576
|
-
_STRIP_GAP
|
|
583
|
+
# The base inter-view corridor: one first-line gap + one dimension tier. Tracks
|
|
584
|
+
# _STRIP_GAP so widening the first-line gap (#347) keeps the below-plan / between-view
|
|
585
|
+
# corridors from razor-fitting the first dim line (the #130 slack guarantee): 10 + 10.
|
|
586
|
+
_DIM_PAD = _STRIP_GAP + _SLOT_DIM_HEIGHT # 20.0
|
|
587
|
+
# _STRIP_GAP / _STRIP_SPACING are defined above (beside the `Strip` dataclass they seed).
|
|
577
588
|
|
|
578
589
|
_PAGE_SIZES = {
|
|
579
590
|
"A4": (297.0, 210.0),
|
|
@@ -302,6 +302,9 @@ class CorridorCandidate:
|
|
|
302
302
|
dedup: tuple | None = None
|
|
303
303
|
precedence: int = 0
|
|
304
304
|
force: bool = False
|
|
305
|
+
# The source IR feature this dim was rendered for — recorded as provenance when the
|
|
306
|
+
# dim is placed at drain (ADR 0010). ``None`` leaves the annotation feature-less.
|
|
307
|
+
feature: object | None = None
|
|
305
308
|
|
|
306
309
|
|
|
307
310
|
def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
@@ -353,13 +356,16 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
353
356
|
_promote_losers(c)
|
|
354
357
|
return
|
|
355
358
|
pairs = [(c.name, c.build) for c in kept]
|
|
356
|
-
|
|
359
|
+
feats = {c.name: c.feature for c in kept if c.feature is not None} # provenance (ADR 0010)
|
|
360
|
+
left = {
|
|
361
|
+
n for n, _ in place_strip_candidates(dwg, strip, view, axis, pairs, tier, features=feats)
|
|
362
|
+
}
|
|
357
363
|
force_pairs = [(c.name, c.build) for c in kept if c.name in left and c.force]
|
|
358
364
|
still = (
|
|
359
365
|
{
|
|
360
366
|
n
|
|
361
367
|
for n, _ in place_strip_candidates(
|
|
362
|
-
dwg, strip, view, axis, force_pairs, tier, force=True
|
|
368
|
+
dwg, strip, view, axis, force_pairs, tier, force=True, features=feats
|
|
363
369
|
)
|
|
364
370
|
}
|
|
365
371
|
if force_pairs
|
|
@@ -393,7 +399,7 @@ def drain_corridors(dwg):
|
|
|
393
399
|
dwg._corridor_batch = {}
|
|
394
400
|
|
|
395
401
|
|
|
396
|
-
def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False):
|
|
402
|
+
def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False, features=None):
|
|
397
403
|
"""Collect-then-solve placement of location/feature dims on one strip (ADR 0009).
|
|
398
404
|
The single shared strip placer that retires the ``Strip.allocate`` cursor (#150,
|
|
399
405
|
P3): each candidate in *cands* — an ``(name, build(pos)->dim)`` pair — is spaced by
|
|
@@ -481,7 +487,9 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False):
|
|
|
481
487
|
if not force and _box_hits(_geom_box(dim), blockers): # corridor crosses a leader
|
|
482
488
|
todo.append((name, build))
|
|
483
489
|
continue
|
|
484
|
-
|
|
490
|
+
# Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
|
|
491
|
+
# dims — `features` maps this batch's names to their source IR feature.
|
|
492
|
+
dwg.add(dim, name, view=view, feature=(features or {}).get(name))
|
|
485
493
|
return todo
|
|
486
494
|
|
|
487
495
|
|
|
@@ -151,13 +151,19 @@ def _record_slot_drop(dwg, kind, idx, view, feat):
|
|
|
151
151
|
)
|
|
152
152
|
|
|
153
153
|
|
|
154
|
-
def render_slots(dwg, model, a) -> int:
|
|
154
|
+
def render_slots(dwg, model, a, *, only=None) -> int:
|
|
155
155
|
"""Dimension milled slots from the IR — width (the defining size, across
|
|
156
156
|
``width_axis``) + length (along ``long_axis``) + a position dim from the part
|
|
157
157
|
datum, in the view the two axes span. Places through the engine's zone strips
|
|
158
158
|
(shared infra, ADR 0008 Amend. 4); a dim with no clear room is dropped and
|
|
159
159
|
recorded at info severity (place-what-fits). Sources `SlotFeature`s from the
|
|
160
|
-
model; replaces the engine's `_annotate_slots`. Returns the count placed.
|
|
160
|
+
model; replaces the engine's `_annotate_slots`. Returns the count placed.
|
|
161
|
+
|
|
162
|
+
``only`` (a set of `SlotFeature`s, #426 Phase 2b) restricts placement to a recorded
|
|
163
|
+
subset for ``finalize()``; ``only=None`` (the auto-pass) places all slots, byte-
|
|
164
|
+
identically. Skips filtered slots **in place** so ``i`` stays the slot's model index
|
|
165
|
+
(the ``m_slot{i}_*`` names must match the auto-pass — never re-enumerate a compacted
|
|
166
|
+
list)."""
|
|
161
167
|
slots = [f for f in model.features if f.kind == "slot"]
|
|
162
168
|
if not slots:
|
|
163
169
|
return 0
|
|
@@ -174,6 +180,8 @@ def render_slots(dwg, model, a) -> int:
|
|
|
174
180
|
|
|
175
181
|
count = 0
|
|
176
182
|
for i, s in enumerate(slots):
|
|
183
|
+
if only is not None and s not in only:
|
|
184
|
+
continue # #426 Ph2b: skip in place — i must stay the model index
|
|
177
185
|
view = views[frozenset((s.width_axis, s.long_axis))]
|
|
178
186
|
name, zones, h_axis, h_proj, _v_axis, v_proj = view
|
|
179
187
|
|
|
@@ -244,7 +252,13 @@ def render_slots(dwg, model, a) -> int:
|
|
|
244
252
|
|
|
245
253
|
def _below_or_drop(nm, _bs=below_strip, _bh=below_hi, _feat=s, _dw=drop_word):
|
|
246
254
|
if _bs is not None and not place_strip_candidates(
|
|
247
|
-
dwg,
|
|
255
|
+
dwg,
|
|
256
|
+
_bs,
|
|
257
|
+
vw[0],
|
|
258
|
+
"y",
|
|
259
|
+
[_cand_for("below", _bh)],
|
|
260
|
+
tier,
|
|
261
|
+
features={cname: _feat},
|
|
248
262
|
):
|
|
249
263
|
return # placed on the below strip
|
|
250
264
|
_record_slot_drop(dwg, _dw, idx, vw[0], _feat)
|
|
@@ -275,6 +289,7 @@ def render_slots(dwg, model, a) -> int:
|
|
|
275
289
|
),
|
|
276
290
|
precedence=1 if is_pos else 0,
|
|
277
291
|
force=False,
|
|
292
|
+
feature=s, # provenance (ADR 0010): this dim belongs to the slot
|
|
278
293
|
),
|
|
279
294
|
)
|
|
280
295
|
return True # deferred — the callback owns the drop; caller's else must not fire
|
|
@@ -286,7 +301,7 @@ def render_slots(dwg, model, a) -> int:
|
|
|
286
301
|
continue
|
|
287
302
|
axis = "y" if side in ("above", "below") else "x"
|
|
288
303
|
if not place_strip_candidates(
|
|
289
|
-
dwg, strip, vw[0], axis, [_cand_for(side, hi)], tier
|
|
304
|
+
dwg, strip, vw[0], axis, [_cand_for(side, hi)], tier, features={cname: s}
|
|
290
305
|
):
|
|
291
306
|
return True
|
|
292
307
|
return False
|
|
@@ -329,7 +344,7 @@ _SIZE_SUBCHAIN = 0
|
|
|
329
344
|
_LOC_SUBCHAIN = 1
|
|
330
345
|
|
|
331
346
|
|
|
332
|
-
def _location_candidate(dwg, name, *, view, span_key, distance, build):
|
|
347
|
+
def _location_candidate(dwg, name, *, view, span_key, distance, build, feature=None):
|
|
333
348
|
"""A :class:`CorridorCandidate` for a datum-referenced hole/pattern location dim.
|
|
334
349
|
Location dims outrank a coincident slot-position line in dedup (#345) and form the
|
|
335
350
|
outer, datum-distance-ordered run of the ladder (#346). Force-kept (policy B): a plan-X
|
|
@@ -353,16 +368,22 @@ def _location_candidate(dwg, name, *, view, span_key, distance, build):
|
|
|
353
368
|
dedup=(view, span_key[0], span_key[1]),
|
|
354
369
|
precedence=2,
|
|
355
370
|
force=True,
|
|
371
|
+
feature=feature, # provenance (ADR 0010): the located hole/pattern
|
|
356
372
|
)
|
|
357
373
|
|
|
358
374
|
|
|
359
|
-
def render_locations(dwg, model, a) -> int:
|
|
375
|
+
def render_locations(dwg, model, a, *, only=None) -> int:
|
|
360
376
|
"""Baseline X/Y hole-location dims from the IR (#238). The planner decides the
|
|
361
377
|
intent (`plan_locations`: which refs, from which datum); this renderer owns the
|
|
362
378
|
layout (Amendment 4) — X dims tier above the plan view, Y dims above the side
|
|
363
379
|
view, nearest-datum-first, legibility-gated, allocated from the existing strips;
|
|
364
380
|
a ref with no room is dropped as `location_ref_dropped`. Replaces the engine's
|
|
365
|
-
`_add_location_dims`. Returns the count placed.
|
|
381
|
+
`_add_location_dims`. Returns the count placed.
|
|
382
|
+
|
|
383
|
+
*only*, when given, restricts placement to refs whose source feature is in the set —
|
|
384
|
+
the #426 finalize() path passes the recorded ``locate`` intents' features so the
|
|
385
|
+
corridor solve runs over the user's edited subset. ``None`` (the auto-pass) places
|
|
386
|
+
every ref, byte-identically."""
|
|
366
387
|
planned = plan_locations(model)
|
|
367
388
|
if not planned:
|
|
368
389
|
return 0
|
|
@@ -372,6 +393,8 @@ def render_locations(dwg, model, a) -> int:
|
|
|
372
393
|
datum_x, datum_y = datum.at[0], datum.at[1]
|
|
373
394
|
refs = []
|
|
374
395
|
for pd in planned:
|
|
396
|
+
if only is not None and pd.feature not in only: # #426: recorded subset only
|
|
397
|
+
continue
|
|
375
398
|
if pd.param.span is None:
|
|
376
399
|
continue
|
|
377
400
|
rx, ry = pd.param.span[1][0], pd.param.span[1][1]
|
|
@@ -385,12 +408,27 @@ def render_locations(dwg, model, a) -> int:
|
|
|
385
408
|
and math.hypot(rx - a.cx, ry - a.cy) <= _CONCENTRIC_TOL_MM
|
|
386
409
|
):
|
|
387
410
|
continue
|
|
388
|
-
refs.append((rx, ry))
|
|
411
|
+
refs.append((rx, ry, pd.feature)) # carry the source feature for provenance (ADR 0010)
|
|
389
412
|
if not refs:
|
|
390
413
|
return 0
|
|
391
414
|
tier = draft.font_size + 2 * draft.pad_around_text
|
|
392
415
|
n = 0
|
|
393
416
|
|
|
417
|
+
# Location-dim names. The auto-pass (only is None) numbers them positionally —
|
|
418
|
+
# m_locx{i}, the historical byte-identical scheme. The finalize() path (only set) may
|
|
419
|
+
# run AFTER live-replayed locate() dims already hold m_loc names, so there it allocates
|
|
420
|
+
# the first FREE index to avoid Drawing.add silently replacing one (#429 review).
|
|
421
|
+
_loc_used = set(dwg._named) if only is not None else None
|
|
422
|
+
|
|
423
|
+
def _loc_name(prefix: str, i: int) -> str:
|
|
424
|
+
if _loc_used is None:
|
|
425
|
+
return f"{prefix}{i}" # auto-pass: unchanged, byte-identical
|
|
426
|
+
j = 0
|
|
427
|
+
while f"{prefix}{j}" in _loc_used:
|
|
428
|
+
j += 1
|
|
429
|
+
_loc_used.add(f"{prefix}{j}")
|
|
430
|
+
return f"{prefix}{j}"
|
|
431
|
+
|
|
394
432
|
# --- X locations: tier above the plan view ---
|
|
395
433
|
PX, PY = a.proj.plan_x, a.proj.plan_y
|
|
396
434
|
x_refs: list = []
|
|
@@ -415,10 +453,14 @@ def render_locations(dwg, model, a) -> int:
|
|
|
415
453
|
# pass carving around the other and interleaving. No alternate view for a plan-X
|
|
416
454
|
# location, so a corridor-blocked dim is force-kept (policy B), not relocated; only a
|
|
417
455
|
# physically full strip drops (→ location_ref_dropped, escalates the hole table).
|
|
418
|
-
for i, (rx, ry) in enumerate(sorted(x_refs, key=lambda r: abs(r[0] - datum_x))):
|
|
456
|
+
for i, (rx, ry, feat) in enumerate(sorted(x_refs, key=lambda r: abs(r[0] - datum_x))):
|
|
419
457
|
if abs(rx - datum_x) * a.SCALE < 1.0:
|
|
420
458
|
continue # on the datum edge — nothing to dimension
|
|
421
459
|
n += 1
|
|
460
|
+
# A single X-location dim shared by two *distinct* features at this X belongs to
|
|
461
|
+
# neither exclusively — leave it unowned so drop() cannot over-strip a sibling's
|
|
462
|
+
# dimension and annotations_of never over-claims it (review #406, ADR 0010).
|
|
463
|
+
_xfeat = None if any(abs(o[0] - rx) < 0.5 and o[2] != feat for o in refs) else feat
|
|
422
464
|
register_corridor(
|
|
423
465
|
dwg,
|
|
424
466
|
("plan", "above"),
|
|
@@ -428,7 +470,7 @@ def render_locations(dwg, model, a) -> int:
|
|
|
428
470
|
tier,
|
|
429
471
|
_location_candidate(
|
|
430
472
|
dwg,
|
|
431
|
-
|
|
473
|
+
_loc_name("m_locx", i),
|
|
432
474
|
view="plan",
|
|
433
475
|
span_key=(round(PX(datum_x), 1), round(PX(rx), 1)),
|
|
434
476
|
distance=abs(rx - datum_x),
|
|
@@ -440,6 +482,7 @@ def render_locations(dwg, model, a) -> int:
|
|
|
440
482
|
draft,
|
|
441
483
|
label=_fmt(_rx - datum_x),
|
|
442
484
|
),
|
|
485
|
+
feature=_xfeat,
|
|
443
486
|
),
|
|
444
487
|
)
|
|
445
488
|
|
|
@@ -466,12 +509,14 @@ def render_locations(dwg, model, a) -> int:
|
|
|
466
509
|
# Cap the side-above strip below the iso view so Y-location dims never run under it
|
|
467
510
|
# (the carve respects outer_limit); the dim_pitch_side dims are obstacles the carve
|
|
468
511
|
# avoids structurally, retiring the old manual allocate(10.0) reservation + cursor.
|
|
469
|
-
if y_refs and any(SX(ry) + 10 > iso_x0 - 4 for _, ry in y_refs):
|
|
512
|
+
if y_refs and any(SX(ry) + 10 > iso_x0 - 4 for _, ry, _ in y_refs):
|
|
470
513
|
a.sv_zones.above.outer_limit = min(a.sv_zones.above.outer_limit, iso_y0 - 4)
|
|
471
|
-
for i, (rx, ry) in enumerate(sorted(y_refs, key=lambda r: abs(r[1] - datum_y))):
|
|
514
|
+
for i, (rx, ry, feat) in enumerate(sorted(y_refs, key=lambda r: abs(r[1] - datum_y))):
|
|
472
515
|
if abs(ry - datum_y) * a.SCALE < 1.0:
|
|
473
516
|
continue
|
|
474
517
|
n += 1
|
|
518
|
+
# Shared-Y location dim → unowned (see the X loop; review #406).
|
|
519
|
+
_yfeat = None if any(abs(o[1] - ry) < 0.5 and o[2] != feat for o in refs) else feat
|
|
475
520
|
register_corridor(
|
|
476
521
|
dwg,
|
|
477
522
|
("side", "above"),
|
|
@@ -481,7 +526,7 @@ def render_locations(dwg, model, a) -> int:
|
|
|
481
526
|
tier,
|
|
482
527
|
_location_candidate(
|
|
483
528
|
dwg,
|
|
484
|
-
|
|
529
|
+
_loc_name("m_locy", i),
|
|
485
530
|
view="side",
|
|
486
531
|
span_key=(round(SX(datum_y), 1), round(SX(ry), 1)),
|
|
487
532
|
distance=abs(ry - datum_y),
|
|
@@ -493,6 +538,7 @@ def render_locations(dwg, model, a) -> int:
|
|
|
493
538
|
draft,
|
|
494
539
|
label=_fmt(_ry - datum_y),
|
|
495
540
|
),
|
|
541
|
+
feature=_yfeat,
|
|
496
542
|
),
|
|
497
543
|
)
|
|
498
544
|
return n
|
|
@@ -513,7 +559,7 @@ def render_centermarks(dwg, groups) -> int:
|
|
|
513
559
|
members = feat.members or (g.anchor,)
|
|
514
560
|
for loc in members:
|
|
515
561
|
px, py, *_ = dwg.at(view, *loc)
|
|
516
|
-
dwg.add(CenterMark((px, py, 0), size, dwg.draft), f"m_cm{n}", view=view)
|
|
562
|
+
dwg.add(CenterMark((px, py, 0), size, dwg.draft), f"m_cm{n}", view=view, feature=feat)
|
|
517
563
|
n += 1
|
|
518
564
|
return n
|
|
519
565
|
|
|
@@ -532,7 +578,7 @@ def _mentioned_diameters(dwg) -> set[float]:
|
|
|
532
578
|
return diams
|
|
533
579
|
|
|
534
580
|
|
|
535
|
-
def _diameter_row_below(dwg, items) -> int:
|
|
581
|
+
def _diameter_row_below(dwg, items, start: int = 0) -> int:
|
|
536
582
|
"""ø-callout row BELOW the front view for X-turned step/boss diameters (#77).
|
|
537
583
|
*items* is ``[(anchor, diameter), ...]``. The row is dropped clear of anything
|
|
538
584
|
already below the profile; labels spread along page-x by the ADR-0003 strip
|
|
@@ -553,30 +599,31 @@ def _diameter_row_below(dwg, items) -> int:
|
|
|
553
599
|
label_y = obstacle_bottom - (draft.font_size + 4 * draft.pad_around_text)
|
|
554
600
|
if label_y < _MARGIN + draft.font_size:
|
|
555
601
|
return 0
|
|
556
|
-
specs = [] # (tip_page, label), tip on the step's bottom silhouette
|
|
557
|
-
for anchor, dia in items:
|
|
602
|
+
specs = [] # (tip_page, label, feature), tip on the step's bottom silhouette
|
|
603
|
+
for anchor, dia, feat in items:
|
|
558
604
|
ax, ay, az = anchor
|
|
559
605
|
tip = dwg.at("front", ax, ay, az - dia / 2)
|
|
560
|
-
specs.append((tip, f"ø{_fmt(dia)}"))
|
|
606
|
+
specs.append((tip, f"ø{_fmt(dia)}", feat))
|
|
561
607
|
specs.sort(key=lambda s: s[0][0])
|
|
562
|
-
half_w = max(len(label) for _, label in specs) * draft.font_size * 0.62 / 2
|
|
608
|
+
half_w = max(len(label) for _, label, _ in specs) * draft.font_size * 0.62 / 2
|
|
563
609
|
min_gap = 2 * half_w + 2 * draft.pad_around_text
|
|
564
|
-
naturals = [tip[0] for tip, _ in specs]
|
|
610
|
+
naturals = [tip[0] for tip, _, _ in specs]
|
|
565
611
|
xs = _solve_strip_ys(naturals, min_gap, fx0 + half_w, fx1 - half_w) or _greedy_strip_ys(
|
|
566
612
|
naturals, min_gap, fx0 + half_w, fx1 - half_w
|
|
567
613
|
)
|
|
568
614
|
if xs is None:
|
|
569
615
|
return 0
|
|
570
|
-
for i, ((tip, label), lx) in enumerate(zip(specs, xs, strict=True)):
|
|
616
|
+
for i, ((tip, label, feat), lx) in enumerate(zip(specs, xs, strict=True)):
|
|
571
617
|
dwg.add(
|
|
572
618
|
Leader(tip=(tip[0], tip[1], 0), elbow=(lx, label_y, 0), label=label, draft=draft),
|
|
573
|
-
f"m_dia_x{i}",
|
|
619
|
+
f"m_dia_x{start + i}",
|
|
574
620
|
view="front",
|
|
621
|
+
feature=feat,
|
|
575
622
|
)
|
|
576
623
|
return len(specs)
|
|
577
624
|
|
|
578
625
|
|
|
579
|
-
def _diameter_column_left(dwg, items) -> int:
|
|
626
|
+
def _diameter_column_left(dwg, items, start: int = 0) -> int:
|
|
580
627
|
"""ø-callout column to the LEFT of the front view for Z-turned step/boss
|
|
581
628
|
diameters (#131) — the page-Y mirror of the row-below. A per-label occupancy
|
|
582
629
|
gate drops only a label that would overprint a bore leader / existing callout
|
|
@@ -585,19 +632,19 @@ def _diameter_column_left(dwg, items) -> int:
|
|
|
585
632
|
return 0
|
|
586
633
|
draft = dwg.draft
|
|
587
634
|
fx0, fy0, _, fy1 = dwg.view_bounds("front")
|
|
588
|
-
label_w = max(len(f"ø{_fmt(dia)}") for _, dia in items) * draft.font_size * 0.62
|
|
635
|
+
label_w = max(len(f"ø{_fmt(dia)}") for _, dia, _ in items) * draft.font_size * 0.62
|
|
589
636
|
elbow_x = fx0 - (draft.font_size + 2 * draft.pad_around_text)
|
|
590
637
|
if elbow_x - label_w < _MARGIN:
|
|
591
638
|
return 0
|
|
592
|
-
specs = [] # (tip_page, label), tip on the step's left silhouette
|
|
593
|
-
for anchor, dia in items:
|
|
639
|
+
specs = [] # (tip_page, label, feature), tip on the step's left silhouette
|
|
640
|
+
for anchor, dia, feat in items:
|
|
594
641
|
ax, ay, az = anchor
|
|
595
642
|
tip = dwg.at("front", ax - dia / 2, ay, az)
|
|
596
|
-
specs.append((tip, f"ø{_fmt(dia)}"))
|
|
643
|
+
specs.append((tip, f"ø{_fmt(dia)}", feat))
|
|
597
644
|
specs.sort(key=lambda s: s[0][1])
|
|
598
645
|
half_h = draft.font_size / 2 + draft.pad_around_text
|
|
599
646
|
min_gap = 2 * half_h
|
|
600
|
-
naturals = [tip[1] for tip, _ in specs]
|
|
647
|
+
naturals = [tip[1] for tip, _, _ in specs]
|
|
601
648
|
ys = _solve_strip_ys(naturals, min_gap, fy0 + half_h, fy1 - half_h) or _greedy_strip_ys(
|
|
602
649
|
naturals, min_gap, fy0 + half_h, fy1 - half_h
|
|
603
650
|
)
|
|
@@ -609,42 +656,70 @@ def _diameter_column_left(dwg, items) -> int:
|
|
|
609
656
|
# occupant class, #358). Centre lines stay crossable (a diameter dim may cross one).
|
|
610
657
|
occupied = strip_obstacles(dwg, view="front", crossable=CROSSABLE_TYPES)
|
|
611
658
|
placed = 0
|
|
612
|
-
for i, ((tip, label), ly) in enumerate(zip(specs, ys, strict=True)):
|
|
659
|
+
for i, ((tip, label, feat), ly) in enumerate(zip(specs, ys, strict=True)):
|
|
613
660
|
ldr = Leader(tip=(tip[0], tip[1], 0), elbow=(elbow_x, ly, 0), label=label, draft=draft)
|
|
614
661
|
if _box_hits(_anno_box(ldr), occupied):
|
|
615
662
|
continue # would overprint a bore leader / existing callout — drop just this one
|
|
616
|
-
dwg.add(ldr, f"m_dia_z{i}", view="front")
|
|
663
|
+
dwg.add(ldr, f"m_dia_z{start + i}", view="front", feature=feat)
|
|
617
664
|
occupied.append(_anno_box(ldr))
|
|
618
665
|
placed += 1
|
|
619
666
|
return placed
|
|
620
667
|
|
|
621
668
|
|
|
622
|
-
def render_diameters(dwg, groups, tol: float = 0.15) -> int:
|
|
669
|
+
def render_diameters(dwg, groups, tol: float = 0.15, *, only=None) -> int:
|
|
623
670
|
"""ø leaders for a turned part's external step/boss diameters, from the IR —
|
|
624
671
|
one distinct callout per diameter, in a tidy row below the front view
|
|
625
672
|
(X-turning) or a column to its left (Z-turning). Orientation is the feature
|
|
626
673
|
frame's axis, not two passes. Replaces the engine's ``_annotate_turned_diameters``
|
|
627
|
-
(ADR 0008 convergence). Diameters another annotation already covers are skipped.
|
|
674
|
+
(ADR 0008 convergence). Diameters another annotation already covers are skipped.
|
|
675
|
+
|
|
676
|
+
*only*, when given, restricts placement to step/boss features in the set — the #426
|
|
677
|
+
finalize() path passes the recorded step/boss ``callout`` intents' features. ``None``
|
|
678
|
+
(the auto-pass) places every diameter with the historical 0-based ``m_dia_{x,z}``
|
|
679
|
+
naming, byte-identically."""
|
|
628
680
|
mentioned = _mentioned_diameters(dwg)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
681
|
+
# One distinct callout per (axis, diameter). Accumulate EVERY feature that shares a
|
|
682
|
+
# diameter (insertion-ordered), so provenance (#412) can tag the callout with its
|
|
683
|
+
# single owner — or leave it unowned when two distinct features share the diameter
|
|
684
|
+
# (the #398c/#406 shared-value rule, so drop can't over-strip a sibling).
|
|
685
|
+
row_buckets: dict = {} # round(dia,2) -> [anchor, dia, {features}] (X-turned)
|
|
686
|
+
col_buckets: dict = {} # Z-turned
|
|
632
687
|
for g in groups:
|
|
633
688
|
if g.feature_kind not in ("step", "boss"):
|
|
634
689
|
continue
|
|
690
|
+
if only is not None and g.feature not in only: # #426 finalize: recorded subset
|
|
691
|
+
continue
|
|
635
692
|
dia = next((pd.param.value for pd in g.dims if pd.param.kind == "diameter"), None)
|
|
636
693
|
if dia is None or any(abs(dia - m) <= tol for m in mentioned):
|
|
637
694
|
continue
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if key in seen: # one distinct callout per diameter
|
|
695
|
+
bucket = {"x": row_buckets, "z": col_buckets}.get(g.feature.frame.axis)
|
|
696
|
+
if bucket is None:
|
|
641
697
|
continue
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
698
|
+
dkey = round(dia, 2)
|
|
699
|
+
bucket.setdefault(dkey, [g.anchor, dia, set()])[2].add(g.feature)
|
|
700
|
+
|
|
701
|
+
def _items(buckets):
|
|
702
|
+
return [(a, d, next(iter(fs)) if len(fs) == 1 else None) for a, d, fs in buckets.values()]
|
|
703
|
+
|
|
704
|
+
# The placers name leaders m_dia_{x,z}{start+i} CONTIGUOUSLY from one start. The auto-pass
|
|
705
|
+
# (only None) uses start=0 — byte-identical. The finalize path (only set) may run after
|
|
706
|
+
# existing m_dia names (a prior batch), so it starts past the MAX existing index — NOT the
|
|
707
|
+
# first-free (which is unsound for a multi-item run when the names are non-contiguous, e.g.
|
|
708
|
+
# after drop(): a gap below an occupied index would let the run wrap onto it and silently
|
|
709
|
+
# overwrite an earlier leader — #432 review). Starting past the max keeps the whole run free.
|
|
710
|
+
def _next_start(prefix):
|
|
711
|
+
idxs = [
|
|
712
|
+
int(n[len(prefix) :])
|
|
713
|
+
for n in dwg._named
|
|
714
|
+
if n.startswith(prefix) and n[len(prefix) :].isdigit()
|
|
715
|
+
]
|
|
716
|
+
return max(idxs) + 1 if idxs else 0
|
|
717
|
+
|
|
718
|
+
start_x = _next_start("m_dia_x") if only is not None else 0
|
|
719
|
+
start_z = _next_start("m_dia_z") if only is not None else 0
|
|
720
|
+
return _diameter_row_below(dwg, _items(row_buckets), start=start_x) + _diameter_column_left(
|
|
721
|
+
dwg, _items(col_buckets), start=start_z
|
|
722
|
+
)
|
|
648
723
|
|
|
649
724
|
|
|
650
725
|
def _env_pd(group, role):
|
|
@@ -769,7 +844,9 @@ def _record_step_chain_drop(dwg, why: str) -> None:
|
|
|
769
844
|
)
|
|
770
845
|
|
|
771
846
|
|
|
772
|
-
def _draw_step_chain(
|
|
847
|
+
def _draw_step_chain(
|
|
848
|
+
dwg, view, segs, name_prefix, detail_scale=None, allow_collapse=True, *, start=0
|
|
849
|
+
) -> int:
|
|
773
850
|
"""Place a turned step-length chain in *view* from *segs* — each ``(pa, pb,
|
|
774
851
|
value)`` already projected to *view*'s page coords, in axis order. Orientation is
|
|
775
852
|
data (the projected span direction): horizontal → chain above the view, vertical
|
|
@@ -800,7 +877,8 @@ def _draw_step_chain(dwg, view, segs, name_prefix, detail_scale=None, allow_coll
|
|
|
800
877
|
dim = _dim((min(xs), y1, 0), (max(xs), y1, 0), "above", gap, draft, label=label)
|
|
801
878
|
else:
|
|
802
879
|
dim = _dim((x1, min(ys), 0), (x1, max(ys), 0), "right", gap, draft, label=label)
|
|
803
|
-
|
|
880
|
+
typ_name = f"{name_prefix}_typ" if start == 0 else f"{name_prefix}_typ{start}"
|
|
881
|
+
candidates = [(typ_name, dim)]
|
|
804
882
|
else:
|
|
805
883
|
tier_step = draft.font_size + 2 * draft.pad_around_text
|
|
806
884
|
tiers = [0] * len(segs)
|
|
@@ -841,7 +919,7 @@ def _draw_step_chain(dwg, view, segs, name_prefix, detail_scale=None, allow_coll
|
|
|
841
919
|
p1, p2, side = (x1, pa[1], 0), (x1, pb[1], 0), "right"
|
|
842
920
|
dist = gap
|
|
843
921
|
candidates.append(
|
|
844
|
-
(f"{name_prefix}{i}", _dim(p1, p2, side, dist, draft, label=_fmt(value)))
|
|
922
|
+
(f"{name_prefix}{start + i}", _dim(p1, p2, side, dist, draft, label=_fmt(value)))
|
|
845
923
|
)
|
|
846
924
|
|
|
847
925
|
# Room guard: if any dim would fall off the drawable page, place NONE.
|
|
@@ -860,7 +938,24 @@ def _draw_step_chain(dwg, view, segs, name_prefix, detail_scale=None, allow_coll
|
|
|
860
938
|
return len(candidates)
|
|
861
939
|
|
|
862
940
|
|
|
863
|
-
def
|
|
941
|
+
def _next_steplen_start(dwg, prefix: str = "m_steplen") -> int:
|
|
942
|
+
"""First free m_steplen index past the MAX existing one — the #426 finalize path names
|
|
943
|
+
the chain as a contiguous run from one start, so it must clear every existing name (max+1,
|
|
944
|
+
not first-free: a gap below an occupied index would let the run wrap onto it, #432)."""
|
|
945
|
+
idxs: list[int] = []
|
|
946
|
+
for n in dwg._named:
|
|
947
|
+
if not n.startswith(prefix):
|
|
948
|
+
continue
|
|
949
|
+
rest = n[len(prefix) :]
|
|
950
|
+
if rest.isdigit():
|
|
951
|
+
idxs.append(int(rest))
|
|
952
|
+
elif rest.startswith("_typ"): # the N× collapse name m_steplen_typ{start}
|
|
953
|
+
tail = rest[4:]
|
|
954
|
+
idxs.append(int(tail) if tail.isdigit() else 0)
|
|
955
|
+
return max(idxs) + 1 if idxs else 0
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def render_step_lengths(dwg, groups, *, only=None) -> int:
|
|
864
959
|
"""Unified turned step-length chain (ADR 0008 #223): each `StepFeature`'s length
|
|
865
960
|
span projects into the front view and joins the chain that tiles the turning axis
|
|
866
961
|
so every shoulder is located. X-turned → horizontal chain above the view;
|
|
@@ -877,6 +972,8 @@ def render_step_lengths(dwg, groups) -> int:
|
|
|
877
972
|
for g in groups:
|
|
878
973
|
if g.feature_kind != "step":
|
|
879
974
|
continue
|
|
975
|
+
if only is not None and g.feature not in only: # #426 finalize: recorded subset
|
|
976
|
+
continue
|
|
880
977
|
length = next(
|
|
881
978
|
(pd.param for pd in g.dims if pd.param.kind == "length" and pd.param.span is not None),
|
|
882
979
|
None,
|
|
@@ -887,6 +984,9 @@ def render_step_lengths(dwg, groups) -> int:
|
|
|
887
984
|
if not rows:
|
|
888
985
|
return 0
|
|
889
986
|
draft = dwg.draft
|
|
987
|
+
# only=None (auto-pass) → start=0, historical m_steplen naming, byte-identical. The
|
|
988
|
+
# finalize path (only set) starts past existing m_steplen names (#426 naming seam).
|
|
989
|
+
start = _next_steplen_start(dwg) if only is not None else 0
|
|
890
990
|
fsegs = [(dwg.at("front", *a), dwg.at("front", *b), v) for a, b, v in rows]
|
|
891
991
|
horizontal = abs(fsegs[0][1][0] - fsegs[0][0][0]) >= abs(fsegs[0][1][1] - fsegs[0][0][1])
|
|
892
992
|
|
|
@@ -936,9 +1036,11 @@ def render_step_lengths(dwg, groups) -> int:
|
|
|
936
1036
|
main.sort(key=lambda s: s[0][0])
|
|
937
1037
|
# The chain now mixes head-block(s) with real steps — never collapse it to a
|
|
938
1038
|
# uniform "N× v" representative (a block is not a repeated step, #307 review).
|
|
939
|
-
return _draw_step_chain(
|
|
1039
|
+
return _draw_step_chain(
|
|
1040
|
+
dwg, "front", main, "m_steplen", allow_collapse=False, start=start
|
|
1041
|
+
)
|
|
940
1042
|
|
|
941
|
-
return _draw_step_chain(dwg, "front", fsegs, "m_steplen")
|
|
1043
|
+
return _draw_step_chain(dwg, "front", fsegs, "m_steplen", start=start)
|
|
942
1044
|
|
|
943
1045
|
|
|
944
1046
|
def _detect_step_repeat(step_zs, bb_min_z, bb_max_z, tol_frac=0.10):
|