draftwright 0.2.1__tar.gz → 0.2.3__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.
Files changed (87) hide show
  1. {draftwright-0.2.1 → draftwright-0.2.3}/CHANGELOG.md +72 -0
  2. {draftwright-0.2.1 → draftwright-0.2.3}/PKG-INFO +1 -1
  3. {draftwright-0.2.1 → draftwright-0.2.3}/pyproject.toml +1 -1
  4. draftwright-0.2.3/src/draftwright/__init__.py +103 -0
  5. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/_core.py +7 -50
  6. draftwright-0.2.3/src/draftwright/annotations/_common.py +401 -0
  7. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/annotations/from_model.py +292 -109
  8. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/annotations/holes.py +436 -205
  9. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/annotations/orchestrator.py +172 -101
  10. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/annotations/sections.py +121 -24
  11. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/cli.py +6 -2
  12. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/drawing.py +58 -31
  13. draftwright-0.2.3/src/draftwright/layout.py +676 -0
  14. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/linting/coverage.py +15 -0
  15. draftwright-0.2.3/tests/layout_snapshots/box.json +116 -0
  16. draftwright-0.2.3/tests/layout_snapshots/bracket.json +388 -0
  17. draftwright-0.2.3/tests/layout_snapshots/drive_screw_x.json +155 -0
  18. draftwright-0.2.3/tests/layout_snapshots/dshape.json +201 -0
  19. draftwright-0.2.3/tests/layout_snapshots/flange.json +287 -0
  20. draftwright-0.2.3/tests/layout_snapshots/plate_holes.json +258 -0
  21. draftwright-0.2.3/tests/layout_snapshots/side_drilled.json +183 -0
  22. draftwright-0.2.3/tests/layout_snapshots/slotted.json +170 -0
  23. draftwright-0.2.3/tests/layout_snapshots/turned_shaft.json +196 -0
  24. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_layout.py +176 -4
  25. draftwright-0.2.3/tests/test_layout_cleanliness.py +179 -0
  26. draftwright-0.2.3/tests/test_layout_property.py +201 -0
  27. draftwright-0.2.3/tests/test_layout_snapshot.py +213 -0
  28. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_make_drawing.py +283 -106
  29. draftwright-0.2.3/tests/test_render_seam.py +200 -0
  30. draftwright-0.2.3/tests/test_strip_layout.py +673 -0
  31. draftwright-0.2.1/src/draftwright/__init__.py +0 -62
  32. draftwright-0.2.1/src/draftwright/annotations/_common.py +0 -51
  33. draftwright-0.2.1/src/draftwright/layout.py +0 -317
  34. draftwright-0.2.1/tests/test_render_seam.py +0 -63
  35. {draftwright-0.2.1 → draftwright-0.2.3}/.gitignore +0 -0
  36. {draftwright-0.2.1 → draftwright-0.2.3}/LICENSE +0 -0
  37. {draftwright-0.2.1 → draftwright-0.2.3}/README.md +0 -0
  38. {draftwright-0.2.1 → draftwright-0.2.3}/skills/SKILL.md +0 -0
  39. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/analysis.py +0 -0
  40. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/annotate.py +0 -0
  41. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/annotations/__init__.py +0 -0
  42. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/builder.py +0 -0
  43. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/export.py +0 -0
  44. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/features.py +0 -0
  45. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  46. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  47. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  48. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  49. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/fonts/__init__.py +0 -0
  50. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/linting/__init__.py +0 -0
  51. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/linting/issues.py +0 -0
  52. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/linting/structural.py +0 -0
  53. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/linting/suggest.py +0 -0
  54. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/make_drawing.py +0 -0
  55. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/model/__init__.py +0 -0
  56. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/model/detect.py +0 -0
  57. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/model/ir.py +0 -0
  58. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/model/planner.py +0 -0
  59. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/pmi.py +0 -0
  60. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/projection.py +0 -0
  61. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/recognition/__init__.py +0 -0
  62. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/recognition/_features.py +0 -0
  63. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/recognition/levels.py +0 -0
  64. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/recognition/slots.py +0 -0
  65. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/recognition/turned.py +0 -0
  66. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/registry.py +0 -0
  67. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/repair.py +0 -0
  68. {draftwright-0.2.1 → draftwright-0.2.3}/src/draftwright/sheet.py +0 -0
  69. {draftwright-0.2.1 → draftwright-0.2.3}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  70. {draftwright-0.2.1 → draftwright-0.2.3}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  71. {draftwright-0.2.1 → draftwright-0.2.3}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  72. {draftwright-0.2.1 → draftwright-0.2.3}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  73. {draftwright-0.2.1 → draftwright-0.2.3}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  74. {draftwright-0.2.1 → draftwright-0.2.3}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  75. {draftwright-0.2.1 → draftwright-0.2.3}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  76. {draftwright-0.2.1 → draftwright-0.2.3}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  77. {draftwright-0.2.1 → draftwright-0.2.3}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  78. {draftwright-0.2.1 → draftwright-0.2.3}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  79. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_e2e_slice.py +0 -0
  80. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_e2e_standards.py +0 -0
  81. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_lint_structural.py +0 -0
  82. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_linting.py +0 -0
  83. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_part_model.py +0 -0
  84. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_pmi.py +0 -0
  85. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_recognition.py +0 -0
  86. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_registry.py +0 -0
  87. {draftwright-0.2.1 → draftwright-0.2.3}/tests/test_turned_steps.py +0 -0
@@ -2,6 +2,78 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.2.3 — 2026-07-03
6
+
7
+ A large patch release: the **annotation-placement engine was rebuilt** as a
8
+ collect-then-solve *boundary-labeling* stage (ADR 0009). Placement is now
9
+ deterministic and minimises total leader length, and the recurring class of
10
+ overlaps where a label was drawn on top of an "invisible" occupant — a leader
11
+ shaft, a witness/extension line, the section hatch — is removed by construction.
12
+ Drawing output changes for many turned, cross-drilled, and multi-feature parts.
13
+
14
+ ### Changed
15
+
16
+ - **Every annotation in a view's margin is now placed by one collect-then-solve
17
+ pass** (ADR 0009, #317–#323). Dimensions, hole callouts, turned-diameter
18
+ leaders, and the section hatch share a single occupancy model instead of several
19
+ independent passes each blind to the others. When a strip is over capacity it now
20
+ drops the *lowest-priority* annotation (smallest bore first) rather than whichever
21
+ pass happened to run last. The legacy strip cursor is retired.
22
+ - **Leader placement minimises total leader length, deterministically** (P4,
23
+ #318). A per-strip solve places each label at the shortest-leader position that
24
+ keeps the labels in order and clear of keep-out rows (a view centre-line, a
25
+ dimension's extension line); central/coaxial callouts are anchored to the
26
+ view-centre row. Output is reproducible across platforms and Python versions.
27
+ - **`scipy` is no longer a dependency** — the leader solve is a small deterministic
28
+ algorithm (weighted-median isotonic regression), not a linear program.
29
+ - **Output changes** for turned, cross-drilled, and multi-feature parts whose
30
+ margin annotations are now positioned by the unified solver.
31
+
32
+ ### Fixed
33
+
34
+ - **A PMI bore-diameter dimension spans the bore radius, not the full diameter**
35
+ (#360). A `pmi="annotate"` diameter callout drew its witness lines at ±diameter
36
+ from the centre — twice too wide, missing the bore edges.
37
+ - **A bore coaxial with a rotational part's turning axis is no longer
38
+ over-dimensioned** (#309). It carried a redundant offset *and* height location
39
+ dimension even though its centre mark already locates it.
40
+ - **A dropped turned step-length chain is no longer silent** (#362). When a turned
41
+ head's shoulders are too crowded to dimension, the drop is now reported
42
+ (`step_dim_dropped`) instead of vanishing with no lint or on-sheet signal.
43
+ - **A diameter callout can no longer overprint a bore callout's leader shaft**
44
+ (#358). The turned-diameter column now avoids the *full* footprint of existing
45
+ annotations, not just their text boxes.
46
+ - **The balloon ring hugs its dimensions on a cramped sheet** (#349) — its band
47
+ depth is clamped to the drawable area.
48
+ - **Dimension detection is robust to `SafeDimension`** (#335/#349) — the corridor
49
+ and balloon-ring filters test the dimension *type*, not a class-name string, so a
50
+ future dimension subclass can't slip through.
51
+
52
+ ## v0.2.2 — 2026-06-30
53
+
54
+ A patch release of turned-part dimension-placement fixes and a CLI start-up
55
+ speed-up. Drawing output changes for the affected turned/cross-drilled parts.
56
+
57
+ ### Fixed
58
+
59
+ - **A coaxial bore callout on a *stepped* turned shaft is now lifted off the round
60
+ view's centre axis** (#305). The earlier fix only triggered for a uniform
61
+ (`is_rotational`) cylinder; a stepped shaft (e.g. the gramel GRM-03 drive screw)
62
+ has a turned step profile but isn't classified rotational, so its `⌀… ↓…` bore
63
+ callout was still leadered straight along the centreline, with the centre mark
64
+ running through the text. The lift now also fires for a turned-profile part.
65
+ - **A side-drilled hole's location dimension now stacks *inside* the overall
66
+ envelope dimension** (ISO order — overall dim outermost, feature/location dims
67
+ nearer the view). It was placed *outside* the envelope, which forced the shorter
68
+ location dim's arrowheads to flip outward and clash (seen on GRM-01 and GRM-02).
69
+ The mandatory overall dimension is still guaranteed placement.
70
+
71
+ ### Changed
72
+
73
+ - **CLI shell completion and `--help` are fast again** (#313). The Typer CLI and
74
+ the heavy CAD engine are now imported lazily, so tab-completion and `--help` no
75
+ longer pay a ~6 s engine-import cost; a real drawing run is unaffected.
76
+
5
77
  ## v0.2.1 — 2026-06-30
6
78
 
7
79
  A patch release focused on **turned-part dimensioning legibility**: crowded
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Automated technical-drawing generation for build123d
5
5
  Project-URL: Homepage, https://github.com/pzfreo/draftwright
6
6
  Project-URL: Repository, https://github.com/pzfreo/draftwright
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -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
+ ]
@@ -14,7 +14,7 @@ import functools
14
14
  import logging
15
15
  import re
16
16
  from collections.abc import Callable
17
- from dataclasses import dataclass, field
17
+ from dataclasses import dataclass
18
18
  from pathlib import Path
19
19
  from types import SimpleNamespace
20
20
  from typing import TYPE_CHECKING
@@ -166,16 +166,17 @@ def _dim(p1, p2, side, distance, draft, **kwargs):
166
166
  class Strip:
167
167
  """A one-dimensional annotation band adjacent to an orthographic view.
168
168
 
169
- Annotations are stacked outward from the view edge by calling
170
- :meth:`allocate`. The cursor starts at ``anchor + direction * gap`` and
171
- advances after each successful allocation.
169
+ A plain geometry record: the collect-then-solve placers (ADR 0009) read its
170
+ bounds (:func:`~draftwright.annotations._common.strip_free_span`) and carve
171
+ around the placed annotations. The mutable ``allocate``/``peek`` cursor was
172
+ retired once every placer moved to the carve (#150).
172
173
 
173
174
  Attributes:
174
175
  anchor: Page coordinate of the view edge this strip starts from.
175
176
  outer_limit: Page coordinate at which the strip ends (page margin,
176
177
  neighbouring view, or title-block boundary).
177
- direction: ``+1`` — cursor moves away from anchor (right/above);
178
- ``-1`` — cursor retreats from anchor (left/below).
178
+ direction: ``+1`` — stacks away from anchor (right/above);
179
+ ``-1`` — stacks back toward smaller coords (left/below).
179
180
  gap: Clearance between the view edge and the first annotation.
180
181
  spacing: Clearance between successive annotations.
181
182
  """
@@ -185,56 +186,12 @@ class Strip:
185
186
  direction: float = 1.0
186
187
  gap: float = 8.0
187
188
  spacing: float = 4.0
188
- _cursor: float = field(init=False, compare=False, repr=False)
189
-
190
- def __post_init__(self):
191
- self._cursor = self.anchor + self.direction * self.gap
192
-
193
- # ------------------------------------------------------------------
194
- # Public API
195
189
 
196
190
  @property
197
191
  def available(self) -> float:
198
192
  """Total space available in this strip (mm)."""
199
193
  return abs(self.outer_limit - self.anchor)
200
194
 
201
- @property
202
- def depth_used(self) -> float:
203
- """How far the cursor has advanced from the anchor (mm)."""
204
- return abs(self._cursor - self.anchor)
205
-
206
- def peek(self, size: float) -> float | None:
207
- """Return what ``allocate(size)`` would return without advancing the cursor."""
208
- if self.direction == 1:
209
- start = self._cursor
210
- return start if (start + size) <= self.outer_limit else None
211
- else:
212
- end = self._cursor
213
- return end if (end - size) >= self.outer_limit else None
214
-
215
- def allocate(self, size: float) -> float | None:
216
- """Reserve *size* mm; return the near-edge page coordinate, or ``None`` if full.
217
-
218
- The returned value is the page coordinate of the annotation's
219
- dimension line (or leader elbow). Convert to a relative offset with::
220
-
221
- distance = abs(page_coord - strip.anchor)
222
- """
223
- if self.direction == 1:
224
- start = self._cursor
225
- end = start + size
226
- if end > self.outer_limit:
227
- return None
228
- self._cursor = end + self.spacing
229
- return start
230
- else:
231
- end = self._cursor
232
- start = end - size
233
- if start < self.outer_limit:
234
- return None
235
- self._cursor = start - self.spacing
236
- return end
237
-
238
195
 
239
196
  @dataclass
240
197
  class ViewZones: