solid123d 0.1.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,28 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, macos-latest]
14
+ python-version: ["3.10", "3.13"]
15
+ runs-on: ${{ matrix.os }}
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: uv sync --dev
26
+
27
+ - name: Run tests
28
+ run: uv run pytest -v
@@ -0,0 +1,19 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ .pytest_cache/
7
+
8
+ # Build artifacts
9
+ dist/
10
+ build/
11
+
12
+ # Generated geometry
13
+ *.step
14
+ *.stp
15
+ *.stl
16
+ *.3mf
17
+
18
+ # OS
19
+ .DS_Store
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,100 @@
1
+ # Conversation Log
2
+
3
+ ## 2026-08-13
4
+
5
+ **User**: Wants to transition from OpenSCAD/SolidPython to build123d. Asked
6
+ for a small bridge letting existing SolidPython code build build123d objects
7
+ by only changing imports, e.g. `from solid import translate, cube,
8
+ scad_render_to_file` → same API, build123d underneath.
9
+
10
+ **Assistant**: Built the `solid123d` package (uv project, build123d 0.11.1,
11
+ Python 3.13, branch `feature/solidpython-bridge`).
12
+
13
+ Key design decisions:
14
+ - Primitives return **native build123d `Shape` objects** (no wrapper class),
15
+ so bridged code interoperates directly with build123d and migration can
16
+ proceed file-by-file. Trade-off: SolidPython's `a * b` intersection
17
+ operator is unavailable (`a & b` / `intersection()(a, b)` instead).
18
+ - Transforms are closures: `translate(v)` returns a callable applying
19
+ `Pos * shape`; multiple children are implicitly unioned as in OpenSCAD.
20
+ - `rotate([x,y,z])` applies sequential global-axis rotations X→Y→Z to match
21
+ OpenSCAD's fixed-axis order exactly; axis-angle and scalar forms supported.
22
+ - OpenSCAD alignment semantics preserved: `cube`/`square` corner at origin
23
+ unless `center=True`; `cylinder` XY-centered sitting on z=0.
24
+ - `scad_render_to_file` exports real geometry: `.scad` filenames rewritten
25
+ to `.step` with a `UserWarning`; `.stl`/`.step`/`.stp` honored as-is.
26
+ `scad_render()` raises `NotImplementedError` (no SCAD source exists).
27
+ - `hull`, `minkowski`, `linear_extrude(twist=)` raise `NotImplementedError`
28
+ (no BRep equivalent); `segments`/`$fn` accepted and ignored.
29
+ - `linear_extrude(scale=)` implemented via loft between base and scaled top.
30
+ - `rotate_extrude` maps the XY profile onto Plane.XZ then `revolve`s about Z.
31
+ - `solid123d.utils` provides `up/down/left/right/forward/back`.
32
+
33
+ Verification: 32 pytest tests pass (volumes/bboxes vs analytic values, e.g.
34
+ torus volume for `rotate_extrude`); user's exact example script runs with
35
+ only the import changed and emits a valid STEP file.
36
+
37
+ ## 2026-08-13 (later)
38
+
39
+ **User**: OpenSCAD found "Academy Engraved LET" by name but build123d
40
+ couldn't. How does font identification work on both systems?
41
+
42
+ **Assistant**: Diagnosed on-machine: OpenSCAD uses fontconfig, which matches
43
+ the family name (nameID 1) and treats subfamily (nameID 2) as a style with
44
+ fuzzy best-match fallback. build123d uses OCCT `Font_FontMgr`, which only
45
+ folds standard subfamilies (Regular/Bold/Italic/Bold Italic) into font
46
+ aspects; this font's subfamily is "Plain", so OCCT registers it as
47
+ "Academy Engraved LET Plain" and a lookup by family name warns on stderr and
48
+ silently substitutes Arial. Verified: `Text(font="Academy Engraved LET
49
+ Plain")` and `font_path=` both render correctly.
50
+
51
+ **User**: Make `text()` resolve fonts by family name.
52
+
53
+ **Assistant**: Added `solid123d/fonts.py`: scans platform font dirs
54
+ (macOS/Linux/Windows) with fontTools (~0.06 s, `lru_cache`d), indexes
55
+ family -> {subfamily -> path} incl. .ttc collections, parses OpenSCAD's
56
+ `"Family:style=Style"` syntax, resolves to a concrete file path passed to
57
+ build123d as `font_path`. Unknown families fall through to build123d's own
58
+ name lookup (with standard styles mapped to `FontStyle`). Default style
59
+ preference: regular/plain/normal/book/roman/medium, else first face.
60
+ 10 new tests (parse, resolution, end-to-end area check vs Arial fallback);
61
+ 42 total pass.
62
+
63
+ ## 2026-08-13 (later still)
64
+
65
+ **User**: Reported unions/differences "not applying" in sollie_token.py;
66
+ suspected linear_extrude without twist.
67
+
68
+ **Assistant**: linear_extrude is exact; real bug was solid123d `scale()`
69
+ padding a 2-vector with 0 instead of 1 (OpenSCAD identity), flattening
70
+ extruded text to degenerate solids so engraving subtractions no-op'd or
71
+ nulled. Fixed via `vec3(v, default=1.0)` + regression test (43 pass);
72
+ user's script verified end-to-end (embeds seat exactly into pockets).
73
+
74
+ **User**: Declined adding color-aware compound export to the bridge
75
+ (OpenSCAD exports colorless STL anyway). Asked how to make one STEP with
76
+ per-shape colors from two build123d Shapes.
77
+
78
+ **Assistant**: Recipe: set `.color` (and optionally `.label`) on each
79
+ Shape, group with `Compound(children=[...])` (grouping, not fusing), then
80
+ `export_step`. Verified two-color output in combined_colored.step. Gotcha
81
+ documented: the 8 predefined STEP colours (yellow, red, ...) are written
82
+ as DRAUGHTING_PRE_DEFINED_COLOUR('name'), not COLOUR_RGB — grep both when
83
+ inspecting files.
84
+
85
+ **User**: Wants a typing alias in solid123d (aliasing build123d.Shape) so
86
+ old SolidPython return-type annotations stay correct. (First asked for
87
+ `SolidPythonObject`, then corrected: that name was a mistake.)
88
+
89
+ **Assistant**: Exported `OpenSCADObject` (SolidPython's real class name)
90
+ and `OpenSCADObjectPlus` (solid2's) as aliases of `build123d.Shape`.
91
+ Test added; 44 pass.
92
+
93
+ **User**: Chose the name `solid123d` (over solid-build123d etc., all
94
+ verified available on PyPI); asked to replace every "solidbd" occurrence.
95
+
96
+ **Assistant**: Renamed src/solidbd -> src/solid123d and
97
+ tests/test_solidbd.py -> tests/test_solid123d.py; updated pyproject.toml,
98
+ README, tests, sources, and CONVERSATION.md; re-locked and synced with uv;
99
+ also updated the imports in sollie_token_aug_2026/sollie_token.py. Grep
100
+ confirms zero remaining occurrences; 44 tests pass.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Evan Jones
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,93 @@
1
+ Metadata-Version: 2.5
2
+ Name: solid123d
3
+ Version: 0.1.0
4
+ Summary: Drop-in bridge from SolidPython to build123d: change one import, get native BRep geometry
5
+ Project-URL: Homepage, https://github.com/etjones/solid123d
6
+ Project-URL: Repository, https://github.com/etjones/solid123d
7
+ Project-URL: Issues, https://github.com/etjones/solid123d/issues
8
+ Author-email: Evan Jones <evan_t_jones@mac.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: 3d,brep,build123d,cad,openscad,solidpython,step
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Manufacturing
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: build123d>=0.11.1
23
+ Requires-Dist: fonttools>=4.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # solid123d
27
+
28
+ A bridge that runs [SolidPython](https://github.com/SolidCode/SolidPython)
29
+ (OpenSCAD-style) code on the [build123d](https://build123d.readthedocs.io/)
30
+ BRep kernel. Change one import and existing code produces native build123d
31
+ shapes instead of OpenSCAD source.
32
+
33
+ ```python
34
+ # before
35
+ from solid import translate, cube, scad_render_to_file
36
+ # after
37
+ from solid123d import translate, cube, scad_render_to_file
38
+
39
+ c = translate([10, 0, 0])(cube(10, center=True))
40
+ scad_render_to_file(c, filename="c.scad") # writes c.step (real BRep geometry)
41
+ ```
42
+
43
+ Every object returned is a plain build123d `Shape`, so bridged code mixes
44
+ freely with native build123d — the intended migration path:
45
+
46
+ ```python
47
+ from build123d import fillet
48
+ from solid123d import cube
49
+
50
+ part = cube(20, center=True)
51
+ part = fillet(part.edges(), radius=2) # native build123d from here on
52
+ ```
53
+
54
+ ## Supported
55
+
56
+ - **Primitives**: `cube`, `sphere`, `cylinder` (incl. `r1`/`r2` cones,
57
+ `d`/`d1`/`d2`), `square`, `circle`, `polygon` (incl. `paths` holes), `text`
58
+ - **Font resolution**: `text(font=...)` matches fonts by family name the way
59
+ fontconfig/OpenSCAD does (incl. `"Family:style=Style"` syntax), by scanning
60
+ system font directories with fontTools and handing build123d the resolved
61
+ file path. This fixes fonts with nonstandard subfamilies (e.g.
62
+ `"Academy Engraved LET"`, whose subfamily "Plain" makes OCCT's own lookup
63
+ fall back to Arial).
64
+ - **Transforms** (callable style, `translate(v)(obj, ...)`): `translate`,
65
+ `rotate` (vector, scalar, and axis-angle forms with OpenSCAD ordering),
66
+ `scale`, `mirror`, `resize`, `color`, `offset`
67
+ - **Booleans**: `union()`, `difference()`, `intersection()` — plus native
68
+ operators `a + b`, `a - b`, `a & b`
69
+ - **2D → 3D**: `linear_extrude` (incl. `center`, `scale`; no `twist`),
70
+ `rotate_extrude` (incl. partial `angle`)
71
+ - **Export**: `scad_render_to_file` writes `.step`/`.stl`; a `.scad`
72
+ filename is rewritten to `.step` with a warning
73
+ - `solid123d.utils`: `up`, `down`, `left`, `right`, `forward`, `back`
74
+ - **Typing aliases**: `OpenSCADObject` and `OpenSCADObjectPlus` are
75
+ aliases of `build123d.Shape`, so existing
76
+ annotations like `def some_obj() -> OpenSCADObject:` remain correct
77
+
78
+ ## Known differences
79
+
80
+ - `a * b` intersection is not overloaded; use `a & b` or `intersection()(a, b)`.
81
+ - `hull()` and `minkowski()` raise `NotImplementedError` (no BRep equivalent);
82
+ in build123d these are usually a `loft`, `sweep`, `offset`, or fillet.
83
+ - `linear_extrude(twist=...)` raises `NotImplementedError`.
84
+ - `$fn`/`segments` arguments are accepted and ignored — BRep curves are exact.
85
+ - `scad_render()` (source string) raises `NotImplementedError`.
86
+ - OpenSCAD modifiers (`#`, `%`, `!`) and `import()`/`surface()`/`projection()`
87
+ are not implemented.
88
+
89
+ ## Development
90
+
91
+ ```bash
92
+ just test # or: uv run pytest
93
+ ```
@@ -0,0 +1,68 @@
1
+ # solid123d
2
+
3
+ A bridge that runs [SolidPython](https://github.com/SolidCode/SolidPython)
4
+ (OpenSCAD-style) code on the [build123d](https://build123d.readthedocs.io/)
5
+ BRep kernel. Change one import and existing code produces native build123d
6
+ shapes instead of OpenSCAD source.
7
+
8
+ ```python
9
+ # before
10
+ from solid import translate, cube, scad_render_to_file
11
+ # after
12
+ from solid123d import translate, cube, scad_render_to_file
13
+
14
+ c = translate([10, 0, 0])(cube(10, center=True))
15
+ scad_render_to_file(c, filename="c.scad") # writes c.step (real BRep geometry)
16
+ ```
17
+
18
+ Every object returned is a plain build123d `Shape`, so bridged code mixes
19
+ freely with native build123d — the intended migration path:
20
+
21
+ ```python
22
+ from build123d import fillet
23
+ from solid123d import cube
24
+
25
+ part = cube(20, center=True)
26
+ part = fillet(part.edges(), radius=2) # native build123d from here on
27
+ ```
28
+
29
+ ## Supported
30
+
31
+ - **Primitives**: `cube`, `sphere`, `cylinder` (incl. `r1`/`r2` cones,
32
+ `d`/`d1`/`d2`), `square`, `circle`, `polygon` (incl. `paths` holes), `text`
33
+ - **Font resolution**: `text(font=...)` matches fonts by family name the way
34
+ fontconfig/OpenSCAD does (incl. `"Family:style=Style"` syntax), by scanning
35
+ system font directories with fontTools and handing build123d the resolved
36
+ file path. This fixes fonts with nonstandard subfamilies (e.g.
37
+ `"Academy Engraved LET"`, whose subfamily "Plain" makes OCCT's own lookup
38
+ fall back to Arial).
39
+ - **Transforms** (callable style, `translate(v)(obj, ...)`): `translate`,
40
+ `rotate` (vector, scalar, and axis-angle forms with OpenSCAD ordering),
41
+ `scale`, `mirror`, `resize`, `color`, `offset`
42
+ - **Booleans**: `union()`, `difference()`, `intersection()` — plus native
43
+ operators `a + b`, `a - b`, `a & b`
44
+ - **2D → 3D**: `linear_extrude` (incl. `center`, `scale`; no `twist`),
45
+ `rotate_extrude` (incl. partial `angle`)
46
+ - **Export**: `scad_render_to_file` writes `.step`/`.stl`; a `.scad`
47
+ filename is rewritten to `.step` with a warning
48
+ - `solid123d.utils`: `up`, `down`, `left`, `right`, `forward`, `back`
49
+ - **Typing aliases**: `OpenSCADObject` and `OpenSCADObjectPlus` are
50
+ aliases of `build123d.Shape`, so existing
51
+ annotations like `def some_obj() -> OpenSCADObject:` remain correct
52
+
53
+ ## Known differences
54
+
55
+ - `a * b` intersection is not overloaded; use `a & b` or `intersection()(a, b)`.
56
+ - `hull()` and `minkowski()` raise `NotImplementedError` (no BRep equivalent);
57
+ in build123d these are usually a `loft`, `sweep`, `offset`, or fillet.
58
+ - `linear_extrude(twist=...)` raises `NotImplementedError`.
59
+ - `$fn`/`segments` arguments are accepted and ignored — BRep curves are exact.
60
+ - `scad_render()` (source string) raises `NotImplementedError`.
61
+ - OpenSCAD modifiers (`#`, `%`, `!`) and `import()`/`surface()`/`projection()`
62
+ are not implemented.
63
+
64
+ ## Development
65
+
66
+ ```bash
67
+ just test # or: uv run pytest
68
+ ```
@@ -0,0 +1,9 @@
1
+ default: test
2
+
3
+ run: test
4
+
5
+ test:
6
+ uv run pytest -q
7
+
8
+ watch:
9
+ uv run watchfiles 'uv run pytest -q' src tests
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "solid123d"
3
+ version = "0.1.0"
4
+ description = "Drop-in bridge from SolidPython to build123d: change one import, get native BRep geometry"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.10"
9
+ authors = [{ name = "Evan Jones", email = "evan_t_jones@mac.com" }]
10
+ keywords = ["openscad", "solidpython", "build123d", "cad", "3d", "step", "brep"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Manufacturing",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Multimedia :: Graphics :: 3D Modeling",
21
+ ]
22
+ dependencies = [
23
+ "build123d>=0.11.1",
24
+ "fonttools>=4.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/etjones/solid123d"
29
+ Repository = "https://github.com/etjones/solid123d"
30
+ Issues = "https://github.com/etjones/solid123d/issues"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=9.1.1",
35
+ ]
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
@@ -0,0 +1,63 @@
1
+ """solid123d: run SolidPython code on the build123d kernel.
2
+
3
+ Change ``from solid import ...`` to ``from solid123d import ...`` and the
4
+ same code produces native build123d shapes, which can be mixed freely
5
+ with regular build123d code and exported to STEP/STL.
6
+ """
7
+
8
+ from build123d import Shape
9
+
10
+ from .booleans import difference, hull, intersection, minkowski, union
11
+ from .extrusions import linear_extrude, rotate_extrude
12
+ from .primitives import (
13
+ circle,
14
+ cube,
15
+ cylinder,
16
+ polygon,
17
+ sphere,
18
+ square,
19
+ text,
20
+ )
21
+ from .render import scad_render, scad_render_to_file
22
+ from .transforms import (
23
+ color,
24
+ mirror,
25
+ offset,
26
+ resize,
27
+ rotate,
28
+ scale,
29
+ translate,
30
+ )
31
+
32
+ # Typing aliases: SolidPython signatures like `-> OpenSCADObject` stay
33
+ # correct, since every solid123d function returns a build123d Shape.
34
+ OpenSCADObject = Shape
35
+ OpenSCADObjectPlus = Shape
36
+
37
+ __all__ = [
38
+ "OpenSCADObject",
39
+ "OpenSCADObjectPlus",
40
+ "circle",
41
+ "color",
42
+ "cube",
43
+ "cylinder",
44
+ "difference",
45
+ "hull",
46
+ "intersection",
47
+ "linear_extrude",
48
+ "minkowski",
49
+ "mirror",
50
+ "offset",
51
+ "polygon",
52
+ "resize",
53
+ "rotate",
54
+ "rotate_extrude",
55
+ "scad_render",
56
+ "scad_render_to_file",
57
+ "scale",
58
+ "sphere",
59
+ "square",
60
+ "text",
61
+ "translate",
62
+ "union",
63
+ ]
@@ -0,0 +1,41 @@
1
+ """Shared helpers for the SolidPython -> build123d bridge."""
2
+
3
+ from collections.abc import Iterable, Sequence
4
+ from functools import reduce
5
+ from operator import add
6
+
7
+ from build123d import Shape
8
+
9
+ Vec3 = tuple[float, float, float]
10
+
11
+
12
+ def vec3(v: float | Sequence[float], default: float = 0.0) -> Vec3:
13
+ """Expand an OpenSCAD-style scalar or vector into an (x, y, z) tuple."""
14
+ if isinstance(v, (int, float)):
15
+ return (float(v), float(v), float(v))
16
+ vals = [float(x) for x in v]
17
+ while len(vals) < 3:
18
+ vals.append(default)
19
+ return (vals[0], vals[1], vals[2])
20
+
21
+
22
+ def flatten(children: Iterable[object]) -> list[Shape]:
23
+ """Flatten nested lists/tuples of shapes (SolidPython allows both
24
+ ``op()(a, b)`` and ``op()([a, b])``)."""
25
+ out: list[Shape] = []
26
+ for child in children:
27
+ if isinstance(child, (list, tuple)):
28
+ out.extend(flatten(child))
29
+ elif child is not None:
30
+ out.append(child)
31
+ return out
32
+
33
+
34
+ def group(children: Iterable[object]) -> Shape:
35
+ """Combine children the way an OpenSCAD block does: implicit union."""
36
+ shapes = flatten(children)
37
+ if not shapes:
38
+ raise ValueError("expected at least one shape")
39
+ if len(shapes) == 1:
40
+ return shapes[0]
41
+ return reduce(add, shapes)
@@ -0,0 +1,64 @@
1
+ """OpenSCAD boolean operations: ``union()(a, b)``, ``difference()(a, b)``.
2
+
3
+ Note that because primitives are native build123d shapes, the algebra
4
+ operators also work directly: ``a + b`` (union), ``a - b`` (difference),
5
+ ``a & b`` (intersection). SolidPython's ``a * b`` intersection operator
6
+ is NOT available — use ``a & b`` or ``intersection()(a, b)``.
7
+ """
8
+
9
+ from collections.abc import Callable
10
+ from functools import reduce
11
+ from operator import and_, sub
12
+
13
+ from build123d import Shape
14
+
15
+ from ._common import flatten, group
16
+
17
+ Applier = Callable[..., Shape]
18
+
19
+
20
+ def union() -> Applier:
21
+ def apply(*children: Shape) -> Shape:
22
+ return group(children)
23
+
24
+ return apply
25
+
26
+
27
+ def difference() -> Applier:
28
+ def apply(*children: Shape) -> Shape:
29
+ shapes = flatten(children)
30
+ if not shapes:
31
+ raise ValueError("difference() requires at least one shape")
32
+ return reduce(sub, shapes)
33
+
34
+ return apply
35
+
36
+
37
+ def intersection() -> Applier:
38
+ def apply(*children: Shape) -> Shape:
39
+ shapes = flatten(children)
40
+ if not shapes:
41
+ raise ValueError("intersection() requires at least one shape")
42
+ return reduce(and_, shapes)
43
+
44
+ return apply
45
+
46
+
47
+ def hull() -> Applier:
48
+ def apply(*children: Shape) -> Shape:
49
+ raise NotImplementedError(
50
+ "hull() has no direct build123d equivalent; model the shape "
51
+ "explicitly (e.g. loft/sweep) instead"
52
+ )
53
+
54
+ return apply
55
+
56
+
57
+ def minkowski() -> Applier:
58
+ def apply(*children: Shape) -> Shape:
59
+ raise NotImplementedError(
60
+ "minkowski() has no build123d equivalent; use offset() or "
61
+ "fillet/chamfer on the build123d object instead"
62
+ )
63
+
64
+ return apply
@@ -0,0 +1,61 @@
1
+ """OpenSCAD 2D -> 3D operations: linear_extrude and rotate_extrude."""
2
+
3
+ from collections.abc import Callable
4
+
5
+ from build123d import Axis, Plane, Pos, Shape
6
+ from build123d import extrude as _bd_extrude
7
+ from build123d import loft as _bd_loft
8
+ from build123d import revolve as _bd_revolve
9
+ from build123d import scale as _bd_scale
10
+
11
+ from ._common import group, vec3
12
+
13
+ Applier = Callable[..., Shape]
14
+
15
+
16
+ def linear_extrude(
17
+ height: float = 100,
18
+ center: bool = False,
19
+ convexity: int | None = None,
20
+ twist: float = 0,
21
+ slices: int | None = None,
22
+ scale: float | tuple[float, float] = 1.0,
23
+ segments: int | None = None,
24
+ ) -> Applier:
25
+ if twist:
26
+ raise NotImplementedError(
27
+ "linear_extrude(twist=...) is not supported; use build123d "
28
+ "sweep() along a helix instead"
29
+ )
30
+ scale_xy = (
31
+ (float(scale), float(scale))
32
+ if isinstance(scale, (int, float))
33
+ else (float(scale[0]), float(scale[1]))
34
+ )
35
+
36
+ def apply(*children: Shape) -> Shape:
37
+ face = group(children)
38
+ if scale_xy == (1.0, 1.0):
39
+ solid = _bd_extrude(face, amount=height)
40
+ else:
41
+ top = _bd_scale(face, by=(scale_xy[0], scale_xy[1], 1.0))
42
+ solid = _bd_loft([face, Pos(0, 0, height) * top])
43
+ if center:
44
+ solid = Pos(0, 0, -height / 2) * solid
45
+ return solid
46
+
47
+ return apply
48
+
49
+
50
+ def rotate_extrude(
51
+ angle: float = 360,
52
+ convexity: int | None = None,
53
+ segments: int | None = None,
54
+ ) -> Applier:
55
+ def apply(*children: Shape) -> Shape:
56
+ # OpenSCAD takes an XY profile (x >= 0) and spins it about Z;
57
+ # map the profile onto the XZ plane, then revolve.
58
+ profile = Plane.XZ * group(children)
59
+ return _bd_revolve(profile, axis=Axis.Z, revolution_arc=angle)
60
+
61
+ return apply