solidsmith 0.2.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scott Shamansky
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,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: solidsmith
3
+ Version: 0.2.0
4
+ Summary: Forge watertight, print-ready 3D parts from Python: robust booleans, colored 3MF export, and multi-view previews.
5
+ Author: Scott Shamansky
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/scottshamansky/solidsmith
8
+ Project-URL: Issues, https://github.com/scottshamansky/solidsmith/issues
9
+ Keywords: 3d-printing,parametric,stl,3mf,trimesh,cad,maker
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Manufacturing
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy>=1.24
20
+ Requires-Dist: scipy>=1.10
21
+ Requires-Dist: trimesh>=4.10
22
+ Requires-Dist: manifold3d>=2.5
23
+ Requires-Dist: matplotlib>=3.7
24
+ Requires-Dist: networkx>=2.8
25
+ Requires-Dist: lxml>=4.9
26
+ Requires-Dist: scikit-image>=0.22
27
+ Requires-Dist: rtree>=1.0
28
+ Dynamic: license-file
29
+
30
+ # solidsmith
31
+
32
+ [![CI](https://github.com/scottshamansky/solidsmith/actions/workflows/ci.yml/badge.svg)](https://github.com/scottshamansky/solidsmith/actions/workflows/ci.yml)
33
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
34
+ ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)
35
+
36
+ **Forge watertight, print-ready 3D parts from Python.**
37
+
38
+ solidsmith is the missing glue between [trimesh](https://trimesh.org) and your
39
+ printer: booleans that don't fall over, 3MF export that keeps its colors, a
40
+ printability report, and quick multi-view renders to iterate against — the
41
+ scaffolding every scripted-CAD project ends up reinventing, packaged once.
42
+
43
+ ![parametric honeycomb bin rendered from three angles](docs/images/honeycomb_bin.png)
44
+
45
+ ## Why script your models?
46
+
47
+ Parametric code beats mouse-driven CAD when you make things repeatedly:
48
+
49
+ - **Every dimension is a variable.** Resize a bin, thicken a wall, or regenerate
50
+ a whole family of sizes by editing constants, not dragging faces.
51
+ - **Geometry you authored is geometry you own.** Marketplaces increasingly
52
+ require sellers of printed goods to use their own original designs. A model
53
+ built from your own code is original by construction, and generic patterns
54
+ (honeycombs, gears, chamfers) stay free for everyone.
55
+ - **Designs diff, review, and version like any other code.**
56
+
57
+ ## What trimesh leaves to you (and solidsmith does)
58
+
59
+ | Pain | What solidsmith does |
60
+ | --- | --- |
61
+ | Booleans fail on real-world meshes | `union` / `difference` / `intersection` run the robust manifold engine with mesh hygiene (`clean`) before and after |
62
+ | 3MF export silently drops color | `write_3mf` injects real `<basematerials>` and tags each body, so multi-color models open in the slicer with filaments already assigned |
63
+ | "Will it print?" found out the hard way | `check` reports watertightness per body, bed fit, plate contact, walls too thin for the nozzle, and stray debris bodies — against a named printer profile (`printer="bambu_a1_mini"`) or your own `Printer` |
64
+ | Seeing your part means opening a slicer | `render_views` renders shaded PNGs from named camera angles in about a second, no GUI or GPU |
65
+
66
+ ## Quickstart
67
+
68
+ ```python
69
+ from solidsmith import Part, check, difference, rounded_prism, render_views, write_3mf
70
+
71
+ shell = rounded_prism((80, 50, 30), radius=8)
72
+ cavity = rounded_prism((74, 44, 30), radius=5)
73
+ cavity.apply_translation((0, 0, 3))
74
+ tray = difference(shell, cavity)
75
+
76
+ print(check(tray)) # watertight? fits the bed? on the plate?
77
+ write_3mf(Part(tray, color=(226, 122, 40), name="tray"), "tray.3mf")
78
+ render_views(tray, "tray.png") # shaded views, one PNG
79
+ ```
80
+
81
+ ```
82
+ ✔ watertight (1 body)
83
+ ✔ 80.0 × 50.0 × 30.0 mm on a 256 × 256 × 256 bed (Bambu X1C / P1S)
84
+ ✔ first layer on the plate (z=0)
85
+ 31.0 cm³ · 564 triangles
86
+ ```
87
+
88
+ Multi-color is just more `Part`s — one per filament — exported together:
89
+
90
+ ```python
91
+ write_3mf([Part(body, (30, 120, 220), "body"),
92
+ Part(cap, (240, 200, 40), "cap")], "model.3mf")
93
+ ```
94
+
95
+ ## Examples
96
+
97
+ | Model | Run it |
98
+ | --- | --- |
99
+ | Honeycomb storage bin — vented walls, supports-free label tab, every dimension a parameter | `python examples/honeycomb_bin.py` |
100
+ | Pebble planter — SDF-sculpted organic form, hollow with a drainage hole, split into two filament colors | `python examples/pebble_planter.py` |
101
+ | Hex bit organizer — 16 slide-fit sockets for 1/4" bits, built on `workflow.main` with preview/final modes | `python examples/bit_organizer.py preview` |
102
+ | Two-tone phone stand — angled boolean cuts, a charging-cable channel, one AMS color swap, checked against a named printer profile | `python examples/phone_stand.py preview` |
103
+
104
+ ![two-color SDF pebble planter](docs/images/pebble_planter.png)
105
+
106
+ Each example writes STL + colored 3MF + a rendered preview into `out/` and
107
+ prints its printability report. Add `--fast` while you iterate.
108
+
109
+ ## Install
110
+
111
+ ```bash
112
+ git clone https://github.com/scottshamansky/solidsmith
113
+ cd solidsmith
114
+ pip install -e .
115
+ ```
116
+
117
+ Requires Python 3.9+. Dimensions are millimeters throughout; the default bed
118
+ is a 256 mm cube (Bambu P1/P2/X1 class) and every check takes your own `bed=`.
119
+
120
+ ## Sculpting (SDF)
121
+
122
+ Alongside the hard-edged boolean toolkit, `solidsmith.sdf` models organic
123
+ shapes as blended signed distance fields — functional primitives, smooth
124
+ unions that read as clay fillets, `offset`/`shell` for hollowing, and
125
+ marching-cubes meshing with Taubin smoothing:
126
+
127
+ ```python
128
+ from solidsmith import sdf
129
+
130
+ body = sdf.smooth_union(12, sdf.sphere((0, 0, 30), 25),
131
+ sdf.ellipsoid((0, 18, 22), (20, 26, 16)))
132
+ solid = sdf.intersect(body, sdf.plane()) # flat print base at z=0
133
+ mesh = sdf.mesh(solid, bounds=((-30, -30, 0), (30, 50, 50)), pitch=0.5)
134
+ ```
135
+
136
+ ## The iteration loop
137
+
138
+ `workflow.main` wraps the loop every scripted model settles into: `preview`
139
+ builds coarse and renders a PNG in seconds; `final` cuts the print-quality
140
+ files; and every preview is archived to `previews/` — script included — so
141
+ each iteration's look and the code that produced it stay side by side.
142
+
143
+ ```python
144
+ from solidsmith import workflow
145
+
146
+ def build(fast: bool):
147
+ ...
148
+ return parts # a mesh, a Part, or a list of them
149
+
150
+ if __name__ == "__main__":
151
+ workflow.main(build, name="widget")
152
+ ```
153
+
154
+ `python widget.py preview`, tweak, repeat — `python widget.py final` once
155
+ the design is locked.
156
+
157
+ ## Roadmap
158
+
159
+ - More examples and an overhang/support-need check
160
+ - PyPI release
161
+
162
+ ## License
163
+
164
+ [MIT](LICENSE)
@@ -0,0 +1,135 @@
1
+ # solidsmith
2
+
3
+ [![CI](https://github.com/scottshamansky/solidsmith/actions/workflows/ci.yml/badge.svg)](https://github.com/scottshamansky/solidsmith/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)
6
+
7
+ **Forge watertight, print-ready 3D parts from Python.**
8
+
9
+ solidsmith is the missing glue between [trimesh](https://trimesh.org) and your
10
+ printer: booleans that don't fall over, 3MF export that keeps its colors, a
11
+ printability report, and quick multi-view renders to iterate against — the
12
+ scaffolding every scripted-CAD project ends up reinventing, packaged once.
13
+
14
+ ![parametric honeycomb bin rendered from three angles](docs/images/honeycomb_bin.png)
15
+
16
+ ## Why script your models?
17
+
18
+ Parametric code beats mouse-driven CAD when you make things repeatedly:
19
+
20
+ - **Every dimension is a variable.** Resize a bin, thicken a wall, or regenerate
21
+ a whole family of sizes by editing constants, not dragging faces.
22
+ - **Geometry you authored is geometry you own.** Marketplaces increasingly
23
+ require sellers of printed goods to use their own original designs. A model
24
+ built from your own code is original by construction, and generic patterns
25
+ (honeycombs, gears, chamfers) stay free for everyone.
26
+ - **Designs diff, review, and version like any other code.**
27
+
28
+ ## What trimesh leaves to you (and solidsmith does)
29
+
30
+ | Pain | What solidsmith does |
31
+ | --- | --- |
32
+ | Booleans fail on real-world meshes | `union` / `difference` / `intersection` run the robust manifold engine with mesh hygiene (`clean`) before and after |
33
+ | 3MF export silently drops color | `write_3mf` injects real `<basematerials>` and tags each body, so multi-color models open in the slicer with filaments already assigned |
34
+ | "Will it print?" found out the hard way | `check` reports watertightness per body, bed fit, plate contact, walls too thin for the nozzle, and stray debris bodies — against a named printer profile (`printer="bambu_a1_mini"`) or your own `Printer` |
35
+ | Seeing your part means opening a slicer | `render_views` renders shaded PNGs from named camera angles in about a second, no GUI or GPU |
36
+
37
+ ## Quickstart
38
+
39
+ ```python
40
+ from solidsmith import Part, check, difference, rounded_prism, render_views, write_3mf
41
+
42
+ shell = rounded_prism((80, 50, 30), radius=8)
43
+ cavity = rounded_prism((74, 44, 30), radius=5)
44
+ cavity.apply_translation((0, 0, 3))
45
+ tray = difference(shell, cavity)
46
+
47
+ print(check(tray)) # watertight? fits the bed? on the plate?
48
+ write_3mf(Part(tray, color=(226, 122, 40), name="tray"), "tray.3mf")
49
+ render_views(tray, "tray.png") # shaded views, one PNG
50
+ ```
51
+
52
+ ```
53
+ ✔ watertight (1 body)
54
+ ✔ 80.0 × 50.0 × 30.0 mm on a 256 × 256 × 256 bed (Bambu X1C / P1S)
55
+ ✔ first layer on the plate (z=0)
56
+ 31.0 cm³ · 564 triangles
57
+ ```
58
+
59
+ Multi-color is just more `Part`s — one per filament — exported together:
60
+
61
+ ```python
62
+ write_3mf([Part(body, (30, 120, 220), "body"),
63
+ Part(cap, (240, 200, 40), "cap")], "model.3mf")
64
+ ```
65
+
66
+ ## Examples
67
+
68
+ | Model | Run it |
69
+ | --- | --- |
70
+ | Honeycomb storage bin — vented walls, supports-free label tab, every dimension a parameter | `python examples/honeycomb_bin.py` |
71
+ | Pebble planter — SDF-sculpted organic form, hollow with a drainage hole, split into two filament colors | `python examples/pebble_planter.py` |
72
+ | Hex bit organizer — 16 slide-fit sockets for 1/4" bits, built on `workflow.main` with preview/final modes | `python examples/bit_organizer.py preview` |
73
+ | Two-tone phone stand — angled boolean cuts, a charging-cable channel, one AMS color swap, checked against a named printer profile | `python examples/phone_stand.py preview` |
74
+
75
+ ![two-color SDF pebble planter](docs/images/pebble_planter.png)
76
+
77
+ Each example writes STL + colored 3MF + a rendered preview into `out/` and
78
+ prints its printability report. Add `--fast` while you iterate.
79
+
80
+ ## Install
81
+
82
+ ```bash
83
+ git clone https://github.com/scottshamansky/solidsmith
84
+ cd solidsmith
85
+ pip install -e .
86
+ ```
87
+
88
+ Requires Python 3.9+. Dimensions are millimeters throughout; the default bed
89
+ is a 256 mm cube (Bambu P1/P2/X1 class) and every check takes your own `bed=`.
90
+
91
+ ## Sculpting (SDF)
92
+
93
+ Alongside the hard-edged boolean toolkit, `solidsmith.sdf` models organic
94
+ shapes as blended signed distance fields — functional primitives, smooth
95
+ unions that read as clay fillets, `offset`/`shell` for hollowing, and
96
+ marching-cubes meshing with Taubin smoothing:
97
+
98
+ ```python
99
+ from solidsmith import sdf
100
+
101
+ body = sdf.smooth_union(12, sdf.sphere((0, 0, 30), 25),
102
+ sdf.ellipsoid((0, 18, 22), (20, 26, 16)))
103
+ solid = sdf.intersect(body, sdf.plane()) # flat print base at z=0
104
+ mesh = sdf.mesh(solid, bounds=((-30, -30, 0), (30, 50, 50)), pitch=0.5)
105
+ ```
106
+
107
+ ## The iteration loop
108
+
109
+ `workflow.main` wraps the loop every scripted model settles into: `preview`
110
+ builds coarse and renders a PNG in seconds; `final` cuts the print-quality
111
+ files; and every preview is archived to `previews/` — script included — so
112
+ each iteration's look and the code that produced it stay side by side.
113
+
114
+ ```python
115
+ from solidsmith import workflow
116
+
117
+ def build(fast: bool):
118
+ ...
119
+ return parts # a mesh, a Part, or a list of them
120
+
121
+ if __name__ == "__main__":
122
+ workflow.main(build, name="widget")
123
+ ```
124
+
125
+ `python widget.py preview`, tweak, repeat — `python widget.py final` once
126
+ the design is locked.
127
+
128
+ ## Roadmap
129
+
130
+ - More examples and an overhang/support-need check
131
+ - PyPI release
132
+
133
+ ## License
134
+
135
+ [MIT](LICENSE)
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "solidsmith"
7
+ version = "0.2.0"
8
+ description = "Forge watertight, print-ready 3D parts from Python: robust booleans, colored 3MF export, and multi-view previews."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Scott Shamansky" }]
12
+ requires-python = ">=3.9"
13
+ dependencies = [
14
+ "numpy>=1.24",
15
+ "scipy>=1.10",
16
+ "trimesh>=4.10",
17
+ "manifold3d>=2.5",
18
+ "matplotlib>=3.7",
19
+ "networkx>=2.8",
20
+ "lxml>=4.9",
21
+ "scikit-image>=0.22",
22
+ "rtree>=1.0",
23
+ ]
24
+ keywords = [
25
+ "3d-printing",
26
+ "parametric",
27
+ "stl",
28
+ "3mf",
29
+ "trimesh",
30
+ "cad",
31
+ "maker",
32
+ ]
33
+ classifiers = [
34
+ "Development Status :: 4 - Beta",
35
+ "Intended Audience :: Manufacturing",
36
+ "License :: OSI Approved :: MIT License",
37
+ "Programming Language :: Python :: 3",
38
+ "Topic :: Multimedia :: Graphics :: 3D Modeling",
39
+ "Topic :: Scientific/Engineering",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/scottshamansky/solidsmith"
44
+ Issues = "https://github.com/scottshamansky/solidsmith/issues"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.setuptools.package-data]
50
+ solidsmith = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,47 @@
1
+ """solidsmith — forge watertight, print-ready 3D parts from Python.
2
+
3
+ The missing glue between trimesh and your printer: booleans that don't fall
4
+ over, 3MF export that keeps its colors, a printability report, and quick
5
+ multi-view renders to iterate against.
6
+ """
7
+
8
+ from solidsmith import printers, sdf, workflow
9
+ from solidsmith.export import write_3mf, write_stl
10
+ from solidsmith.ops import (
11
+ clean,
12
+ concat,
13
+ difference,
14
+ hex_prism,
15
+ intersection,
16
+ rounded_prism,
17
+ union,
18
+ )
19
+ from solidsmith.part import Part
20
+ from solidsmith.printers import Printer
21
+ from solidsmith.preview import DEFAULT_VIEWS, render_views
22
+ from solidsmith.report import DEFAULT_BED, PrintReport, check
23
+
24
+ __version__ = "0.2.0"
25
+
26
+ __all__ = [
27
+ "DEFAULT_BED",
28
+ "DEFAULT_VIEWS",
29
+ "Part",
30
+ "PrintReport",
31
+ "Printer",
32
+ "printers",
33
+ "check",
34
+ "clean",
35
+ "concat",
36
+ "difference",
37
+ "hex_prism",
38
+ "intersection",
39
+ "render_views",
40
+ "rounded_prism",
41
+ "sdf",
42
+ "union",
43
+ "workflow",
44
+ "write_3mf",
45
+ "write_stl",
46
+ "__version__",
47
+ ]
@@ -0,0 +1,92 @@
1
+ """Export meshes to print-ready files.
2
+
3
+ STL is the easy half. 3MF is the interesting one: trimesh writes valid 3MF
4
+ geometry but silently drops color, so every body opens gray in the slicer.
5
+ `write_3mf` post-processes the archive it writes — injecting a real
6
+ ``<basematerials>`` resource and tagging each body with a material index —
7
+ so a multi-color model opens with its filament assignments already in place.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import xml.etree.ElementTree as ET
13
+ import zipfile
14
+ from pathlib import Path
15
+
16
+ import trimesh
17
+
18
+ from solidsmith.ops import concat
19
+ from solidsmith.part import Part, as_parts
20
+
21
+ _CORE_NS = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
22
+ _MODEL_ENTRY = "3D/3dmodel.model"
23
+
24
+
25
+ def write_stl(parts, path) -> Path:
26
+ """Write one binary STL. Multiple parts are concatenated (STL has no color)."""
27
+ path = Path(path)
28
+ parts = as_parts(parts)
29
+ mesh = parts[0].mesh if len(parts) == 1 else concat(*(p.mesh for p in parts))
30
+ mesh.export(path)
31
+ return path
32
+
33
+
34
+ def write_3mf(parts, path) -> Path:
35
+ """Write a single 3MF containing every part as a separately colored body."""
36
+ path = Path(path)
37
+ parts = as_parts(parts)
38
+
39
+ scene = trimesh.Scene()
40
+ for i, part in enumerate(parts):
41
+ # zero-padded prefix pins a stable object order in the archive
42
+ scene.add_geometry(part.mesh, geom_name=f"{i:03d}_{part.name}")
43
+ scene.export(path)
44
+
45
+ _inject_materials(path, parts)
46
+ return path
47
+
48
+
49
+ def _inject_materials(path: Path, parts: "list[Part]") -> None:
50
+ """Rewrite the 3MF's model XML with basematerials + per-object material refs."""
51
+ with zipfile.ZipFile(path, "r") as zf:
52
+ entries = {info.filename: zf.read(info.filename) for info in zf.infolist()}
53
+ if _MODEL_ENTRY not in entries:
54
+ raise ValueError(f"{path} has no {_MODEL_ENTRY}; not a 3MF written by trimesh?")
55
+
56
+ ET.register_namespace("", _CORE_NS)
57
+ root = ET.fromstring(entries[_MODEL_ENTRY])
58
+ ns = {"m": _CORE_NS}
59
+ resources = root.find("m:resources", ns)
60
+ if resources is None:
61
+ raise ValueError("3MF model has no <resources> element")
62
+
63
+ objects = [
64
+ obj for obj in resources.findall("m:object", ns)
65
+ if obj.find("m:mesh", ns) is not None
66
+ ]
67
+ if len(objects) != len(parts):
68
+ raise ValueError(
69
+ f"expected {len(parts)} mesh objects in the 3MF, found {len(objects)}"
70
+ )
71
+
72
+ used_ids = [int(obj.get("id")) for obj in resources.findall("m:object", ns)]
73
+ material_id = str(max(used_ids) + 1)
74
+
75
+ materials = ET.Element(f"{{{_CORE_NS}}}basematerials", {"id": material_id})
76
+ for part in parts:
77
+ r, g, b = (int(c) for c in part.color)
78
+ ET.SubElement(
79
+ materials,
80
+ f"{{{_CORE_NS}}}base",
81
+ {"name": part.name, "displaycolor": f"#{r:02X}{g:02X}{b:02X}"},
82
+ )
83
+ resources.insert(0, materials)
84
+
85
+ for index, obj in enumerate(objects):
86
+ obj.set("pid", material_id)
87
+ obj.set("pindex", str(index))
88
+
89
+ entries[_MODEL_ENTRY] = ET.tostring(root, encoding="UTF-8", xml_declaration=True)
90
+ with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
91
+ for name, data in entries.items():
92
+ zf.writestr(name, data)
@@ -0,0 +1,96 @@
1
+ """Boolean modeling that stays watertight.
2
+
3
+ trimesh's default boolean backend rejects meshes that are fine in practice;
4
+ the manifold engine is far more robust but still rewards mesh hygiene on the
5
+ way in and out. These wrappers bake both in, so `difference(a, b)` is a thing
6
+ you can stop thinking about.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+ import trimesh
13
+
14
+
15
+ def clean(mesh: trimesh.Trimesh) -> trimesh.Trimesh:
16
+ """Scrub a mesh in place: weld vertices, drop degenerate faces, fix winding.
17
+
18
+ Marching-cubes and heavily boolean'd meshes accumulate slivers that make
19
+ later booleans fail; running this between operations keeps them reliable.
20
+ """
21
+ mesh.merge_vertices()
22
+ mesh.update_faces(mesh.nondegenerate_faces())
23
+ mesh.remove_unreferenced_vertices()
24
+ mesh.fix_normals()
25
+ return mesh
26
+
27
+
28
+ def _boolean(kind: str, meshes: "list[trimesh.Trimesh]") -> trimesh.Trimesh:
29
+ prepared = [clean(m.copy()) for m in meshes]
30
+ result = getattr(trimesh.boolean, kind)(prepared, engine="manifold")
31
+ return clean(result)
32
+
33
+
34
+ def union(*meshes: trimesh.Trimesh) -> trimesh.Trimesh:
35
+ """Fuse meshes into one solid."""
36
+ if len(meshes) == 1:
37
+ return clean(meshes[0].copy())
38
+ return _boolean("union", list(meshes))
39
+
40
+
41
+ def difference(base: trimesh.Trimesh, *cutters: trimesh.Trimesh) -> trimesh.Trimesh:
42
+ """Subtract every cutter from base."""
43
+ if not cutters:
44
+ return clean(base.copy())
45
+ return _boolean("difference", [base, *cutters])
46
+
47
+
48
+ def intersection(*meshes: trimesh.Trimesh) -> trimesh.Trimesh:
49
+ """Keep only the volume common to all meshes."""
50
+ return _boolean("intersection", list(meshes))
51
+
52
+
53
+ def concat(*meshes: trimesh.Trimesh) -> trimesh.Trimesh:
54
+ """Append meshes without any boolean (touching bodies that print as one)."""
55
+ return trimesh.util.concatenate(list(meshes))
56
+
57
+
58
+ def rounded_prism(
59
+ extents,
60
+ radius: float,
61
+ sections: int = 64,
62
+ ) -> trimesh.Trimesh:
63
+ """Rectangular prism with rounded vertical corners, sitting on z=0.
64
+
65
+ extents is (x, y, z) in mm; radius rounds the four vertical edges. Built
66
+ from boxes plus corner cylinders so it needs no 2D triangulation deps,
67
+ and clamps the radius rather than erroring when it exceeds the footprint.
68
+ """
69
+ x, y, z = (float(v) for v in extents)
70
+ if min(x, y, z) <= 0:
71
+ raise ValueError(f"extents must be positive, got {extents}")
72
+ radius = max(0.0, min(float(radius), x / 2 - 1e-3, y / 2 - 1e-3))
73
+
74
+ if radius < 1e-6:
75
+ prism = trimesh.creation.box((x, y, z))
76
+ else:
77
+ core_x = trimesh.creation.box((x, y - 2 * radius, z))
78
+ core_y = trimesh.creation.box((x - 2 * radius, y, z))
79
+ corners = []
80
+ for sx in (-1.0, 1.0):
81
+ for sy in (-1.0, 1.0):
82
+ cyl = trimesh.creation.cylinder(radius=radius, height=z, sections=sections)
83
+ cyl.apply_translation((sx * (x / 2 - radius), sy * (y / 2 - radius), 0))
84
+ corners.append(cyl)
85
+ prism = union(core_x, core_y, *corners)
86
+
87
+ prism.apply_translation((0, 0, z / 2))
88
+ return prism
89
+
90
+
91
+ def hex_prism(circumradius: float, height: float, point_up: bool = True) -> trimesh.Trimesh:
92
+ """Hexagonal prism along z, centered at the origin (honeycomb building block)."""
93
+ hexagon = trimesh.creation.cylinder(radius=circumradius, height=height, sections=6)
94
+ if point_up:
95
+ hexagon.apply_transform(trimesh.transformations.rotation_matrix(np.pi / 6, (0, 0, 1)))
96
+ return hexagon
@@ -0,0 +1,41 @@
1
+ """A mesh plus how it should appear when exported or rendered."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Tuple
7
+
8
+ import trimesh
9
+
10
+
11
+ @dataclass
12
+ class Part:
13
+ """One printable body: a mesh, a display color, and a name.
14
+
15
+ Color is sRGB 0-255. A multi-color model is just a list of Parts (one
16
+ per filament); exported together they become a single 3MF that opens in
17
+ the slicer with every body already assigned its color.
18
+ """
19
+
20
+ mesh: trimesh.Trimesh
21
+ color: Tuple[int, int, int] = (140, 140, 145)
22
+ name: str = "part"
23
+
24
+
25
+ def as_parts(obj) -> "list[Part]":
26
+ """Normalize a Part, a mesh, or a sequence of either into a list of Parts."""
27
+ if isinstance(obj, Part):
28
+ return [obj]
29
+ if isinstance(obj, trimesh.Trimesh):
30
+ return [Part(obj)]
31
+ parts = []
32
+ for i, item in enumerate(obj):
33
+ if isinstance(item, Part):
34
+ parts.append(item)
35
+ elif isinstance(item, trimesh.Trimesh):
36
+ parts.append(Part(item, name=f"part_{i}"))
37
+ else:
38
+ raise TypeError(f"expected Part or Trimesh, got {type(item).__name__}")
39
+ if not parts:
40
+ raise ValueError("no parts given")
41
+ return parts