draftwright 0.1.4__tar.gz → 0.1.6__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 (23) hide show
  1. {draftwright-0.1.4 → draftwright-0.1.6}/CHANGELOG.md +21 -0
  2. {draftwright-0.1.4 → draftwright-0.1.6}/PKG-INFO +1 -1
  3. {draftwright-0.1.4 → draftwright-0.1.6}/pyproject.toml +6 -2
  4. {draftwright-0.1.4 → draftwright-0.1.6}/src/draftwright/make_drawing.py +91 -1
  5. draftwright-0.1.6/tests/fixtures/nist_ctc_01_asme1_ap203.stp +5506 -0
  6. draftwright-0.1.6/tests/fixtures/nist_ctc_02_asme1_ap203.stp +24369 -0
  7. draftwright-0.1.6/tests/fixtures/nist_ctc_02_asme1_ap242.stp +22900 -0
  8. draftwright-0.1.6/tests/fixtures/nist_ctc_03_asme1_ap203.stp +5833 -0
  9. draftwright-0.1.6/tests/fixtures/nist_ctc_03_asme1_ap242.stp +6325 -0
  10. draftwright-0.1.6/tests/fixtures/nist_ctc_04_asme1_ap203.stp +14677 -0
  11. draftwright-0.1.6/tests/fixtures/nist_ctc_04_asme1_ap242.stp +21445 -0
  12. draftwright-0.1.6/tests/fixtures/nist_ctc_05_asme1_ap203.stp +7005 -0
  13. draftwright-0.1.6/tests/fixtures/nist_ctc_05_asme1_ap242.stp +14204 -0
  14. {draftwright-0.1.4 → draftwright-0.1.6}/tests/test_e2e_standards.py +45 -0
  15. {draftwright-0.1.4 → draftwright-0.1.6}/tests/test_make_drawing.py +46 -0
  16. {draftwright-0.1.4 → draftwright-0.1.6}/.gitignore +0 -0
  17. {draftwright-0.1.4 → draftwright-0.1.6}/LICENSE +0 -0
  18. {draftwright-0.1.4 → draftwright-0.1.6}/README.md +0 -0
  19. {draftwright-0.1.4 → draftwright-0.1.6}/skills/SKILL.md +0 -0
  20. {draftwright-0.1.4 → draftwright-0.1.6}/src/draftwright/__init__.py +0 -0
  21. {draftwright-0.1.4 → draftwright-0.1.6}/src/draftwright/pmi.py +0 -0
  22. {draftwright-0.1.4 → draftwright-0.1.6}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  23. {draftwright-0.1.4 → draftwright-0.1.6}/tests/test_pmi.py +0 -0
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.1.5 — 2026-06-15
6
+
7
+ ### Fixed
8
+
9
+ - CTC-02 spurious full-page line: build123d's `ExportSVG` projected
10
+ circle-edge-on edges (hole/fillet rims seen edge-on) as elliptical arcs with
11
+ a near-zero minor radius, which renderers blow up into full-page lines.
12
+ `sanitize_svg_arcs()` rewrites any arc with a sub-1e-3 mm radius into the
13
+ straight line it actually is, leaving real arcs untouched (#19). Not a PMI
14
+ issue — the file is AP203 geometry-only.
15
+
16
+ ### Tests
17
+
18
+ - Added the full NIST CTC set (01–05) as fixtures, both AP203 geometry-only and
19
+ AP242 (with-PMI) variants.
20
+ - Heavy end-to-end CTC fixture builds are marked `slow` and deselected from the
21
+ default `pytest` run (fast normal run, ~4.5 min); CI runs the fast tier across
22
+ the OS/Python matrix and the slow tier once.
23
+ - Known: AP242 CTC-02 and both CTC-04 variants crash OCCT on import (#20); their
24
+ fixtures are excluded from build tests.
25
+
5
26
  ## v0.1.4 — 2026-06-15
6
27
 
7
28
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.1.4
3
+ Version: 0.1.6
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.1.4"
7
+ version = "0.1.6"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -57,7 +57,11 @@ include = [
57
57
  [tool.pytest.ini_options]
58
58
  testpaths = ["tests"]
59
59
  timeout = 300
60
- addopts = "--tb=short --cov=src/draftwright --cov-report=term-missing --cov-report=xml"
60
+ # `slow` covers the heavy end-to-end builds over the large NIST CTC fixtures
61
+ # (minutes each). They are deselected by default for a fast normal run; run the
62
+ # full suite with `-m ""` or just the slow tier with `-m slow` (CI runs both).
63
+ markers = ["slow: heavy end-to-end CTC fixture builds (deselected by default)"]
64
+ addopts = "--tb=short --cov=src/draftwright --cov-report=term-missing --cov-report=xml -m 'not slow'"
61
65
 
62
66
  [tool.coverage.run]
63
67
  source = ["src/draftwright"]
@@ -71,7 +71,9 @@ from build123d_drafting.helpers import (
71
71
  view_axes,
72
72
  )
73
73
  from OCP.BRepAdaptor import BRepAdaptor_Surface
74
+ from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut
74
75
  from OCP.GeomAbs import GeomAbs_Plane
76
+ from OCP.TopTools import TopTools_ListOfShape
75
77
 
76
78
  _log = logging.getLogger(__name__)
77
79
 
@@ -118,6 +120,53 @@ def fix_svg_page_size(svg_path: str, page_w: float, page_h: float) -> None:
118
120
  Path(svg_path).write_text(data, encoding="utf-8")
119
121
 
120
122
 
123
+ # Below this, an elliptical-arc radius (page-mm) is treated as degenerate.
124
+ # Real feature arcs are orders of magnitude larger; the bad ones are ~1e-7.
125
+ _MIN_ARC_RADIUS = 1e-3
126
+
127
+ _SVG_NUM = r"(-?\d+\.?\d*(?:[eE][-+]?\d+)?)"
128
+ _SVG_ARC_RE = re.compile(
129
+ r"A\s*"
130
+ + _SVG_NUM
131
+ + r"[ ,]+"
132
+ + _SVG_NUM
133
+ + r"[ ,]+"
134
+ + _SVG_NUM
135
+ + r"[ ,]+([01])[ ,]*([01])[ ,]+"
136
+ + _SVG_NUM
137
+ + r"[ ,]+"
138
+ + _SVG_NUM
139
+ )
140
+
141
+
142
+ def sanitize_svg_arcs(svg_path: str) -> int:
143
+ """Rewrite near-degenerate elliptical arcs as straight line segments.
144
+
145
+ build123d's ``ExportSVG`` projects a circular edge seen edge-on (a hole or
146
+ fillet rim whose plane is parallel to the view direction) as an elliptical
147
+ arc with a vanishing minor radius (``ry`` ≈ 1e-7). The SVG spec says a
148
+ zero-radius arc is a straight line, but because the radius is not *exactly*
149
+ zero, renderers (librsvg, cairosvg) treat it as a hugely eccentric ellipse
150
+ and draw a spurious full-page line. Each such arc (``A rx ry rot lf sf x
151
+ y``) with ``rx`` or ``ry`` below :data:`_MIN_ARC_RADIUS` is replaced by
152
+ ``L x y`` — its true geometry. Returns the number of arcs rewritten.
153
+ """
154
+ data = Path(svg_path).read_text(encoding="utf-8")
155
+ n = 0
156
+
157
+ def _repl(m):
158
+ nonlocal n
159
+ if abs(float(m.group(1))) < _MIN_ARC_RADIUS or abs(float(m.group(2))) < _MIN_ARC_RADIUS:
160
+ n += 1
161
+ return f"L {m.group(6)} {m.group(7)}"
162
+ return m.group(0)
163
+
164
+ fixed = _SVG_ARC_RE.sub(_repl, data)
165
+ if n:
166
+ Path(svg_path).write_text(fixed, encoding="utf-8")
167
+ return n
168
+
169
+
121
170
  # ---------------------------------------------------------------------------
122
171
  # Geometry analysis
123
172
  # ---------------------------------------------------------------------------
@@ -1305,6 +1354,9 @@ class Drawing:
1305
1354
  svg_path = out + ".svg"
1306
1355
  svg.write(svg_path)
1307
1356
  fix_svg_page_size(svg_path, self.page_w, self.page_h)
1357
+ n_arcs = sanitize_svg_arcs(svg_path)
1358
+ if n_arcs:
1359
+ _log.info("Rewrote %d degenerate (near-zero-radius) arc(s) as line segments", n_arcs)
1308
1360
  _log.info("SVG → %s", svg_path)
1309
1361
 
1310
1362
  dxf = ExportDXF()
@@ -2261,6 +2313,39 @@ def _section_hatch_edges(face, SX, SZ, spacing):
2261
2313
  return result
2262
2314
 
2263
2315
 
2316
+ def _fuzzy_cut(body, cutter, fuzzy: float = 1e-3):
2317
+ """Boolean-subtract *cutter* from *body* with a fuzzy tolerance.
2318
+
2319
+ Returns a build123d ``Solid`` (or ``Compound`` of solids), or ``None`` if the
2320
+ boolean fails or yields no solid.
2321
+
2322
+ build123d's plain ``body - cutter`` runs an exact (zero-fuzzy) boolean which
2323
+ raises an *uncatchable* ``Standard_DomainError`` on some cast geometry — the
2324
+ C++ exception escapes to ``libc++abi: terminating`` (SIGABRT) and a
2325
+ surrounding ``try/except`` never sees it, killing the whole drawing (NIST
2326
+ CTC-04 section cut, #20). A small fuzzy value makes ``BRepAlgoAPI_Cut`` robust
2327
+ on the same input. We drive the OCCT op directly (build123d's
2328
+ ``Shape._bool_op`` result-processing aborts on the resulting compound) and
2329
+ keep only the solids, so non-solid boolean artefacts can't crash the
2330
+ downstream hidden-line projection.
2331
+ """
2332
+ args = TopTools_ListOfShape()
2333
+ args.Append(body.wrapped)
2334
+ tools = TopTools_ListOfShape()
2335
+ tools.Append(cutter.wrapped)
2336
+ op = BRepAlgoAPI_Cut()
2337
+ op.SetArguments(args)
2338
+ op.SetTools(tools)
2339
+ op.SetFuzzyValue(fuzzy)
2340
+ op.Build()
2341
+ if not op.IsDone():
2342
+ return None
2343
+ solids = Compound(op.Shape()).solids()
2344
+ if not solids:
2345
+ return None
2346
+ return solids[0] if len(solids) == 1 else Compound(children=list(solids))
2347
+
2348
+
2264
2349
  def _add_section_view(dwg, a, axis_letter, holes=None):
2265
2350
  """Full section A–A when blind or stepped holes hide their structure (#94).
2266
2351
 
@@ -2330,10 +2415,15 @@ def _add_section_view(dwg, a, axis_letter, holes=None):
2330
2415
  return
2331
2416
  body = solids[0] if len(solids) == 1 else Compound(children=list(solids))
2332
2417
  try:
2333
- keep_behind = body - Pos(a.cx, y_star - big / 2, a.cz) * Box(big, big, big)
2418
+ # Fuzzy boolean: the exact `body - Box(...)` aborts uncatchably
2419
+ # (Standard_DomainError) on some cast geometry — see _fuzzy_cut / #20.
2420
+ keep_behind = _fuzzy_cut(body, Pos(a.cx, y_star - big / 2, a.cz) * Box(big, big, big))
2334
2421
  except Exception as exc: # noqa: BLE001 — OCC booleans raise broadly
2335
2422
  _log.warning("Section A–A skipped (cut failed: %s)", exc)
2336
2423
  return
2424
+ if keep_behind is None:
2425
+ _log.warning("Section A–A skipped (boolean cut produced no solid)")
2426
+ return
2337
2427
  camera = (dwg.look_at[0], dwg.look_at[1] - dwg.dist, dwg.look_at[2])
2338
2428
  dwg.add_view("section_aa", keep_behind, camera, (0, 0, 1), (pos_x, a.FV_Y))
2339
2429
  dwg.add(