draftwright 0.1.12__tar.gz → 0.1.13__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.1.12 → draftwright-0.1.13}/CHANGELOG.md +28 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/PKG-INFO +3 -4
- {draftwright-0.1.12 → draftwright-0.1.13}/README.md +1 -2
- {draftwright-0.1.12 → draftwright-0.1.13}/pyproject.toml +22 -6
- {draftwright-0.1.12 → draftwright-0.1.13}/src/draftwright/__init__.py +2 -3
- {draftwright-0.1.12 → draftwright-0.1.13}/src/draftwright/_core.py +166 -15
- draftwright-0.1.13/src/draftwright/analysis.py +479 -0
- draftwright-0.1.13/src/draftwright/annotate.py +13 -0
- draftwright-0.1.13/src/draftwright/annotations/__init__.py +6 -0
- draftwright-0.1.13/src/draftwright/annotations/_common.py +51 -0
- draftwright-0.1.13/src/draftwright/annotations/holes.py +1063 -0
- draftwright-0.1.13/src/draftwright/annotations/orchestrator.py +551 -0
- draftwright-0.1.13/src/draftwright/annotations/pmi.py +371 -0
- draftwright-0.1.13/src/draftwright/annotations/sections.py +466 -0
- draftwright-0.1.13/src/draftwright/annotations/turned.py +196 -0
- draftwright-0.1.13/src/draftwright/builder.py +705 -0
- draftwright-0.1.13/src/draftwright/drawing.py +1122 -0
- draftwright-0.1.13/src/draftwright/export.py +257 -0
- draftwright-0.1.13/src/draftwright/features.py +318 -0
- draftwright-0.1.13/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- draftwright-0.1.13/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- draftwright-0.1.13/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +93 -0
- draftwright-0.1.13/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +93 -0
- draftwright-0.1.13/src/draftwright/fonts/__init__.py +20 -0
- draftwright-0.1.13/src/draftwright/linting.py +268 -0
- draftwright-0.1.13/src/draftwright/make_drawing.py +17 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/src/draftwright/pmi.py +3 -2
- draftwright-0.1.13/src/draftwright/projection.py +291 -0
- draftwright-0.1.13/src/draftwright/registry.py +126 -0
- draftwright-0.1.13/src/draftwright/repair.py +111 -0
- draftwright-0.1.13/src/draftwright/sheet.py +893 -0
- draftwright-0.1.13/tests/golden/ctc01.json +335 -0
- draftwright-0.1.13/tests/golden/cylinder.json +176 -0
- draftwright-0.1.13/tests/golden/plate.json +156 -0
- draftwright-0.1.13/tests/golden/stepped.json +148 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/test_e2e_standards.py +2 -1
- draftwright-0.1.13/tests/test_golden.py +309 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/test_layout.py +6 -2
- draftwright-0.1.13/tests/test_linting.py +36 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/test_make_drawing.py +357 -153
- draftwright-0.1.13/tests/test_registry.py +105 -0
- draftwright-0.1.12/src/draftwright/annotate.py +0 -2193
- draftwright-0.1.12/src/draftwright/make_drawing.py +0 -3907
- {draftwright-0.1.12 → draftwright-0.1.13}/.gitignore +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/LICENSE +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/skills/SKILL.md +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/src/draftwright/layout.py +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.1.12 → draftwright-0.1.13}/tests/test_pmi.py +0 -0
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.1.13 — 2026-06-27
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Requires `build123d-drafting-helpers>=0.13.0`; text pinned to bundled fonts**
|
|
10
|
+
(#149, ADR 0006). draftwright now vendors IBM Plex (OFL-1.1) and renders and
|
|
11
|
+
measures all text via `font_path` — IBM Plex Mono for dimensions/callouts/notes,
|
|
12
|
+
IBM Plex Sans Condensed for the title block — instead of resolving the system
|
|
13
|
+
font name `"Arial"`. Resolving a name substitutes a different font on Linux,
|
|
14
|
+
which shifted the whole sheet ~1 mm; pinning a bundled font file makes generated
|
|
15
|
+
layout **deterministic across Linux/macOS/Windows** and gives a consistent
|
|
16
|
+
typeface. **Drawing output changes**: positions shift slightly from prior
|
|
17
|
+
releases and labels render in IBM Plex (helpers #172).
|
|
18
|
+
|
|
19
|
+
### Internal
|
|
20
|
+
|
|
21
|
+
- **Compiler-pipeline module split** (#138, ADR 0005). The two large modules
|
|
22
|
+
`make_drawing.py` (3,907 lines) and `annotate.py` (2,587) were decomposed into a
|
|
23
|
+
DAG of focused stage modules — `projection`, `sheet`, `analysis`, `drawing`,
|
|
24
|
+
`builder`, the `annotations/` subpackage (sections/turned/pmi/holes/orchestrator),
|
|
25
|
+
alongside the existing `registry`/`linting`/`repair`/`export`/`fonts`. Annotation
|
|
26
|
+
identity, the lint coverage signal, and the deterministic repair loop each gained a
|
|
27
|
+
single owner; `make_drawing.py` / `annotate.py` are now thin compat facades, so all
|
|
28
|
+
existing imports and the `draftwright` CLI entry point keep working. A golden-output
|
|
29
|
+
regression gate verified every step is behaviour-preserving (output byte-identical),
|
|
30
|
+
and mypy was tightened on the settled contracts. No public API or drawing-output
|
|
31
|
+
change. (Phases #160–#166.)
|
|
32
|
+
|
|
5
33
|
## v0.1.12 — 2026-06-21
|
|
6
34
|
|
|
7
35
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: draftwright
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
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
|
|
@@ -680,7 +680,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
680
680
|
Classifier: Programming Language :: Python :: 3.12
|
|
681
681
|
Classifier: Topic :: Scientific/Engineering
|
|
682
682
|
Requires-Python: <3.15,>=3.10
|
|
683
|
-
Requires-Dist: build123d-drafting-helpers>=0.
|
|
683
|
+
Requires-Dist: build123d-drafting-helpers>=0.13.0
|
|
684
684
|
Requires-Dist: build123d>=0.9.0
|
|
685
685
|
Requires-Dist: kiwisolver<2,>=1.4
|
|
686
686
|
Requires-Dist: numpy>=1.24
|
|
@@ -756,8 +756,7 @@ make_drawing(part, out="drawing", title="My Part", number="DWG-001")
|
|
|
756
756
|
# Composable: get a Drawing object to inspect or extend
|
|
757
757
|
dwg = build_drawing(part, title="My Part")
|
|
758
758
|
issues = dwg.lint() # list[LintIssue]
|
|
759
|
-
dwg.
|
|
760
|
-
dwg.export_dxf("drawing.dxf")
|
|
759
|
+
svg_path, dxf_path = dwg.export("drawing")
|
|
761
760
|
```
|
|
762
761
|
|
|
763
762
|
### From a STEP file
|
|
@@ -66,8 +66,7 @@ make_drawing(part, out="drawing", title="My Part", number="DWG-001")
|
|
|
66
66
|
# Composable: get a Drawing object to inspect or extend
|
|
67
67
|
dwg = build_drawing(part, title="My Part")
|
|
68
68
|
issues = dwg.lint() # list[LintIssue]
|
|
69
|
-
dwg.
|
|
70
|
-
dwg.export_dxf("drawing.dxf")
|
|
69
|
+
svg_path, dxf_path = dwg.export("drawing")
|
|
71
70
|
```
|
|
72
71
|
|
|
73
72
|
### From a STEP file
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "draftwright"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.13"
|
|
8
8
|
description = "Automated technical-drawing generation for build123d"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -12,7 +12,7 @@ authors = [{ name = "Paul Fremantle", email = "pzfreo@gmail.com" }]
|
|
|
12
12
|
requires-python = ">=3.10,<3.15"
|
|
13
13
|
dependencies = [
|
|
14
14
|
"build123d>=0.9.0",
|
|
15
|
-
"build123d-drafting-helpers>=0.
|
|
15
|
+
"build123d-drafting-helpers>=0.13.0",
|
|
16
16
|
"kiwisolver>=1.4,<2",
|
|
17
17
|
"numpy>=1.24",
|
|
18
18
|
]
|
|
@@ -59,10 +59,17 @@ include = [
|
|
|
59
59
|
testpaths = ["tests"]
|
|
60
60
|
timeout = 300
|
|
61
61
|
# `slow` covers the heavy end-to-end builds over the large NIST CTC fixtures
|
|
62
|
-
# (minutes each)
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
# (minutes each), deselected by default. `smoke` is a curated, build-light subset
|
|
63
|
+
# (~30 s) for a quick local "did I break something obvious" check — run it with
|
|
64
|
+
# `uv run pytest -m smoke`. The default run is the full fast tier (`-m 'not slow'`,
|
|
65
|
+
# ~8 min); CI owns that plus the slow tier across the OS/Python matrix.
|
|
66
|
+
markers = [
|
|
67
|
+
"slow: heavy end-to-end CTC fixture builds (deselected by default)",
|
|
68
|
+
"smoke: curated build-light subset for a fast local check (~30 s)",
|
|
69
|
+
]
|
|
70
|
+
# Coverage is a CI concern (it adds ~13% locally and instruments every line); the
|
|
71
|
+
# CI workflow passes the --cov flags explicitly. Keep the local default lean.
|
|
72
|
+
addopts = "--tb=short -m 'not slow'"
|
|
66
73
|
|
|
67
74
|
[tool.coverage.run]
|
|
68
75
|
source = ["src/draftwright"]
|
|
@@ -79,6 +86,14 @@ python_version = "3.10"
|
|
|
79
86
|
ignore_missing_imports = true
|
|
80
87
|
check_untyped_defs = true
|
|
81
88
|
warn_unused_ignores = true
|
|
89
|
+
# Tightened on the settled module contracts (#138 / ADR 0005 P7). disallow_untyped_defs
|
|
90
|
+
# stays off (geometry helpers lean on untyped build123d returns); these flags catch
|
|
91
|
+
# real gaps without demanding annotations everywhere.
|
|
92
|
+
no_implicit_optional = true
|
|
93
|
+
warn_redundant_casts = true
|
|
94
|
+
strict_equality = true
|
|
95
|
+
warn_return_any = true
|
|
96
|
+
extra_checks = true
|
|
82
97
|
|
|
83
98
|
[tool.ruff]
|
|
84
99
|
line-length = 99
|
|
@@ -94,5 +109,6 @@ dev = [
|
|
|
94
109
|
"pytest>=8",
|
|
95
110
|
"pytest-cov>=5",
|
|
96
111
|
"pytest-timeout>=2",
|
|
112
|
+
"pytest-xdist>=3",
|
|
97
113
|
"ruff>=0.4",
|
|
98
114
|
]
|
|
@@ -11,19 +11,18 @@ Requires build123d-drafting-helpers for annotation primitives.
|
|
|
11
11
|
Licensed under the GNU Affero General Public License v3 (AGPL-3.0).
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
from draftwright.analysis import analyse_face_levels, dedup_diams
|
|
14
15
|
from draftwright.make_drawing import (
|
|
15
16
|
Drawing,
|
|
16
17
|
FeatureInfo,
|
|
17
|
-
analyse_face_levels,
|
|
18
18
|
build_drawing,
|
|
19
|
-
choose_scale,
|
|
20
|
-
dedup_diams,
|
|
21
19
|
fix_svg_page_size,
|
|
22
20
|
generate_script,
|
|
23
21
|
lint_feature_coverage,
|
|
24
22
|
make_drawing,
|
|
25
23
|
)
|
|
26
24
|
from draftwright.pmi import PmiRecord, extract_pmi
|
|
25
|
+
from draftwright.sheet import choose_scale
|
|
27
26
|
|
|
28
27
|
__all__ = [
|
|
29
28
|
"Drawing",
|
|
@@ -10,21 +10,22 @@ the dimension/format helpers, and the layout constants). It imports only from
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import functools
|
|
13
14
|
import logging
|
|
14
15
|
import re
|
|
15
16
|
from dataclasses import dataclass, field
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from types import SimpleNamespace
|
|
18
19
|
|
|
19
|
-
from build123d import BoundBox, Location, Shape
|
|
20
|
+
from build123d import Align, BoundBox, Location, Mode, Shape, Text
|
|
20
21
|
from build123d_drafting.helpers import (
|
|
21
|
-
_TB_COL_FRACTIONS, # private: title-block column widths — tight coupling, see CLAUDE.md
|
|
22
22
|
Dimension,
|
|
23
23
|
TitleBlock,
|
|
24
24
|
draft_preset,
|
|
25
25
|
format_drawing_scale,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
+
from draftwright.fonts import PLEX_MONO, PLEX_SANS_CONDENSED
|
|
28
29
|
from draftwright.layout import _greedy_strip_1d, _solve_strip_1d
|
|
29
30
|
|
|
30
31
|
_log = logging.getLogger(__name__)
|
|
@@ -64,6 +65,38 @@ def _tag_sequence(n):
|
|
|
64
65
|
|
|
65
66
|
_DIAM_RE = re.compile(r"[øØ⌀]\s*(\d+(?:\.\d+)?)")
|
|
66
67
|
|
|
68
|
+
# A single-quoted label lifted from a lint message, e.g. "labels 'A' and 'B' …".
|
|
69
|
+
# Shared by the #29 lint suggestions (linting.py) and the #30 repair loop.
|
|
70
|
+
_QUOTED_RE = re.compile(r"'([^']*)'")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@functools.lru_cache(maxsize=512)
|
|
74
|
+
def _text_width(text: str, font_size: float, font_path: str = PLEX_MONO) -> float:
|
|
75
|
+
"""Measured rendered width (page-mm) of *text* at *font_size*.
|
|
76
|
+
|
|
77
|
+
Uses build123d's ``Text`` — the same primitive ``Dimension``/``HoleCallout``
|
|
78
|
+
stroke their labels with — so callout-width estimates use real glyph metrics
|
|
79
|
+
instead of a character-count fudge (#31). Pinned to a vendored font **file**
|
|
80
|
+
(``font_path``), not a system font *name*: name resolution substitutes a
|
|
81
|
+
different font on Linux, which makes this estimate — and the layout it feeds —
|
|
82
|
+
platform-variant (#149). The default is the same face the annotations render
|
|
83
|
+
with, so estimate and render agree. Cached because the same numeric labels
|
|
84
|
+
recur across holes and the rasterisation is the costly part.
|
|
85
|
+
"""
|
|
86
|
+
if not text:
|
|
87
|
+
return 0.0
|
|
88
|
+
return (
|
|
89
|
+
Text(
|
|
90
|
+
txt=text,
|
|
91
|
+
font_size=font_size,
|
|
92
|
+
font_path=font_path,
|
|
93
|
+
align=(Align.CENTER, Align.CENTER),
|
|
94
|
+
mode=Mode.PRIVATE,
|
|
95
|
+
)
|
|
96
|
+
.bounding_box()
|
|
97
|
+
.size.X
|
|
98
|
+
)
|
|
99
|
+
|
|
67
100
|
|
|
68
101
|
def _axis_letter(obj) -> str:
|
|
69
102
|
"""Letter (``"x"``/``"y"``/``"z"``) of ``obj.axis``'s dominant component.
|
|
@@ -193,6 +226,12 @@ _SLOT_DIM_DEPTH = 2 * _FONT_SIZE + _PAD # sv_zones.below: overall depth dimensi
|
|
|
193
226
|
_SLOT_DIM_HEIGHT = 2 * _FONT_SIZE + 2 * _PAD # fv_zones.right: overall height dim
|
|
194
227
|
|
|
195
228
|
|
|
229
|
+
_STRIP_SPACING = 4.0 # page-mm between successive annotations in a strip
|
|
230
|
+
_MIN_VIEW_MM = (
|
|
231
|
+
10.0 # min projected view dimension; below it annotation geometry degenerates (#129)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
196
235
|
_SLOT_DIM_STEP = 4 * _FONT_SIZE + _PAD # fv_zones.right: step-height dimension
|
|
197
236
|
|
|
198
237
|
|
|
@@ -362,6 +401,7 @@ class Analysis:
|
|
|
362
401
|
bbox_max: float
|
|
363
402
|
holes: list
|
|
364
403
|
patterns: list
|
|
404
|
+
slots: list
|
|
365
405
|
z_diams: list[float]
|
|
366
406
|
cross_diams: list[float]
|
|
367
407
|
cyls: tuple[list, list]
|
|
@@ -433,29 +473,140 @@ def _add_title_block(dwg, a: Analysis):
|
|
|
433
473
|
revision="A",
|
|
434
474
|
legal_owner="",
|
|
435
475
|
width=a.TB_W,
|
|
436
|
-
|
|
476
|
+
# Title block renders in condensed sans (the tight ISO 7200 cells), a
|
|
477
|
+
# different face from the monospace dimensions — so it carries its own
|
|
478
|
+
# pinned-font draft rather than reusing dwg.draft (#149).
|
|
479
|
+
draft=draft_preset(
|
|
480
|
+
font_size=dwg.draft.font_size,
|
|
481
|
+
decimal_precision=dwg.draft.decimal_precision,
|
|
482
|
+
font_path=PLEX_SANS_CONDENSED,
|
|
483
|
+
),
|
|
437
484
|
)
|
|
438
|
-
# Drawn-by cell geometry,
|
|
439
|
-
# so the hyperlink rect tracks any upstream
|
|
440
|
-
#
|
|
441
|
-
|
|
442
|
-
cell_h = tb.block_bbox["height"] / 2.0
|
|
485
|
+
# Drawn-by cell geometry, from the block's own public cell bbox (#139) rather
|
|
486
|
+
# than hardcoded column fractions, so the hyperlink rect tracks any upstream
|
|
487
|
+
# TitleBlock layout change. Build-frame bbox; translated to page space below.
|
|
488
|
+
cell = tb.drawn_by_cell_bbox()
|
|
443
489
|
tb = tb.locate(Location((a.PAGE_W - a.TB_W - _TB_CLEAR, _TB_CLEAR, 0)))
|
|
444
490
|
dwg.add(tb, "title_block")
|
|
445
491
|
|
|
446
492
|
# Record that cell's page-space rectangle so export() can place a clickable
|
|
447
|
-
# draftwright hyperlink over the "… / draftwright" author text.
|
|
448
|
-
#
|
|
449
|
-
# third row would squeeze the dense-part hole table).
|
|
493
|
+
# draftwright hyperlink over the "… / draftwright" author text. The build-frame
|
|
494
|
+
# cell corners are offset by the block's page location (bx, _TB_CLEAR).
|
|
450
495
|
bx = a.PAGE_W - a.TB_W - _TB_CLEAR
|
|
451
496
|
dwg._draftwright_link_rect = (
|
|
452
|
-
bx +
|
|
453
|
-
_TB_CLEAR,
|
|
454
|
-
bx +
|
|
455
|
-
_TB_CLEAR +
|
|
497
|
+
bx + cell["min_x"],
|
|
498
|
+
_TB_CLEAR + cell["min_y"],
|
|
499
|
+
bx + cell["max_x"],
|
|
500
|
+
_TB_CLEAR + cell["max_y"],
|
|
456
501
|
)
|
|
457
502
|
|
|
458
503
|
|
|
459
504
|
def _iso_bbox(dwg):
|
|
460
505
|
"""(min_x, min_y, max_x, max_y) of the placed iso view, hidden lines included."""
|
|
461
506
|
return dwg.view_bounds("iso")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# --- page/scale selection + sheet-layout constants and helpers --------------
|
|
510
|
+
# Relocated from make_drawing for the sheet.py split (#162). Shared by sheet.py
|
|
511
|
+
# (choose_scale/_layout_geometry) and make_drawing's repack pass, so they live
|
|
512
|
+
# here in the shared base to keep the DAG acyclic.
|
|
513
|
+
_DIM_PAD = 18.0
|
|
514
|
+
_STRIP_GAP = 8.0
|
|
515
|
+
|
|
516
|
+
_PAGE_SIZES = {
|
|
517
|
+
"A4": (297.0, 210.0),
|
|
518
|
+
"A3": (420.0, 297.0),
|
|
519
|
+
"A2": (594.0, 420.0),
|
|
520
|
+
"A1": (841.0, 594.0),
|
|
521
|
+
"A0": (1189.0, 841.0),
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
_SCALES = [10.0, 5.0, 2.0, 1.0, 0.5, 0.2]
|
|
525
|
+
|
|
526
|
+
# Horizontal page budget to reserve for the isometric view during scale
|
|
527
|
+
# selection and view placement, as a fraction of bbox_max * scale. This is a
|
|
528
|
+
# deliberate *under-estimate*, not the true projected size (a cube's iso
|
|
529
|
+
# projection is ~1.63*bbox_max wide): the iso is the last column and is fitted
|
|
530
|
+
# to the actual largest-empty-rect afterwards by _fit_iso_view(), which shrinks
|
|
531
|
+
# it to whatever space is genuinely left. A true fit test here is circular —
|
|
532
|
+
# the empty rect depends on the very view positions this estimate feeds — so
|
|
533
|
+
# the budget stays a single, named factor rather than a recomputed fit (#31).
|
|
534
|
+
_ISO_WIDTH_BUDGET = 0.7
|
|
535
|
+
|
|
536
|
+
# Scale selection accepts a layout when the largest empty rectangle left for the
|
|
537
|
+
# iso view can hold a square of at least this fraction of the iso's natural size
|
|
538
|
+
# (bbox_max * scale * _ISO_WIDTH_BUDGET). Below 1.0 because _fit_iso_view scales
|
|
539
|
+
# the iso down to whatever space remains, so a modestly smaller rectangle still
|
|
540
|
+
# renders a legible iso — letting a long/short part enlarge onto a sheet (e.g.
|
|
541
|
+
# 2:1 on A3) where the strict row model would have under-scaled it.
|
|
542
|
+
_ISO_MIN_FIT_FRAC = 0.6
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# Automatic scale/page preference ladder, first-fit. The enlargement/unity
|
|
546
|
+
# region is page-major: every standard scale on the smallest sheet (A4) is tried
|
|
547
|
+
# before moving to the next sheet, so a part lands on the smallest sheet it fits
|
|
548
|
+
# at the largest scale that sheet allows — e.g. a 20×15×10 part gets 2:1 on A4,
|
|
549
|
+
# not 5:1 on A3. Reductions (below 1:1) keep their legibility-vs-sheet balance
|
|
550
|
+
# (least reduction first) so a large part is not over-reduced onto a small sheet.
|
|
551
|
+
_LADDER = [
|
|
552
|
+
# A4 — smallest sheet first, largest scale first
|
|
553
|
+
(10.0, 297.0, 210.0, 120.0), # A4 10:1
|
|
554
|
+
(5.0, 297.0, 210.0, 120.0), # A4 5:1
|
|
555
|
+
(2.0, 297.0, 210.0, 120.0), # A4 2:1
|
|
556
|
+
(1.0, 297.0, 210.0, 120.0), # A4 1:1
|
|
557
|
+
# A3
|
|
558
|
+
(5.0, 420.0, 297.0, 150.0), # A3 5:1
|
|
559
|
+
(2.0, 420.0, 297.0, 150.0), # A3 2:1
|
|
560
|
+
(1.0, 420.0, 297.0, 150.0), # A3 1:1
|
|
561
|
+
# A2
|
|
562
|
+
(2.0, 594.0, 420.0, 150.0), # A2 2:1
|
|
563
|
+
(1.0, 594.0, 420.0, 150.0), # A2 1:1
|
|
564
|
+
# A1
|
|
565
|
+
(1.0, 841.0, 594.0, 150.0), # A1 1:1
|
|
566
|
+
# Reductions — least reduction first, so a too-big part is not crammed onto a
|
|
567
|
+
# small sheet at an illegible scale.
|
|
568
|
+
(0.5, 594.0, 420.0, 150.0), # A2 1:2
|
|
569
|
+
(0.2, 420.0, 297.0, 150.0), # A3 1:5
|
|
570
|
+
(0.2, 594.0, 420.0, 150.0), # A2 1:5
|
|
571
|
+
(0.5, 841.0, 594.0, 150.0), # A1 1:2
|
|
572
|
+
(0.2, 841.0, 594.0, 150.0), # A1 1:5
|
|
573
|
+
(0.5, 1189.0, 841.0, 150.0), # A0 1:2
|
|
574
|
+
(0.2, 1189.0, 841.0, 150.0), # A0 1:5
|
|
575
|
+
]
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _tb_width(page_w: float) -> float:
|
|
579
|
+
"""Title-block width for a page: 120 mm on A4, 150 mm on A3 and larger."""
|
|
580
|
+
return 120.0 if page_w <= 297.0 else 150.0
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _parse_page(page) -> tuple:
|
|
584
|
+
"""Resolve a page spec to ``(PAGE_W, PAGE_H, TB_W)``.
|
|
585
|
+
|
|
586
|
+
Accepts an ISO name (``"A4"``…``"A0"``, case-insensitive), a
|
|
587
|
+
``"WIDTHxHEIGHT"`` string in mm (e.g. ``"420x297"``), or a
|
|
588
|
+
``(width, height)`` tuple in mm.
|
|
589
|
+
"""
|
|
590
|
+
if isinstance(page, str):
|
|
591
|
+
name = page.strip().upper()
|
|
592
|
+
if name in _PAGE_SIZES:
|
|
593
|
+
pw, ph = _PAGE_SIZES[name]
|
|
594
|
+
else:
|
|
595
|
+
m = re.fullmatch(r"(\d+(?:\.\d+)?)\s*[xX×]\s*(\d+(?:\.\d+)?)", page.strip())
|
|
596
|
+
if not m:
|
|
597
|
+
raise ValueError(
|
|
598
|
+
f"unknown page size {page!r} — expected one of "
|
|
599
|
+
f"{', '.join(_PAGE_SIZES)} or WIDTHxHEIGHT in mm (e.g. '420x297')"
|
|
600
|
+
)
|
|
601
|
+
pw, ph = float(m.group(1)), float(m.group(2))
|
|
602
|
+
else:
|
|
603
|
+
try:
|
|
604
|
+
pw, ph = float(page[0]), float(page[1])
|
|
605
|
+
except (TypeError, ValueError, IndexError):
|
|
606
|
+
raise ValueError(
|
|
607
|
+
f"invalid page size {page!r} — expected an ISO name, "
|
|
608
|
+
f"'WIDTHxHEIGHT', or a (width, height) tuple in mm"
|
|
609
|
+
) from None
|
|
610
|
+
if pw <= 0 or ph <= 0:
|
|
611
|
+
raise ValueError(f"page dimensions must be positive, got {page!r}")
|
|
612
|
+
return pw, ph, _tb_width(pw)
|