draftwright 0.2.1__tar.gz → 0.2.2__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.1 → draftwright-0.2.2}/CHANGELOG.md +25 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/PKG-INFO +1 -1
- {draftwright-0.2.1 → draftwright-0.2.2}/pyproject.toml +1 -1
- draftwright-0.2.2/src/draftwright/__init__.py +103 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/annotations/from_model.py +17 -3
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/annotations/holes.py +58 -23
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/annotations/orchestrator.py +31 -3
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/cli.py +6 -2
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_make_drawing.py +149 -19
- draftwright-0.2.1/src/draftwright/__init__.py +0 -62
- {draftwright-0.2.1 → draftwright-0.2.2}/.gitignore +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/LICENSE +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/README.md +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/skills/SKILL.md +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/_core.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/analysis.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/annotate.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/annotations/_common.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/annotations/sections.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/builder.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/drawing.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/export.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/features.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/model/__init__.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/model/detect.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/model/ir.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/repair.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/sheet.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_layout.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_linting.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_part_model.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_pmi.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_recognition.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_registry.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_render_seam.py +0 -0
- {draftwright-0.2.1 → draftwright-0.2.2}/tests/test_turned_steps.py +0 -0
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.2.2 — 2026-06-30
|
|
6
|
+
|
|
7
|
+
A patch release of turned-part dimension-placement fixes and a CLI start-up
|
|
8
|
+
speed-up. Drawing output changes for the affected turned/cross-drilled parts.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A coaxial bore callout on a *stepped* turned shaft is now lifted off the round
|
|
13
|
+
view's centre axis** (#305). The earlier fix only triggered for a uniform
|
|
14
|
+
(`is_rotational`) cylinder; a stepped shaft (e.g. the gramel GRM-03 drive screw)
|
|
15
|
+
has a turned step profile but isn't classified rotational, so its `⌀… ↓…` bore
|
|
16
|
+
callout was still leadered straight along the centreline, with the centre mark
|
|
17
|
+
running through the text. The lift now also fires for a turned-profile part.
|
|
18
|
+
- **A side-drilled hole's location dimension now stacks *inside* the overall
|
|
19
|
+
envelope dimension** (ISO order — overall dim outermost, feature/location dims
|
|
20
|
+
nearer the view). It was placed *outside* the envelope, which forced the shorter
|
|
21
|
+
location dim's arrowheads to flip outward and clash (seen on GRM-01 and GRM-02).
|
|
22
|
+
The mandatory overall dimension is still guaranteed placement.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **CLI shell completion and `--help` are fast again** (#313). The Typer CLI and
|
|
27
|
+
the heavy CAD engine are now imported lazily, so tab-completion and `--help` no
|
|
28
|
+
longer pay a ~6 s engine-import cost; a real drawing run is unaffected.
|
|
29
|
+
|
|
5
30
|
## v0.2.1 — 2026-06-30
|
|
6
31
|
|
|
7
32
|
A patch release focused on **turned-part dimensioning legibility**: crowded
|
|
@@ -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
|
+
]
|
|
@@ -532,6 +532,20 @@ def _env_pd(group, role):
|
|
|
532
532
|
return next((pd for pd in group.dims if pd.param.role == role), None)
|
|
533
533
|
|
|
534
534
|
|
|
535
|
+
def envelope_group(groups):
|
|
536
|
+
"""The envelope `DimensionGroup` in *groups*, or None."""
|
|
537
|
+
return next((g for g in groups if g.feature_kind == "envelope"), None)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def env_dim_placed(pd) -> bool:
|
|
541
|
+
"""Whether :func:`render_envelope` will actually place an envelope dim for the
|
|
542
|
+
PlannedDimension *pd* — present, not suppressed by the planner (square footprint /
|
|
543
|
+
X-turned, #250), and carrying a span. The single source of truth for that
|
|
544
|
+
decision, shared with the orchestrator's side-below tier reservation so the two
|
|
545
|
+
can never drift (#316 review)."""
|
|
546
|
+
return pd is not None and not pd.suppressed and pd.param.span is not None
|
|
547
|
+
|
|
548
|
+
|
|
535
549
|
def render_envelope(dwg, groups, a) -> int:
|
|
536
550
|
"""Overall width (plan, below) + depth (side, below) envelope dims via the IR,
|
|
537
551
|
placed through the engine's below-strip zone allocators (the zone-aware render
|
|
@@ -539,12 +553,12 @@ def render_envelope(dwg, groups, a) -> int:
|
|
|
539
553
|
those strips). The **planner** decides suppression (square footprint / X-turned;
|
|
540
554
|
#250); this renderer just skips suppressed dims and places the rest. Returns the
|
|
541
555
|
count placed."""
|
|
542
|
-
env =
|
|
556
|
+
env = envelope_group(groups)
|
|
543
557
|
if env is None:
|
|
544
558
|
return 0
|
|
545
559
|
n = 0
|
|
546
560
|
width = _env_pd(env, "width")
|
|
547
|
-
if width
|
|
561
|
+
if env_dim_placed(width):
|
|
548
562
|
(x0, y0, z0), (x1, _, _) = width.param.span
|
|
549
563
|
p1, p2 = dwg.at("plan", x0, y0, z0), dwg.at("plan", x1, y0, z0)
|
|
550
564
|
witness = p1[1] - 2
|
|
@@ -564,7 +578,7 @@ def render_envelope(dwg, groups, a) -> int:
|
|
|
564
578
|
)
|
|
565
579
|
n += 1
|
|
566
580
|
depth = _env_pd(env, "depth")
|
|
567
|
-
if depth
|
|
581
|
+
if env_dim_placed(depth):
|
|
568
582
|
(x0, y0, z0), (_, y1, _) = depth.param.span
|
|
569
583
|
p1, p2 = dwg.at("side", x0, y0, z0), dwg.at("side", x0, y1, z0)
|
|
570
584
|
witness = p1[1] - 2
|
|
@@ -73,17 +73,29 @@ def _record_callout_drop(dwg, view, diam, reason):
|
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def _locate_off_axis_holes(dwg, a: Analysis, holes_in=None):
|
|
76
|
+
def _locate_off_axis_holes(dwg, a: Analysis, holes_in=None, *, which):
|
|
77
77
|
"""Location dimensions for side-drilled holes (#133).
|
|
78
78
|
|
|
79
79
|
An X-axis hole is a circle in the SIDE view (locate its Y below the view and
|
|
80
80
|
its Z to the right — the side view has no left strip); a Y-axis hole is a
|
|
81
81
|
circle in the FRONT view (locate its X below and its Z to the right). Each
|
|
82
|
-
offset is allocated from the view's strip so dims stack without overlap
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
offset is allocated from the view's strip so dims stack without overlap. A tier
|
|
83
|
+
with no room is dropped and recorded as ``off_axis_location_dropped`` — never
|
|
84
|
+
force-stacked. Holes already covered by a pattern callout are skipped.
|
|
85
|
+
|
|
86
|
+
Run in two phases (``which`` is ``"across"`` or ``"along"``) so each dim stacks in
|
|
87
|
+
the ISO order — overall dim OUTERMOST, feature/location dims nearer the view:
|
|
88
|
+
|
|
89
|
+
- ``"across"`` — an X-axis hole's in-plane (Y, side-below) location, placed BEFORE
|
|
90
|
+
the envelope so the overall depth dim lands outside it (the side-view
|
|
91
|
+
counterpart of the plan view, where location dims already precede the
|
|
92
|
+
envelope). Fixes the inverted stack where the overall dim sat innermost and
|
|
93
|
+
forced the shorter location dim's arrows outside. The orchestrator reserves the
|
|
94
|
+
envelope's tier first, so these best-effort dims can't starve it.
|
|
95
|
+
- ``"along"`` — a Y-axis hole's X (front-below) location and every hole's height
|
|
96
|
+
(Z, right-strip), placed AFTER the envelope and the turned-diameter passes so
|
|
97
|
+
they never evict those overall dims from the contended front-below / right
|
|
98
|
+
strips (#133).
|
|
87
99
|
"""
|
|
88
100
|
draft = dwg.draft
|
|
89
101
|
all_holes = a.holes if holes_in is None else holes_in
|
|
@@ -150,17 +162,35 @@ def _locate_off_axis_holes(dwg, a: Analysis, holes_in=None):
|
|
|
150
162
|
):
|
|
151
163
|
_drop(axis, view)
|
|
152
164
|
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
# "across" phase — an X-axis hole's Y position below the SIDE view, placed BEFORE
|
|
166
|
+
# the envelope so the overall depth dim stacks outside it (ISO order). Confined to
|
|
167
|
+
# the side view: a Y-axis hole's X position contends the FRONT-below strip with the
|
|
168
|
+
# turned-diameter ø-row, so it stays in the "along" phase (after the diameter pass),
|
|
169
|
+
# preserving the #133 priority. The orchestrator reserves one sv_zones.below tier
|
|
170
|
+
# for the mandatory envelope depth before calling this, so these best-effort
|
|
171
|
+
# location dims can never starve it.
|
|
172
|
+
if which == "across":
|
|
173
|
+
yw = SZ(dz) - 2
|
|
174
|
+
seen_y: set = set()
|
|
175
|
+
for h in (h for h in off if _axis_letter(h) == "x"):
|
|
176
|
+
yo = round(abs(h.location[1] - dy), 2)
|
|
177
|
+
if yo * a.SCALE >= 1.0 and yo not in seen_y:
|
|
178
|
+
seen_y.add(yo)
|
|
179
|
+
_below(
|
|
180
|
+
a.sv_zones.below,
|
|
181
|
+
"side",
|
|
182
|
+
(SX(dy), yw, 0),
|
|
183
|
+
(SX(h.location[1]), yw, 0),
|
|
184
|
+
yw,
|
|
185
|
+
yo,
|
|
186
|
+
"y",
|
|
187
|
+
)
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# "along" phase (after the envelope + turned-diameter passes): a Y-axis hole's X
|
|
191
|
+
# position below the FRONT view, then every hole's height (Z) to the right.
|
|
192
|
+
xw = FZ(dz) - 2
|
|
193
|
+
seen_x: set = set()
|
|
164
194
|
for h in (h for h in off if _axis_letter(h) == "y"):
|
|
165
195
|
xo = round(abs(h.location[0] - dx), 2)
|
|
166
196
|
if xo * a.SCALE >= 1.0 and xo not in seen_x:
|
|
@@ -520,21 +550,26 @@ def _annotate_holes(dwg, a: Analysis, view_of_axis, groups, feature_keys):
|
|
|
520
550
|
|
|
521
551
|
A bore on the turning axis is led out along the view's horizontal centre
|
|
522
552
|
axis, so the centre mark / centreline runs straight through the "⌀… ↓…"
|
|
523
|
-
callout text. Detect that one bore — a rotational part, hole at
|
|
524
|
-
centre — and lift its row a clearance off the axis (an angled leader
|
|
525
|
-
central feature is standard practice), toward the roomier side. Off-axis
|
|
553
|
+
callout text. Detect that one bore — a **turned/rotational** part, hole at
|
|
554
|
+
the view centre — and lift its row a clearance off the axis (an angled leader
|
|
555
|
+
to a central feature is standard practice), toward the roomier side. Off-axis
|
|
526
556
|
holes and every prismatic-part hole are untouched (front-view round parts
|
|
527
557
|
place coaxial bores as vertical shafts below the view, not along an axis,
|
|
528
558
|
so they can't hit this and are exempt by construction).
|
|
529
559
|
|
|
560
|
+
The "turned/rotational" gate is ``is_rotational OR prof`` — a *stepped*
|
|
561
|
+
turned shaft (e.g. the GRM-03 drive screw) has a turned step profile but is
|
|
562
|
+
not ``is_rotational`` (its varying OD doesn't fill a square cross-section),
|
|
563
|
+
yet its coaxial bore hits exactly this defect. A prismatic part has neither,
|
|
564
|
+
so it stays excluded (the regression the original gate guarded against, #305).
|
|
565
|
+
|
|
530
566
|
Tactical: the principled fix is to not draw the crossing line at all — a
|
|
531
567
|
centred bore is located by the axis, so its linear location dims are
|
|
532
568
|
redundant (#309) — or to make this a layout-solver separation constraint
|
|
533
569
|
(ADR 0003). This nudge becomes dead code once either lands."""
|
|
534
570
|
tol = draft.font_size # "hole at the view centre" tolerance (page mm)
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
):
|
|
571
|
+
turned = a.is_rotational or a.prof is not None
|
|
572
|
+
if not (turned and abs(centre[0] - view_cx) < tol and abs(centre[1] - view_cy) < tol):
|
|
538
573
|
return ny
|
|
539
574
|
# Lift the row a full text height + padding clear of the axis: enough for
|
|
540
575
|
# the text box (half a font tall) to sit wholly off the centre line with a
|
|
@@ -19,6 +19,7 @@ import math
|
|
|
19
19
|
|
|
20
20
|
from draftwright._core import (
|
|
21
21
|
_CONCENTRIC_TOL_MM,
|
|
22
|
+
_SLOT_DIM_DEPTH,
|
|
22
23
|
_TABULATE_MIN_HOLES,
|
|
23
24
|
Analysis,
|
|
24
25
|
HoleRef,
|
|
@@ -30,6 +31,9 @@ from draftwright._core import (
|
|
|
30
31
|
_tag_sequence,
|
|
31
32
|
)
|
|
32
33
|
from draftwright.annotations.from_model import (
|
|
34
|
+
_env_pd,
|
|
35
|
+
env_dim_placed,
|
|
36
|
+
envelope_group,
|
|
33
37
|
render_centermarks,
|
|
34
38
|
render_diameters,
|
|
35
39
|
render_envelope,
|
|
@@ -213,6 +217,29 @@ def _auto_annotate(dwg, a: Analysis, *, detail_view: bool = False):
|
|
|
213
217
|
# turned parts, and a Z-turned overall height is suppressed there (ISO 129).
|
|
214
218
|
render_height_ladder(dwg, _model, a)
|
|
215
219
|
|
|
220
|
+
# Side-drilled holes' in-plane (side-below) locations FIRST, so the overall
|
|
221
|
+
# envelope depth lands OUTSIDE them — ISO stacks the overall dim outermost,
|
|
222
|
+
# feature/location dims nearer the view (matches the plan view, where location
|
|
223
|
+
# dims precede the envelope). To keep the #133 guarantee that the MANDATORY
|
|
224
|
+
# envelope dim is never starved, reserve its tier in the side-below strip first
|
|
225
|
+
# (shrink the strip by one depth slot for the location pass, then restore) — so
|
|
226
|
+
# the best-effort locations fill inner tiers and the envelope always gets the
|
|
227
|
+
# outermost. The height (right-strip) locations stay in the "along" phase.
|
|
228
|
+
if feature_holes:
|
|
229
|
+
# Only reserve when render_envelope will actually place the depth dim — the
|
|
230
|
+
# planner suppresses it for a square footprint / X-turned part (#250); reserving
|
|
231
|
+
# a tier it never claims would needlessly shrink the strip and drop a location
|
|
232
|
+
# dim that would otherwise fit (#316 review). Uses render_envelope's own
|
|
233
|
+
# place-predicate (env_dim_placed) so the two can never drift.
|
|
234
|
+
_env_g = envelope_group(_groups)
|
|
235
|
+
_reserve = _env_g is not None and env_dim_placed(_env_pd(_env_g, "depth"))
|
|
236
|
+
_below = a.sv_zones.below
|
|
237
|
+
_saved_limit = _below.outer_limit
|
|
238
|
+
if _reserve:
|
|
239
|
+
_below.outer_limit -= _below.direction * (_SLOT_DIM_DEPTH + _below.spacing)
|
|
240
|
+
_locate_off_axis_holes(dwg, a, holes_in=feature_holes, which="across")
|
|
241
|
+
_below.outer_limit = _saved_limit
|
|
242
|
+
|
|
216
243
|
# Overall width (plan, below) + depth (side, below) envelope dims — IR renderer,
|
|
217
244
|
# placed through the same below-strip zone allocators the engine used (zone-aware
|
|
218
245
|
# render stage, ADR 0008). Suppression (square footprint / X-turned width) is now
|
|
@@ -245,10 +272,11 @@ def _auto_annotate(dwg, a: Analysis, *, detail_view: bool = False):
|
|
|
245
272
|
if a.prof is not None:
|
|
246
273
|
render_step_lengths(dwg, _groups)
|
|
247
274
|
|
|
248
|
-
# Side-drilled (X/Y-axis) hole locations — last, so the envelope and
|
|
249
|
-
# turned-diameter dims claim their strip space first and are
|
|
275
|
+
# Side-drilled (X/Y-axis) hole HEIGHT locations — last, so the envelope and
|
|
276
|
+
# turned-diameter dims claim their (contended right) strip space first and are
|
|
277
|
+
# never evicted (#133). The in-plane locations were placed before the envelope.
|
|
250
278
|
if feature_holes:
|
|
251
|
-
_locate_off_axis_holes(dwg, a, holes_in=feature_holes)
|
|
279
|
+
_locate_off_axis_holes(dwg, a, holes_in=feature_holes, which="along")
|
|
252
280
|
|
|
253
281
|
# Non-cylindrical machined features: slots / reduced across-flats sections
|
|
254
282
|
# (#135) — IR renderer, placed through the zone strips (shared infra). Runs
|
|
@@ -17,8 +17,6 @@ from importlib.metadata import version as _pkg_version
|
|
|
17
17
|
|
|
18
18
|
import typer
|
|
19
19
|
|
|
20
|
-
from draftwright.builder import build_drawing, generate_script
|
|
21
|
-
|
|
22
20
|
app = typer.Typer(
|
|
23
21
|
add_completion=True,
|
|
24
22
|
# Plain tracebacks on an engine error, matching the old argparse CLI — a rich
|
|
@@ -144,6 +142,12 @@ def main(
|
|
|
144
142
|
|
|
145
143
|
formats = _parse_formats(output_format)
|
|
146
144
|
|
|
145
|
+
# Import the engine lazily, only on the build path: it pulls in build123d/OCP
|
|
146
|
+
# (~5 s of CAD-kernel import). Keeping it out of module scope means shell
|
|
147
|
+
# completion, --help and --version (which import this module but never call
|
|
148
|
+
# the command) stay sub-second instead of paying for the kernel every time.
|
|
149
|
+
from draftwright.builder import build_drawing, generate_script
|
|
150
|
+
|
|
147
151
|
if script:
|
|
148
152
|
py_path = generate_script(
|
|
149
153
|
step_file=step_file,
|
|
@@ -1648,6 +1648,42 @@ def test_cli_version_reports_installed_version():
|
|
|
1648
1648
|
assert result.stdout.strip() == f"draftwright {_pkg_version('draftwright')}"
|
|
1649
1649
|
|
|
1650
1650
|
|
|
1651
|
+
def test_cli_import_does_not_load_the_engine():
|
|
1652
|
+
"""Importing the CLI must not pull in build123d/OCP (#313). Shell completion,
|
|
1653
|
+
--help and --version import this module on every invocation; loading the
|
|
1654
|
+
~5 s CAD kernel there made each TAB press take ~6 s. Guard the lazy boundary:
|
|
1655
|
+
the engine is imported only on the actual build path, in a fresh process so
|
|
1656
|
+
other tests' imports can't mask a regression."""
|
|
1657
|
+
code = (
|
|
1658
|
+
"import sys, draftwright.cli; "
|
|
1659
|
+
"heavy = [m for m in ('build123d', 'OCP') if m in sys.modules]; "
|
|
1660
|
+
"print(','.join(heavy)); sys.exit(1 if heavy else 0)"
|
|
1661
|
+
)
|
|
1662
|
+
result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True)
|
|
1663
|
+
assert result.returncode == 0, (
|
|
1664
|
+
f"`import draftwright.cli` eagerly loaded: {result.stdout.strip()}"
|
|
1665
|
+
)
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
def test_lazy_public_api_preserves_make_drawing_identity():
|
|
1669
|
+
"""The lazy package __init__ (#313) must still expose the public API, and
|
|
1670
|
+
`draftwright.make_drawing` must stay the FUNCTION even after the compat
|
|
1671
|
+
submodule of the same name is imported and would otherwise shadow it."""
|
|
1672
|
+
code = (
|
|
1673
|
+
"import types, draftwright as d; "
|
|
1674
|
+
"from draftwright import make_drawing, build_drawing, Drawing, choose_scale; "
|
|
1675
|
+
"assert callable(make_drawing) and not isinstance(make_drawing, types.ModuleType); "
|
|
1676
|
+
"assert d.make_drawing is make_drawing; "
|
|
1677
|
+
"import draftwright.make_drawing; " # provoke the shadowing path
|
|
1678
|
+
"assert callable(d.make_drawing) and not isinstance(d.make_drawing, types.ModuleType); "
|
|
1679
|
+
"print('ok')"
|
|
1680
|
+
)
|
|
1681
|
+
result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True)
|
|
1682
|
+
assert result.returncode == 0 and result.stdout.strip() == "ok", (
|
|
1683
|
+
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
|
1684
|
+
)
|
|
1685
|
+
|
|
1686
|
+
|
|
1651
1687
|
# ---------------------------------------------------------------------------
|
|
1652
1688
|
# --format selector (#288) — pure logic, no OCP build needed
|
|
1653
1689
|
# ---------------------------------------------------------------------------
|
|
@@ -2294,6 +2330,68 @@ class TestPrismaticClassification:
|
|
|
2294
2330
|
# the right strips — the sheet stays lint-clean (#133 rework).
|
|
2295
2331
|
assert [i for i in dwg.lint() if i.severity != "info"] == []
|
|
2296
2332
|
|
|
2333
|
+
def test_side_view_location_dim_stacks_inside_the_envelope(self):
|
|
2334
|
+
# ISO stacking: the overall (envelope) dim sits OUTERMOST, the feature/location
|
|
2335
|
+
# dim nearer the view. A side-drilled hole's in-plane location must therefore
|
|
2336
|
+
# be CLOSER to the side view than the envelope-depth dim. The inverted stack
|
|
2337
|
+
# (envelope innermost) forced the shorter location dim's arrows to flip outward
|
|
2338
|
+
# and clash with the envelope (GRM-01 / GRM-02).
|
|
2339
|
+
from build123d import Rot
|
|
2340
|
+
|
|
2341
|
+
# Off-centre hole (y=2, not the centreline) so the location dim is real, not a
|
|
2342
|
+
# redundant centred one — this isolates the stacking order.
|
|
2343
|
+
part = Box(12, 11, 40) - Pos(0, 2, 6) * Rot(0, 90, 0) * Cylinder(3, 12)
|
|
2344
|
+
dwg = build_drawing(part)
|
|
2345
|
+
env = dwg._named.get("m_env_depth")
|
|
2346
|
+
loc = [o for n, o in dwg._named.items() if n.startswith("dim_loc_side_y")]
|
|
2347
|
+
assert env is not None and loc, "expected an envelope-depth dim and a side location dim"
|
|
2348
|
+
|
|
2349
|
+
def ymid(o):
|
|
2350
|
+
bb = o.bounding_box()
|
|
2351
|
+
return (bb.min.Y + bb.max.Y) / 2
|
|
2352
|
+
|
|
2353
|
+
# The below strip extends downward from the side view, so nearer the view =
|
|
2354
|
+
# higher Y. The location dim must sit nearer the view than the overall dim.
|
|
2355
|
+
assert min(ymid(o) for o in loc) > ymid(env), "location must stack inside the envelope"
|
|
2356
|
+
assert [i for i in dwg.lint() if i.severity != "info"] == []
|
|
2357
|
+
|
|
2358
|
+
def test_envelope_depth_survives_many_side_location_dims(self):
|
|
2359
|
+
# The mandatory overall depth dim must always be placed, even when several
|
|
2360
|
+
# side-drilled holes fill the side-below strip with location dims. The
|
|
2361
|
+
# location pass now runs before the envelope (for ISO stacking), so the
|
|
2362
|
+
# orchestrator reserves the envelope's tier first — best-effort location dims
|
|
2363
|
+
# can never starve it (the #316-review regression).
|
|
2364
|
+
from build123d import Cylinder, Pos, Rot
|
|
2365
|
+
|
|
2366
|
+
part = Box(12, 24, 60)
|
|
2367
|
+
for y, z in [(-9, -20), (-5, -8), (7, 4), (10, 16)]:
|
|
2368
|
+
part -= Pos(0, y, z) * Rot(0, 90, 0) * Cylinder(1.5, 12)
|
|
2369
|
+
dwg = build_drawing(part)
|
|
2370
|
+
ylocs = [n for n in dwg._named if n.startswith("dim_loc_side_y")]
|
|
2371
|
+
assert len(ylocs) >= 2, "expected several side-below location dims for strip pressure"
|
|
2372
|
+
assert "m_env_depth" in dwg._named, "the mandatory overall depth dim was starved"
|
|
2373
|
+
assert dwg.lint_summary()["by_code"].get("missing_principal_dimension", 0) == 0
|
|
2374
|
+
|
|
2375
|
+
def ymid(o):
|
|
2376
|
+
bb = o.bounding_box()
|
|
2377
|
+
return (bb.min.Y + bb.max.Y) / 2
|
|
2378
|
+
|
|
2379
|
+
env = dwg._named["m_env_depth"]
|
|
2380
|
+
assert all(ymid(dwg._named[n]) > ymid(env) for n in ylocs), "locations must stack inside"
|
|
2381
|
+
|
|
2382
|
+
def test_square_footprint_does_not_reserve_a_suppressed_envelope_tier(self):
|
|
2383
|
+
# When the planner suppresses the depth dim (square footprint / X-turned),
|
|
2384
|
+
# the side-below envelope-tier reservation must NOT fire — reserving a tier
|
|
2385
|
+
# render_envelope never claims would needlessly shrink the strip and drop a
|
|
2386
|
+
# side location that otherwise fits (#316 review).
|
|
2387
|
+
from build123d import Cylinder, Pos, Rot
|
|
2388
|
+
|
|
2389
|
+
part = Box(20, 20, 40) - Pos(0, 4, 0) * Rot(0, 90, 0) * Cylinder(2, 20)
|
|
2390
|
+
dwg = build_drawing(part)
|
|
2391
|
+
assert "m_env_depth" not in dwg._named # square footprint → depth suppressed
|
|
2392
|
+
assert [n for n in dwg._named if n.startswith("dim_loc_side_y")], "location was dropped"
|
|
2393
|
+
assert dwg.lint_summary()["by_code"].get("off_axis_location_dropped", 0) == 0
|
|
2394
|
+
|
|
2297
2395
|
@pytest.mark.timeout(60)
|
|
2298
2396
|
def test_locates_every_side_drilled_hole_not_just_the_first(self):
|
|
2299
2397
|
# Two side-drilled (Y-axis) holes at distinct x: each must get its own
|
|
@@ -3687,6 +3785,32 @@ class TestLintSummaryAndDrops:
|
|
|
3687
3785
|
|
|
3688
3786
|
|
|
3689
3787
|
class TestLayoutGeneralisation:
|
|
3788
|
+
@staticmethod
|
|
3789
|
+
def _lines_crossing_label(dwg, callout_name, label_bbox):
|
|
3790
|
+
"""Horizontal lines (other than the callout's own shelf) that cross the
|
|
3791
|
+
callout's text box — the #305 'line through the callout text' defect. Shared
|
|
3792
|
+
by the coaxial-bore tests."""
|
|
3793
|
+
tx0, ty0, tx1, ty1 = label_bbox
|
|
3794
|
+
crossings = []
|
|
3795
|
+
for n, o in dwg._named.items():
|
|
3796
|
+
if n == callout_name:
|
|
3797
|
+
continue
|
|
3798
|
+
try:
|
|
3799
|
+
edges = list(o.edges())
|
|
3800
|
+
except Exception:
|
|
3801
|
+
continue
|
|
3802
|
+
for e in edges:
|
|
3803
|
+
vs = e.vertices()
|
|
3804
|
+
if len(vs) != 2:
|
|
3805
|
+
continue
|
|
3806
|
+
(x0, y0), (x1, y1) = (vs[0].X, vs[0].Y), (vs[1].X, vs[1].Y)
|
|
3807
|
+
if abs(y0 - y1) < 0.05 and abs(x0 - x1) > 1.0: # a horizontal line
|
|
3808
|
+
ym = (y0 + y1) / 2
|
|
3809
|
+
xa, xb = min(x0, x1), max(x0, x1)
|
|
3810
|
+
if ty0 + 0.3 < ym < ty1 - 0.3 and xb > tx0 + 0.3 and xa < tx1 - 0.3:
|
|
3811
|
+
crossings.append((n, round(ym, 2)))
|
|
3812
|
+
return crossings
|
|
3813
|
+
|
|
3690
3814
|
@pytest.mark.timeout(120)
|
|
3691
3815
|
def test_turned_flange_gets_both_od_and_hole_furniture(self):
|
|
3692
3816
|
# A turned-and-drilled flange (cylinder OD + centre bore + bolt circle)
|
|
@@ -3756,26 +3880,32 @@ class TestLayoutGeneralisation:
|
|
|
3756
3880
|
hc = [(n, o) for n, o in dwg._named.items() if n.startswith("hc_side")]
|
|
3757
3881
|
assert hc, "expected a bore callout on the round (side) view"
|
|
3758
3882
|
name, leader = hc[0]
|
|
3759
|
-
|
|
3883
|
+
crossings = self._lines_crossing_label(dwg, name, leader.label_bbox)
|
|
3884
|
+
assert not crossings, f"line(s) cross the bore callout text: {crossings}"
|
|
3760
3885
|
|
|
3761
|
-
|
|
3762
|
-
for
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3886
|
+
def test_coaxial_bore_callout_clears_centre_axis_on_stepped_shaft(self):
|
|
3887
|
+
# #305 regression: the lift must also fire for a *stepped* turned shaft (the
|
|
3888
|
+
# GRM-03 drive screw), which has a turned step profile but is NOT
|
|
3889
|
+
# is_rotational (its varying OD doesn't fill a square cross-section) — the
|
|
3890
|
+
# original is_rotational-only gate missed it, leaving the bore callout led
|
|
3891
|
+
# straight along the centre axis. Assert no horizontal line crosses the text.
|
|
3892
|
+
from build123d import Align, Cylinder, Pos, Rotation
|
|
3893
|
+
|
|
3894
|
+
from draftwright import build_drawing
|
|
3895
|
+
|
|
3896
|
+
b = Align.MIN
|
|
3897
|
+
part = (
|
|
3898
|
+
Cylinder(6, 12, align=(Align.CENTER, Align.CENTER, b))
|
|
3899
|
+
+ Pos(0, 0, 12) * Cylinder(4, 12, align=(Align.CENTER, Align.CENTER, b))
|
|
3900
|
+
- Cylinder(0.8, 8, align=(Align.CENTER, Align.CENTER, b))
|
|
3901
|
+
)
|
|
3902
|
+
dwg = build_drawing(Rotation(0, 90, 0) * part, scale=2.0)
|
|
3903
|
+
assert dwg._analysis.prof is not None and not dwg._analysis.is_rotational
|
|
3904
|
+
|
|
3905
|
+
hc = [(n, o) for n, o in dwg._named.items() if n.startswith("hc_side")]
|
|
3906
|
+
assert hc, "expected a bore callout on the round (side) view"
|
|
3907
|
+
name, leader = hc[0]
|
|
3908
|
+
crossings = self._lines_crossing_label(dwg, name, leader.label_bbox)
|
|
3779
3909
|
assert not crossings, f"line(s) cross the bore callout text: {crossings}"
|
|
3780
3910
|
|
|
3781
3911
|
def test_prismatic_central_hole_callout_not_lifted(self):
|
|
@@ -1,62 +0,0 @@
|
|
|
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 sys as _sys
|
|
15
|
-
import types as _types
|
|
16
|
-
|
|
17
|
-
from draftwright.analysis import analyse_face_levels, dedup_diams
|
|
18
|
-
from draftwright.builder import (
|
|
19
|
-
build_drawing,
|
|
20
|
-
generate_script,
|
|
21
|
-
make_drawing,
|
|
22
|
-
)
|
|
23
|
-
from draftwright.drawing import Drawing, FeatureInfo
|
|
24
|
-
from draftwright.export import fix_svg_page_size
|
|
25
|
-
from draftwright.linting import lint_feature_coverage
|
|
26
|
-
from draftwright.pmi import PmiRecord, extract_pmi
|
|
27
|
-
from draftwright.sheet import choose_scale
|
|
28
|
-
|
|
29
|
-
_make_drawing_function = make_drawing
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class _DraftwrightModule(_types.ModuleType):
|
|
33
|
-
def __getattribute__(self, name):
|
|
34
|
-
if name == "make_drawing":
|
|
35
|
-
namespace = _types.ModuleType.__getattribute__(self, "__dict__")
|
|
36
|
-
value = namespace.get(name)
|
|
37
|
-
if (
|
|
38
|
-
isinstance(value, _types.ModuleType)
|
|
39
|
-
and value.__name__ == "draftwright.make_drawing"
|
|
40
|
-
):
|
|
41
|
-
public = namespace["_make_drawing_function"]
|
|
42
|
-
_types.ModuleType.__setattr__(self, name, public)
|
|
43
|
-
return public
|
|
44
|
-
return _types.ModuleType.__getattribute__(self, name)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
_sys.modules[__name__].__class__ = _DraftwrightModule
|
|
48
|
-
|
|
49
|
-
__all__ = [
|
|
50
|
-
"Drawing",
|
|
51
|
-
"FeatureInfo",
|
|
52
|
-
"PmiRecord",
|
|
53
|
-
"analyse_face_levels",
|
|
54
|
-
"build_drawing",
|
|
55
|
-
"choose_scale",
|
|
56
|
-
"dedup_diams",
|
|
57
|
-
"extract_pmi",
|
|
58
|
-
"fix_svg_page_size",
|
|
59
|
-
"generate_script",
|
|
60
|
-
"lint_feature_coverage",
|
|
61
|
-
"make_drawing",
|
|
62
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf
RENAMED
|
File without changes
|
|
File without changes
|
{draftwright-0.2.1 → draftwright-0.2.2}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|