polyhedral 0.3.0__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.
- polyhedral-0.3.0/CHANGELOG.md +12 -0
- polyhedral-0.3.0/LICENSE +21 -0
- polyhedral-0.3.0/MANIFEST.in +12 -0
- polyhedral-0.3.0/PKG-INFO +231 -0
- polyhedral-0.3.0/README.md +191 -0
- polyhedral-0.3.0/docs/DXF_EXPORT_SPEC.md +64 -0
- polyhedral-0.3.0/docs/INTERNALS.md +162 -0
- polyhedral-0.3.0/docs/SPEC.md +416 -0
- polyhedral-0.3.0/package-lock.json +52 -0
- polyhedral-0.3.0/package.json +5 -0
- polyhedral-0.3.0/pyproject.toml +82 -0
- polyhedral-0.3.0/setup.cfg +4 -0
- polyhedral-0.3.0/src/polyhedral/__init__.py +107 -0
- polyhedral-0.3.0/src/polyhedral/bool3d.py +445 -0
- polyhedral-0.3.0/src/polyhedral/camera.py +237 -0
- polyhedral-0.3.0/src/polyhedral/clip2d.py +335 -0
- polyhedral-0.3.0/src/polyhedral/core.py +1518 -0
- polyhedral-0.3.0/src/polyhedral/looks.py +328 -0
- polyhedral-0.3.0/src/polyhedral/make.py +665 -0
- polyhedral-0.3.0/src/polyhedral/mesh.py +647 -0
- polyhedral-0.3.0/src/polyhedral/py.typed +0 -0
- polyhedral-0.3.0/src/polyhedral/shapes.py +140 -0
- polyhedral-0.3.0/src/polyhedral/sheet.py +1290 -0
- polyhedral-0.3.0/src/polyhedral/validate.py +260 -0
- polyhedral-0.3.0/src/polyhedral.egg-info/PKG-INFO +231 -0
- polyhedral-0.3.0/src/polyhedral.egg-info/SOURCES.txt +82 -0
- polyhedral-0.3.0/src/polyhedral.egg-info/dependency_links.txt +1 -0
- polyhedral-0.3.0/src/polyhedral.egg-info/requires.txt +17 -0
- polyhedral-0.3.0/src/polyhedral.egg-info/top_level.txt +1 -0
- polyhedral-0.3.0/tests/conftest.py +45 -0
- polyhedral-0.3.0/tests/scenes.py +40 -0
- polyhedral-0.3.0/tests/test_bool_laws.py +121 -0
- polyhedral-0.3.0/tests/test_camera_looks.py +742 -0
- polyhedral-0.3.0/tests/test_clash.py +96 -0
- polyhedral-0.3.0/tests/test_drawing_oracle.py +112 -0
- polyhedral-0.3.0/tests/test_engine_regressions.py +289 -0
- polyhedral-0.3.0/tests/test_export_units.py +164 -0
- polyhedral-0.3.0/tests/test_gauntlet.py +239 -0
- polyhedral-0.3.0/tests/test_invariants.py +165 -0
- polyhedral-0.3.0/tests/test_mesh_roundtrip.py +49 -0
- polyhedral-0.3.0/tests/test_plot_svg.py +269 -0
- polyhedral-0.3.0/tests/test_primitives.py +96 -0
- polyhedral-0.3.0/tests/test_pythonic.py +584 -0
- polyhedral-0.3.0/tests/test_reader_caps.py +73 -0
- polyhedral-0.3.0/tests/test_readme.py +30 -0
- polyhedral-0.3.0/tests/test_render_pixels.py +176 -0
- polyhedral-0.3.0/tests/test_shade.py +133 -0
- polyhedral-0.3.0/tests/test_sheet.py +247 -0
- polyhedral-0.3.0/tests/test_surface.py +157 -0
- polyhedral-0.3.0/tests/test_sweep.py +111 -0
- polyhedral-0.3.0/tests/test_visual_smoke.py +21 -0
- polyhedral-0.3.0/tests/visual/f_conn_elev_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_elev_nohid.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_elev_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_elev_wire.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_iso_highlight.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_iso_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_iso_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_iso_xray_conc.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_iso_xray_full.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_plan_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_plan_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_sec_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_sec_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_secx_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_conn_secx_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_crank_iso_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_crank_iso_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_crank_plan_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_crank_plan_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_fused_iso_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_fused_iso_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_pipe_elev_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_pipe_elev_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_pipe_iso_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_pipe_iso_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_ubar_iso_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_ubar_iso_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/f_ubar_side_line.png +0 -0
- polyhedral-0.3.0/tests/visual/f_ubar_side_shade.png +0 -0
- polyhedral-0.3.0/tests/visual/generate_visuals.py +172 -0
- polyhedral-0.3.0/tools/bench.py +69 -0
- polyhedral-0.3.0/tools/pyodide_gate.mjs +91 -0
- polyhedral-0.3.0/typings/pyclipper.pyi +6 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.3.0] - 2026-09-02
|
|
11
|
+
|
|
12
|
+
First public release. See docs/SPEC.md for the API.
|
polyhedral-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wuttiwong Banjongwattana
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# sdist = the full source archive: everything needed to build, test and
|
|
2
|
+
# understand the project. The WHEEL stays lean (docs deliberately not
|
|
3
|
+
# shipped; README travels in METADATA).
|
|
4
|
+
graft tests
|
|
5
|
+
recursive-include docs *.md
|
|
6
|
+
recursive-include tools *
|
|
7
|
+
recursive-include typings *.pyi
|
|
8
|
+
include CHANGELOG.md
|
|
9
|
+
include package.json
|
|
10
|
+
include package-lock.json
|
|
11
|
+
prune tests/visual/__pycache__
|
|
12
|
+
global-exclude __pycache__ *.py[cod] .coverage
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polyhedral
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Planar-facet solid modeling with self-verified hidden-line and shaded drawings, DXF/SVG export (pure Python, Pyodide-ready)
|
|
5
|
+
Author-email: Wuttiwong Banjongwattana <banjongwattana.w@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: cad,b-rep,solid-modeling,hidden-line,dxf,svg,engineering-drawing,pyodide
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Intended Audience :: Manufacturing
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.21
|
|
25
|
+
Requires-Dist: shapely>=2.0
|
|
26
|
+
Requires-Dist: pyclipper>=1.3
|
|
27
|
+
Provides-Extra: dxf
|
|
28
|
+
Requires-Dist: ezdxf>=1.1; extra == "dxf"
|
|
29
|
+
Requires-Dist: Pillow; extra == "dxf"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: ruff; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest; extra == "dev"
|
|
34
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
35
|
+
Requires-Dist: ezdxf>=1.1; extra == "dev"
|
|
36
|
+
Requires-Dist: Pillow; extra == "dev"
|
|
37
|
+
Requires-Dist: cairosvg; extra == "dev"
|
|
38
|
+
Requires-Dist: build; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# polyhedral
|
|
42
|
+
|
|
43
|
+
**Solid modeling + engineering drawings — pure Python**
|
|
44
|
+
|
|
45
|
+
Planar-facet solids (B-rep), n-ary booleans, and self-verified 2D
|
|
46
|
+
drawing geometry — hidden lines, sections, shaded views — written into
|
|
47
|
+
[ezdxf](https://ezdxf.mozman.at) documents or rendered to SVG (the SVG
|
|
48
|
+
is always the DXF, rendered). Runs anywhere
|
|
49
|
+
`numpy` + `shapely` + `pyclipper` do, including Pyodide in the browser.
|
|
50
|
+
Fully typed (`py.typed`).
|
|
51
|
+
|
|
52
|
+
Two rules carry the whole design: **model space is millimetres**, and
|
|
53
|
+
**everything exports at true size (1:1)** — you pick the file's unit at
|
|
54
|
+
export; geometry is never rescaled in model space.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
pip install polyhedral # core: numpy, shapely, pyclipper
|
|
58
|
+
pip install polyhedral[dxf] # + ezdxf and Pillow, for DXF/SVG output
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Naming follows shapely (`.area`, `.bounds`, `.is_valid` — properties,
|
|
62
|
+
not methods); booleans have one spelling. The normative contract is
|
|
63
|
+
`docs/SPEC.md`, shipped in the source distribution on PyPI.
|
|
64
|
+
|
|
65
|
+
## From zero to a dimensioned drawing
|
|
66
|
+
|
|
67
|
+
Every code block below runs as written, in order, as one script —
|
|
68
|
+
enforced by the test suite.
|
|
69
|
+
|
|
70
|
+
**Model, then verify.** Constructors take geometry positionally,
|
|
71
|
+
everything else keyword-only, with `pid=` (the part's name) among them:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from polyhedral import make
|
|
75
|
+
|
|
76
|
+
plate = make.box((450, 450, 25), center=(0, 0, 12.5), pid="PL-01")
|
|
77
|
+
assert plate.volume == 5062500.0 and plate.is_valid
|
|
78
|
+
assert plate.check() == [] # every face planar, watertight, normals out
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`check()` returns issue records and `[]` means usable; call it after
|
|
82
|
+
construction and after booleans — a non-empty list means later results
|
|
83
|
+
cannot be trusted.
|
|
84
|
+
|
|
85
|
+
**Drill.** Circular sizes are **radii** (a Ø30 hole is `r=15`; passing
|
|
86
|
+
`d=` raises a TypeError that names the fix). Booleans are three n-ary
|
|
87
|
+
functions and nothing else — typing `a - b` or `a.difference(b)` raises
|
|
88
|
+
an error naming the function to call. A boolean result is a new part
|
|
89
|
+
(name it with `pid=`); one that removes everything returns an empty,
|
|
90
|
+
falsy Solid:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from polyhedral import make, subtract
|
|
94
|
+
|
|
95
|
+
holes = [make.cylinder(r=15, h=60, center=(x, y, 12.5))
|
|
96
|
+
for x in (-175, 175) for y in (-175, 175)]
|
|
97
|
+
drilled = subtract(plate, *holes, pid="PL-01")
|
|
98
|
+
assert drilled and drilled.check() == []
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**A shaded figure.** A report figure *is* a one-viewport sheet. `style=`
|
|
102
|
+
picks the pipeline (`Linework()` hidden-line drafting — the default —
|
|
103
|
+
or `Shaded()`); `look=` is appearance — the library ships no colors;
|
|
104
|
+
bring a small `by_kind` of your own:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from polyhedral import Look, Plot, Shaded, Sheet, View, Viewport, by_kind
|
|
108
|
+
|
|
109
|
+
my_colors = by_kind({"steel": Look(color=(122, 144, 168))}, default=Look())
|
|
110
|
+
iso = View.from_eye((1, -1, 1), name="ISO")
|
|
111
|
+
fig = Sheet([drilled], [Viewport(iso, at=(0, 0), style=Shaded())],
|
|
112
|
+
look=my_colors)
|
|
113
|
+
svg = fig.to_svg(plot=Plot(paper=(160, 120)))
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`Plot` is a CAD plot dialog as a value: paper (`A0`…`A6` constants or
|
|
117
|
+
any custom `(w, h)` mm), plot area (`window=`), and plot scale 1:scale
|
|
118
|
+
(`5` = 1:5, `0.5` = 2:1; `None` fits inside the margin).
|
|
119
|
+
|
|
120
|
+
**A sheet, for CAD.** `View.from_eye(eye)` or `from_direction(gaze)`
|
|
121
|
+
build the camera; `Viewport(view, at=…)` places the model origin's
|
|
122
|
+
projection at `at`; `row()` lays out a row of aligned views:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from polyhedral import Sheet, row
|
|
126
|
+
|
|
127
|
+
plan = View.from_direction((0, 0, -1), name="PLAN")
|
|
128
|
+
elev = View.from_direction((0, 1, 0), name="ELEV", title="ELEVATION A-A")
|
|
129
|
+
sheet = Sheet([drilled], row([plan, elev], [drilled], gap=200.0))
|
|
130
|
+
sheet.to_dxf("plate.dxf") # units="m"/"cm"/"in"/"ft" convert, true size
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Output is ISO 128: layers `VISIBLE/HIDDEN/CUT/HATCH` with real pens
|
|
134
|
+
(0.35/0.18 mm) and ISO dashes, everything ByLayer (shaded
|
|
135
|
+
output adds `SHADE`/`EDGES` carrying truecolor, transparency, and
|
|
136
|
+
per-entity lineweight), layer `0` empty.
|
|
137
|
+
|
|
138
|
+
**Dimensions are ezdxf code — yours.** polyhedral computes the
|
|
139
|
+
drawing *geometry*; annotation is ordinary ezdxf code on the returned
|
|
140
|
+
document. One formula maps model to sheet —
|
|
141
|
+
`sheet_pt = at + view.project_pt(p)`, available as `Viewport.pt()` —
|
|
142
|
+
and geometry is 1:1, so dimensions measure true millimetres with no
|
|
143
|
+
correction factor (in mm files; other units carry the factor below):
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
doc = sheet.to_dxf_doc()
|
|
147
|
+
vp = sheet.viewports[0]
|
|
148
|
+
doc.modelspace().add_aligned_dim(
|
|
149
|
+
p1=vp.pt((-225, -225, 0)), p2=vp.pt((225, -225, 0)),
|
|
150
|
+
distance=-60).render() # measures 450: the real size
|
|
151
|
+
doc.saveas("plate.dxf")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
When plotting at 1:S, polyhedral stamps its own dash/hatch styling per
|
|
155
|
+
object (supply S as `to_dxf(…, plot_scale=S)`, or `Plot(scale=S)`
|
|
156
|
+
at plot time). Annotation is ezdxf's business, and its bases — dim
|
|
157
|
+
text and arrows, leaders, text heights, hatch patterns — are all
|
|
158
|
+
in millimetres, so one rule covers everything: **multiply by S,
|
|
159
|
+
and by the unit factor when the file is not mm** (`units="m"` →
|
|
160
|
+
× 0.001, so 1:20 in metres is `dimscale=0.02`, not 20; same factor
|
|
161
|
+
on `vp.pt()` coordinates; `dimlfac=1000` keeps dimension figures in mm —
|
|
162
|
+
it converts the measured value, not the sizes). Carry it in one *new*
|
|
163
|
+
dimstyle per scale, named after it —
|
|
164
|
+
`doc.dimstyles.new("S5", dxfattribs={"dimscale": 5})` —
|
|
165
|
+
never a repurposed `Standard`, never the `$DIMSCALE` header. The
|
|
166
|
+
same rule already runs polyhedral's own styling — hidden-line
|
|
167
|
+
`ltscale` carries pen × S × unit factor per object and hatch scale
|
|
168
|
+
its 1.0 base × S × unit factor, while the linetype definition
|
|
169
|
+
stays AutoCAD's metric default verbatim and hatches keep `ANSI31`,
|
|
170
|
+
scale per entity. `plot_svg(doc, plot=…)` then renders that same document —
|
|
171
|
+
the DXF you save and the SVG you show cannot disagree.
|
|
172
|
+
|
|
173
|
+
**Sections.** `Section(n, d)` keeps the half-space `n·p <= d`; the
|
|
174
|
+
camera must look back into the cut (`D·n < 0` — get it backwards and
|
|
175
|
+
the error names the exact `D` to flip to). Cut faces land on
|
|
176
|
+
`CUT`/`HATCH` with the source part id as XDATA; `exclude=("bolt",)`
|
|
177
|
+
passes kinds through un-sectioned:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from polyhedral import Section
|
|
181
|
+
|
|
182
|
+
sec = View.from_direction((0, -1, 0), name="SEC", title="SECTION A-A",
|
|
183
|
+
cut=Section((0, 1, 0), -175.0))
|
|
184
|
+
Sheet([drilled], [Viewport(sec, at=(0, 0))]).to_dxf("section.dxf")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Beyond the walkthrough
|
|
188
|
+
|
|
189
|
+
* **Primitives**: `make.box wedge cylinder tube cone sphere torus`.
|
|
190
|
+
* **Profiles**: `shapes.rect circle hexagon ring isection channel tee
|
|
191
|
+
angle cruciform rhs chs` — shapely Polygons (the class is re-exported
|
|
192
|
+
as `polyhedral.Polygon`), so a custom profile is just
|
|
193
|
+
`Polygon([...])`, holes included. Then `make.extrude(profile, vec)`
|
|
194
|
+
(`vec` is the extrusion vector, direction *and* length;
|
|
195
|
+
`origin=`/`ex=`/`ey=` set the workplane),
|
|
196
|
+
`make.revolve(profile, angle=, n=)`,
|
|
197
|
+
`make.sweep(profile, path, closed=)` — corners are
|
|
198
|
+
miters, curves are sampled points you supply.
|
|
199
|
+
* **Raw faces**: `Solid.from_polyhedron(verts, faces)` — winding is
|
|
200
|
+
fixed up automatically.
|
|
201
|
+
* **Transforms & queries**: `translate rotate scale mirror transform`;
|
|
202
|
+
`volume area centroid bounds`; `a.clashes(b)` returns the
|
|
203
|
+
interference volume (0.0 is falsy).
|
|
204
|
+
* **Details**: `Viewport(view, at=…, crop=…, parts=[…])` is a cropped
|
|
205
|
+
detail at 1:1 with occlusion recomputed for the subset; enlarge it at
|
|
206
|
+
plot time with `Plot(window=vp.window(), scale=0.5)`, never by
|
|
207
|
+
scaling data. `highlight(("PL-01",), base=my_colors)` accents listed
|
|
208
|
+
parts and ghosts the rest.
|
|
209
|
+
* **Meshes**: `mesh.write_stl / write_obj / write_3mf(s, path,
|
|
210
|
+
units=)` and `read_stl / read_3mf`; `from_mesh` merges coplanar
|
|
211
|
+
triangles, nests hole loops, welds vertices, and guesses smooth
|
|
212
|
+
groups — otherwise every triangle diagonal would be drawn.
|
|
213
|
+
* **Self-verification**: `validate.compare(parts, view)` scores the
|
|
214
|
+
line work against an independent z-buffer that shares no occlusion
|
|
215
|
+
code with the engine.
|
|
216
|
+
|
|
217
|
+
## Reliability
|
|
218
|
+
|
|
219
|
+
`union/subtract/intersect` satisfy all eight boolean-algebra laws
|
|
220
|
+
(inclusion-exclusion through rigid-motion invariance) on randomized
|
|
221
|
+
pairs **including coplanar-face contacts**, enforced by zero-failure
|
|
222
|
+
test baselines. Sections are never taken exactly at a face plane,
|
|
223
|
+
and coincident faces resolve by a fixed ownership rule.
|
|
224
|
+
|
|
225
|
+
## Limitations
|
|
226
|
+
|
|
227
|
+
* Curved surfaces are faceted — no NURBS. `smooth_groups` marks which
|
|
228
|
+
seams are not sharp creases (`make.*` sets them automatically;
|
|
229
|
+
`make.extrude` takes `smooth_rings=` for your own profiles).
|
|
230
|
+
* No `fillet` `chamfer` `shell` `offset`.
|
|
231
|
+
* No STEP / IGES.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# polyhedral
|
|
2
|
+
|
|
3
|
+
**Solid modeling + engineering drawings — pure Python**
|
|
4
|
+
|
|
5
|
+
Planar-facet solids (B-rep), n-ary booleans, and self-verified 2D
|
|
6
|
+
drawing geometry — hidden lines, sections, shaded views — written into
|
|
7
|
+
[ezdxf](https://ezdxf.mozman.at) documents or rendered to SVG (the SVG
|
|
8
|
+
is always the DXF, rendered). Runs anywhere
|
|
9
|
+
`numpy` + `shapely` + `pyclipper` do, including Pyodide in the browser.
|
|
10
|
+
Fully typed (`py.typed`).
|
|
11
|
+
|
|
12
|
+
Two rules carry the whole design: **model space is millimetres**, and
|
|
13
|
+
**everything exports at true size (1:1)** — you pick the file's unit at
|
|
14
|
+
export; geometry is never rescaled in model space.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pip install polyhedral # core: numpy, shapely, pyclipper
|
|
18
|
+
pip install polyhedral[dxf] # + ezdxf and Pillow, for DXF/SVG output
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Naming follows shapely (`.area`, `.bounds`, `.is_valid` — properties,
|
|
22
|
+
not methods); booleans have one spelling. The normative contract is
|
|
23
|
+
`docs/SPEC.md`, shipped in the source distribution on PyPI.
|
|
24
|
+
|
|
25
|
+
## From zero to a dimensioned drawing
|
|
26
|
+
|
|
27
|
+
Every code block below runs as written, in order, as one script —
|
|
28
|
+
enforced by the test suite.
|
|
29
|
+
|
|
30
|
+
**Model, then verify.** Constructors take geometry positionally,
|
|
31
|
+
everything else keyword-only, with `pid=` (the part's name) among them:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from polyhedral import make
|
|
35
|
+
|
|
36
|
+
plate = make.box((450, 450, 25), center=(0, 0, 12.5), pid="PL-01")
|
|
37
|
+
assert plate.volume == 5062500.0 and plate.is_valid
|
|
38
|
+
assert plate.check() == [] # every face planar, watertight, normals out
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`check()` returns issue records and `[]` means usable; call it after
|
|
42
|
+
construction and after booleans — a non-empty list means later results
|
|
43
|
+
cannot be trusted.
|
|
44
|
+
|
|
45
|
+
**Drill.** Circular sizes are **radii** (a Ø30 hole is `r=15`; passing
|
|
46
|
+
`d=` raises a TypeError that names the fix). Booleans are three n-ary
|
|
47
|
+
functions and nothing else — typing `a - b` or `a.difference(b)` raises
|
|
48
|
+
an error naming the function to call. A boolean result is a new part
|
|
49
|
+
(name it with `pid=`); one that removes everything returns an empty,
|
|
50
|
+
falsy Solid:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from polyhedral import make, subtract
|
|
54
|
+
|
|
55
|
+
holes = [make.cylinder(r=15, h=60, center=(x, y, 12.5))
|
|
56
|
+
for x in (-175, 175) for y in (-175, 175)]
|
|
57
|
+
drilled = subtract(plate, *holes, pid="PL-01")
|
|
58
|
+
assert drilled and drilled.check() == []
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**A shaded figure.** A report figure *is* a one-viewport sheet. `style=`
|
|
62
|
+
picks the pipeline (`Linework()` hidden-line drafting — the default —
|
|
63
|
+
or `Shaded()`); `look=` is appearance — the library ships no colors;
|
|
64
|
+
bring a small `by_kind` of your own:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from polyhedral import Look, Plot, Shaded, Sheet, View, Viewport, by_kind
|
|
68
|
+
|
|
69
|
+
my_colors = by_kind({"steel": Look(color=(122, 144, 168))}, default=Look())
|
|
70
|
+
iso = View.from_eye((1, -1, 1), name="ISO")
|
|
71
|
+
fig = Sheet([drilled], [Viewport(iso, at=(0, 0), style=Shaded())],
|
|
72
|
+
look=my_colors)
|
|
73
|
+
svg = fig.to_svg(plot=Plot(paper=(160, 120)))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`Plot` is a CAD plot dialog as a value: paper (`A0`…`A6` constants or
|
|
77
|
+
any custom `(w, h)` mm), plot area (`window=`), and plot scale 1:scale
|
|
78
|
+
(`5` = 1:5, `0.5` = 2:1; `None` fits inside the margin).
|
|
79
|
+
|
|
80
|
+
**A sheet, for CAD.** `View.from_eye(eye)` or `from_direction(gaze)`
|
|
81
|
+
build the camera; `Viewport(view, at=…)` places the model origin's
|
|
82
|
+
projection at `at`; `row()` lays out a row of aligned views:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from polyhedral import Sheet, row
|
|
86
|
+
|
|
87
|
+
plan = View.from_direction((0, 0, -1), name="PLAN")
|
|
88
|
+
elev = View.from_direction((0, 1, 0), name="ELEV", title="ELEVATION A-A")
|
|
89
|
+
sheet = Sheet([drilled], row([plan, elev], [drilled], gap=200.0))
|
|
90
|
+
sheet.to_dxf("plate.dxf") # units="m"/"cm"/"in"/"ft" convert, true size
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Output is ISO 128: layers `VISIBLE/HIDDEN/CUT/HATCH` with real pens
|
|
94
|
+
(0.35/0.18 mm) and ISO dashes, everything ByLayer (shaded
|
|
95
|
+
output adds `SHADE`/`EDGES` carrying truecolor, transparency, and
|
|
96
|
+
per-entity lineweight), layer `0` empty.
|
|
97
|
+
|
|
98
|
+
**Dimensions are ezdxf code — yours.** polyhedral computes the
|
|
99
|
+
drawing *geometry*; annotation is ordinary ezdxf code on the returned
|
|
100
|
+
document. One formula maps model to sheet —
|
|
101
|
+
`sheet_pt = at + view.project_pt(p)`, available as `Viewport.pt()` —
|
|
102
|
+
and geometry is 1:1, so dimensions measure true millimetres with no
|
|
103
|
+
correction factor (in mm files; other units carry the factor below):
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
doc = sheet.to_dxf_doc()
|
|
107
|
+
vp = sheet.viewports[0]
|
|
108
|
+
doc.modelspace().add_aligned_dim(
|
|
109
|
+
p1=vp.pt((-225, -225, 0)), p2=vp.pt((225, -225, 0)),
|
|
110
|
+
distance=-60).render() # measures 450: the real size
|
|
111
|
+
doc.saveas("plate.dxf")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
When plotting at 1:S, polyhedral stamps its own dash/hatch styling per
|
|
115
|
+
object (supply S as `to_dxf(…, plot_scale=S)`, or `Plot(scale=S)`
|
|
116
|
+
at plot time). Annotation is ezdxf's business, and its bases — dim
|
|
117
|
+
text and arrows, leaders, text heights, hatch patterns — are all
|
|
118
|
+
in millimetres, so one rule covers everything: **multiply by S,
|
|
119
|
+
and by the unit factor when the file is not mm** (`units="m"` →
|
|
120
|
+
× 0.001, so 1:20 in metres is `dimscale=0.02`, not 20; same factor
|
|
121
|
+
on `vp.pt()` coordinates; `dimlfac=1000` keeps dimension figures in mm —
|
|
122
|
+
it converts the measured value, not the sizes). Carry it in one *new*
|
|
123
|
+
dimstyle per scale, named after it —
|
|
124
|
+
`doc.dimstyles.new("S5", dxfattribs={"dimscale": 5})` —
|
|
125
|
+
never a repurposed `Standard`, never the `$DIMSCALE` header. The
|
|
126
|
+
same rule already runs polyhedral's own styling — hidden-line
|
|
127
|
+
`ltscale` carries pen × S × unit factor per object and hatch scale
|
|
128
|
+
its 1.0 base × S × unit factor, while the linetype definition
|
|
129
|
+
stays AutoCAD's metric default verbatim and hatches keep `ANSI31`,
|
|
130
|
+
scale per entity. `plot_svg(doc, plot=…)` then renders that same document —
|
|
131
|
+
the DXF you save and the SVG you show cannot disagree.
|
|
132
|
+
|
|
133
|
+
**Sections.** `Section(n, d)` keeps the half-space `n·p <= d`; the
|
|
134
|
+
camera must look back into the cut (`D·n < 0` — get it backwards and
|
|
135
|
+
the error names the exact `D` to flip to). Cut faces land on
|
|
136
|
+
`CUT`/`HATCH` with the source part id as XDATA; `exclude=("bolt",)`
|
|
137
|
+
passes kinds through un-sectioned:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from polyhedral import Section
|
|
141
|
+
|
|
142
|
+
sec = View.from_direction((0, -1, 0), name="SEC", title="SECTION A-A",
|
|
143
|
+
cut=Section((0, 1, 0), -175.0))
|
|
144
|
+
Sheet([drilled], [Viewport(sec, at=(0, 0))]).to_dxf("section.dxf")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Beyond the walkthrough
|
|
148
|
+
|
|
149
|
+
* **Primitives**: `make.box wedge cylinder tube cone sphere torus`.
|
|
150
|
+
* **Profiles**: `shapes.rect circle hexagon ring isection channel tee
|
|
151
|
+
angle cruciform rhs chs` — shapely Polygons (the class is re-exported
|
|
152
|
+
as `polyhedral.Polygon`), so a custom profile is just
|
|
153
|
+
`Polygon([...])`, holes included. Then `make.extrude(profile, vec)`
|
|
154
|
+
(`vec` is the extrusion vector, direction *and* length;
|
|
155
|
+
`origin=`/`ex=`/`ey=` set the workplane),
|
|
156
|
+
`make.revolve(profile, angle=, n=)`,
|
|
157
|
+
`make.sweep(profile, path, closed=)` — corners are
|
|
158
|
+
miters, curves are sampled points you supply.
|
|
159
|
+
* **Raw faces**: `Solid.from_polyhedron(verts, faces)` — winding is
|
|
160
|
+
fixed up automatically.
|
|
161
|
+
* **Transforms & queries**: `translate rotate scale mirror transform`;
|
|
162
|
+
`volume area centroid bounds`; `a.clashes(b)` returns the
|
|
163
|
+
interference volume (0.0 is falsy).
|
|
164
|
+
* **Details**: `Viewport(view, at=…, crop=…, parts=[…])` is a cropped
|
|
165
|
+
detail at 1:1 with occlusion recomputed for the subset; enlarge it at
|
|
166
|
+
plot time with `Plot(window=vp.window(), scale=0.5)`, never by
|
|
167
|
+
scaling data. `highlight(("PL-01",), base=my_colors)` accents listed
|
|
168
|
+
parts and ghosts the rest.
|
|
169
|
+
* **Meshes**: `mesh.write_stl / write_obj / write_3mf(s, path,
|
|
170
|
+
units=)` and `read_stl / read_3mf`; `from_mesh` merges coplanar
|
|
171
|
+
triangles, nests hole loops, welds vertices, and guesses smooth
|
|
172
|
+
groups — otherwise every triangle diagonal would be drawn.
|
|
173
|
+
* **Self-verification**: `validate.compare(parts, view)` scores the
|
|
174
|
+
line work against an independent z-buffer that shares no occlusion
|
|
175
|
+
code with the engine.
|
|
176
|
+
|
|
177
|
+
## Reliability
|
|
178
|
+
|
|
179
|
+
`union/subtract/intersect` satisfy all eight boolean-algebra laws
|
|
180
|
+
(inclusion-exclusion through rigid-motion invariance) on randomized
|
|
181
|
+
pairs **including coplanar-face contacts**, enforced by zero-failure
|
|
182
|
+
test baselines. Sections are never taken exactly at a face plane,
|
|
183
|
+
and coincident faces resolve by a fixed ownership rule.
|
|
184
|
+
|
|
185
|
+
## Limitations
|
|
186
|
+
|
|
187
|
+
* Curved surfaces are faceted — no NURBS. `smooth_groups` marks which
|
|
188
|
+
seams are not sharp creases (`make.*` sets them automatically;
|
|
189
|
+
`make.extrude` takes `smooth_rings=` for your own profiles).
|
|
190
|
+
* No `fillet` `chamfer` `shell` `offset`.
|
|
191
|
+
* No STEP / IGES.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# DXF export format
|
|
2
|
+
|
|
3
|
+
Normative for `Sheet.to_dxf_doc`. All geometry is written to
|
|
4
|
+
modelspace at true size (1:1) — nothing is ever rescaled; display and
|
|
5
|
+
plot scale are the viewer's job. `units=` ("mm" default, cm/m/in/ft)
|
|
6
|
+
converts coordinates and, on fresh documents, sets `$INSUNITS`
|
|
7
|
+
(4/5/6/1/2); the LTYPE pattern stays acadiso.lin verbatim, and
|
|
8
|
+
per-object `ltscale` / hatch pattern scale carry the file-unit
|
|
9
|
+
factor (see Rules), so real ISO lengths hold in every unit.
|
|
10
|
+
Lineweights are absolute (0.01 mm units) per the DXF spec.
|
|
11
|
+
`$LTSCALE = 1` on fresh documents only.
|
|
12
|
+
|
|
13
|
+
Fresh documents: R2010, `$INSUNITS` per `units=`, `$MEASUREMENT` 1,
|
|
14
|
+
`$LTSCALE` 1.0. Templates are used as found: header untouched, existing
|
|
15
|
+
layers and linetype definitions win.
|
|
16
|
+
|
|
17
|
+
## Layers (created on demand)
|
|
18
|
+
|
|
19
|
+
| layer | linetype | lw | ACI | entities |
|
|
20
|
+
|---|---|---|---|---|
|
|
21
|
+
| `VISIBLE` | CONTINUOUS | 35 | 7 | LWPOLYLINE |
|
|
22
|
+
| `HIDDEN` | `ACAD_ISO02W100` = `[15, 12, -3]` | 18 | 8 | LWPOLYLINE, `ltscale = 0.18 × plot_scale × file-unit factor` |
|
|
23
|
+
| `CUT` | CONTINUOUS | 35 | 7 | LWPOLYLINE |
|
|
24
|
+
| `HATCH` | CONTINUOUS | 18 | 8 | HATCH `ANSI31` 45° (metric, 3.175 mm), pattern scale = 1.0 × plot_scale × file-unit factor (3.175 mm spacing in a mm file at 1:1) |
|
|
25
|
+
| `SHADE` | CONTINUOUS | default | 7 | HATCH solid truecolor fill + transparency (Shaded faces) |
|
|
26
|
+
| `EDGES` | CONTINUOUS | 35 | 7 | LWPOLYLINE truecolor (Shaded edge overlay; each entity carries `Shaded.edge_width` × 100 snapped to the standard DXF lineweight set) |
|
|
27
|
+
|
|
28
|
+
## Rules
|
|
29
|
+
|
|
30
|
+
* Two pens (ISO 128-2): 0.35 visible/cut/edges, 0.18 hidden/hatch.
|
|
31
|
+
Colors 7/8 are for screen legibility only — printed grading comes from
|
|
32
|
+
true lineweights, never color tables.
|
|
33
|
+
* Everything in modelspace; layer `0` never carries content; every
|
|
34
|
+
entity is ByLayer (Shaded truecolors, the edge overlay's per-object
|
|
35
|
+
lineweight, and per-object `ltscale` are the exceptions). The LTYPE
|
|
36
|
+
table carries acadiso.lin verbatim in every export unit; ISO 128
|
|
37
|
+
W100 patterns are defined at 1 mm pen width, so every dashed object
|
|
38
|
+
carries `ltscale = pen width × plot_scale × file-unit factor` —
|
|
39
|
+
dashes print at their pen-proportional ISO size at that plot scale
|
|
40
|
+
with `$LTSCALE = 1` untouched. Styled objects carry their base in
|
|
41
|
+
XDATA appid `POLYHEDRAL_PLOT` (`lt:` / `hatch:` bases, in file
|
|
42
|
+
units), which is how `plot_svg` restamps exactly these objects —
|
|
43
|
+
and only these — at the resolved plot scale. The mechanism is
|
|
44
|
+
pattern-agnostic and the base in the mark is the density knob: to
|
|
45
|
+
restyle a section hatch, `set_pattern_fill("AR-CONC", scale=...)`
|
|
46
|
+
on the entity, re-set `h.dxf.hatch_style = 0` and
|
|
47
|
+
`h.dxf.color = 256` (the fill call silently resets them to 1 and
|
|
48
|
+
7), and rewrite its mark to `hatch:<your base, in file units>` —
|
|
49
|
+
restamping keeps working at every plot scale.
|
|
50
|
+
* Every HATCH carries `hatch_style` 0 (nested / odd parity) so interior
|
|
51
|
+
rings stay holes in every renderer.
|
|
52
|
+
* `Sheet(layer=fn)`: `fn(role, kind) -> name` maps roles to any office
|
|
53
|
+
scheme; a layer created for the hidden role gets `ACAD_ISO02W100`
|
|
54
|
+
whatever the hook names it. `kind` is non-empty for section content
|
|
55
|
+
only.
|
|
56
|
+
* Section content (CUT outlines + HATCH fills) carries XDATA appid
|
|
57
|
+
`POLYHEDRAL`: `(1000, pid), (1000, kind)`. Merged visible/hidden line
|
|
58
|
+
work carries no per-part identity.
|
|
59
|
+
* `doc.audit()` on any output: zero errors, zero fixes.
|
|
60
|
+
|
|
61
|
+
polyhedral writes no text or dimensions — annotation is the caller's
|
|
62
|
+
ezdxf code (SPEC §9). Geometry is 1:1, so dimensions measure true
|
|
63
|
+
size; in a mm file that is millimetres directly, in other units apply
|
|
64
|
+
the annotation rule in SPEC §9.
|