solid123d 0.1.0__py3-none-any.whl
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.
- solid123d/__init__.py +63 -0
- solid123d/_common.py +41 -0
- solid123d/booleans.py +64 -0
- solid123d/extrusions.py +61 -0
- solid123d/fonts.py +100 -0
- solid123d/primitives.py +133 -0
- solid123d/render.py +53 -0
- solid123d/transforms.py +128 -0
- solid123d/utils.py +33 -0
- solid123d-0.1.0.dist-info/METADATA +93 -0
- solid123d-0.1.0.dist-info/RECORD +13 -0
- solid123d-0.1.0.dist-info/WHEEL +4 -0
- solid123d-0.1.0.dist-info/licenses/LICENSE +21 -0
solid123d/__init__.py
ADDED
|
@@ -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
|
+
]
|
solid123d/_common.py
ADDED
|
@@ -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)
|
solid123d/booleans.py
ADDED
|
@@ -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
|
solid123d/extrusions.py
ADDED
|
@@ -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
|
solid123d/fonts.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Resolve font family names to font files, fontconfig-style.
|
|
2
|
+
|
|
3
|
+
OpenSCAD finds fonts via fontconfig, which matches on the family name in
|
|
4
|
+
the font's name table. build123d delegates to OCCT's Font_FontMgr, which
|
|
5
|
+
registers fonts with nonstandard subfamilies (e.g. "Plain") under a
|
|
6
|
+
combined name, so lookups by plain family name silently fall back to
|
|
7
|
+
Arial. This module scans the system font directories with fontTools and
|
|
8
|
+
resolves an OpenSCAD-style ``"Family"`` or ``"Family:style=Style"`` spec
|
|
9
|
+
to a concrete font file path.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from fontTools.ttLib import TTCollection, TTFont
|
|
17
|
+
|
|
18
|
+
_FONT_SUFFIXES = (".ttf", ".otf", ".ttc", ".otc")
|
|
19
|
+
_DEFAULT_STYLES = ("regular", "plain", "normal", "book", "roman", "medium")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _font_dirs() -> list[Path]:
|
|
23
|
+
home = Path.home()
|
|
24
|
+
if sys.platform == "darwin":
|
|
25
|
+
return [
|
|
26
|
+
Path("/System/Library/Fonts"),
|
|
27
|
+
Path("/Library/Fonts"),
|
|
28
|
+
home / "Library" / "Fonts",
|
|
29
|
+
]
|
|
30
|
+
if sys.platform.startswith("win"):
|
|
31
|
+
dirs = [Path(r"C:\Windows\Fonts")]
|
|
32
|
+
local = home / "AppData" / "Local" / "Microsoft" / "Windows" / "Fonts"
|
|
33
|
+
return dirs + [local]
|
|
34
|
+
return [
|
|
35
|
+
Path("/usr/share/fonts"),
|
|
36
|
+
Path("/usr/local/share/fonts"),
|
|
37
|
+
home / ".local" / "share" / "fonts",
|
|
38
|
+
home / ".fonts",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _faces_in_file(path: Path) -> list[tuple[str, str]]:
|
|
43
|
+
"""Return (family, subfamily) for each face in a font file."""
|
|
44
|
+
faces: list[tuple[str, str]] = []
|
|
45
|
+
try:
|
|
46
|
+
if path.suffix.lower() in (".ttc", ".otc"):
|
|
47
|
+
fonts = TTCollection(path, lazy=True).fonts
|
|
48
|
+
else:
|
|
49
|
+
fonts = [TTFont(path, lazy=True)]
|
|
50
|
+
for font in fonts:
|
|
51
|
+
family = font["name"].getDebugName(1)
|
|
52
|
+
subfamily = font["name"].getDebugName(2) or ""
|
|
53
|
+
if family:
|
|
54
|
+
faces.append((family, subfamily))
|
|
55
|
+
font.close()
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
return faces
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@lru_cache(maxsize=1)
|
|
62
|
+
def _font_index() -> dict[str, dict[str, Path]]:
|
|
63
|
+
"""Map lowercase family name -> {lowercase style: file path}."""
|
|
64
|
+
index: dict[str, dict[str, Path]] = {}
|
|
65
|
+
for directory in _font_dirs():
|
|
66
|
+
if not directory.is_dir():
|
|
67
|
+
continue
|
|
68
|
+
for path in sorted(directory.rglob("*")):
|
|
69
|
+
if path.suffix.lower() not in _FONT_SUFFIXES:
|
|
70
|
+
continue
|
|
71
|
+
for family, subfamily in _faces_in_file(path):
|
|
72
|
+
styles = index.setdefault(family.lower(), {})
|
|
73
|
+
styles.setdefault(subfamily.lower(), path)
|
|
74
|
+
return index
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_font_spec(spec: str) -> tuple[str, str | None]:
|
|
78
|
+
"""Split OpenSCAD's ``"Family:style=Style"`` syntax."""
|
|
79
|
+
family, _, rest = spec.partition(":")
|
|
80
|
+
style: str | None = None
|
|
81
|
+
for part in rest.split(":"):
|
|
82
|
+
key, _, value = part.partition("=")
|
|
83
|
+
if key.strip().lower() == "style" and value.strip():
|
|
84
|
+
style = value.strip()
|
|
85
|
+
return family.strip(), style
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def find_font_path(spec: str) -> str | None:
|
|
89
|
+
"""Resolve a font spec to a file path, or None if no family matches."""
|
|
90
|
+
family, style = parse_font_spec(spec)
|
|
91
|
+
styles = _font_index().get(family.lower())
|
|
92
|
+
if not styles:
|
|
93
|
+
return None
|
|
94
|
+
if style is not None:
|
|
95
|
+
path = styles.get(style.lower())
|
|
96
|
+
return str(path) if path is not None else None
|
|
97
|
+
for preferred in _DEFAULT_STYLES:
|
|
98
|
+
if preferred in styles:
|
|
99
|
+
return str(styles[preferred])
|
|
100
|
+
return str(next(iter(styles.values())))
|
solid123d/primitives.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""OpenSCAD primitive shapes, emitted as build123d objects.
|
|
2
|
+
|
|
3
|
+
``segments`` / ``$fn`` style arguments are accepted and ignored:
|
|
4
|
+
build123d is a BRep kernel, so curves are exact.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from build123d import Align, Box, Cone, Cylinder, FontStyle, Rectangle, Shape
|
|
10
|
+
from build123d import Circle as _BdCircle
|
|
11
|
+
from build123d import Polygon as _BdPolygon
|
|
12
|
+
from build123d import Sphere as _BdSphere
|
|
13
|
+
from build123d import Text as _BdText
|
|
14
|
+
|
|
15
|
+
from ._common import group, vec3
|
|
16
|
+
from .fonts import find_font_path, parse_font_spec
|
|
17
|
+
|
|
18
|
+
_CENTERED = (Align.CENTER, Align.CENTER, Align.CENTER)
|
|
19
|
+
_CORNER = (Align.MIN, Align.MIN, Align.MIN)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cube(size: float | Sequence[float] = 1, center: bool = False) -> Shape:
|
|
23
|
+
x, y, z = vec3(size)
|
|
24
|
+
return Box(x, y, z, align=_CENTERED if center else _CORNER)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def sphere(
|
|
28
|
+
r: float | None = None,
|
|
29
|
+
d: float | None = None,
|
|
30
|
+
segments: int | None = None,
|
|
31
|
+
) -> Shape:
|
|
32
|
+
radius = r if r is not None else (d / 2 if d is not None else 1.0)
|
|
33
|
+
return _BdSphere(radius)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cylinder(
|
|
37
|
+
r: float | None = None,
|
|
38
|
+
h: float | None = None,
|
|
39
|
+
r1: float | None = None,
|
|
40
|
+
r2: float | None = None,
|
|
41
|
+
center: bool = False,
|
|
42
|
+
d: float | None = None,
|
|
43
|
+
d1: float | None = None,
|
|
44
|
+
d2: float | None = None,
|
|
45
|
+
segments: int | None = None,
|
|
46
|
+
) -> Shape:
|
|
47
|
+
base = r if r is not None else (d / 2 if d is not None else None)
|
|
48
|
+
bottom = r1 if r1 is not None else (d1 / 2 if d1 is not None else base)
|
|
49
|
+
top = r2 if r2 is not None else (d2 / 2 if d2 is not None else base)
|
|
50
|
+
bottom = 1.0 if bottom is None else float(bottom)
|
|
51
|
+
top = 1.0 if top is None else float(top)
|
|
52
|
+
height = 1.0 if h is None else float(h)
|
|
53
|
+
z_align = Align.CENTER if center else Align.MIN
|
|
54
|
+
align = (Align.CENTER, Align.CENTER, z_align)
|
|
55
|
+
if bottom == top:
|
|
56
|
+
return Cylinder(bottom, height, align=align)
|
|
57
|
+
return Cone(bottom_radius=bottom, top_radius=top, height=height, align=align)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def square(size: float | Sequence[float] = 1, center: bool = False) -> Shape:
|
|
61
|
+
x, y, _ = vec3(size)
|
|
62
|
+
align = (Align.CENTER, Align.CENTER) if center else (Align.MIN, Align.MIN)
|
|
63
|
+
return Rectangle(x, y, align=align)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def circle(
|
|
67
|
+
r: float | None = None,
|
|
68
|
+
d: float | None = None,
|
|
69
|
+
segments: int | None = None,
|
|
70
|
+
) -> Shape:
|
|
71
|
+
radius = r if r is not None else (d / 2 if d is not None else 1.0)
|
|
72
|
+
return _BdCircle(radius)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def polygon(
|
|
76
|
+
points: Sequence[Sequence[float]],
|
|
77
|
+
paths: Sequence[Sequence[int]] | None = None,
|
|
78
|
+
convexity: int | None = None,
|
|
79
|
+
) -> Shape:
|
|
80
|
+
pts = [(float(p[0]), float(p[1])) for p in points]
|
|
81
|
+
if paths is None:
|
|
82
|
+
return _BdPolygon(*pts, align=None)
|
|
83
|
+
faces = [
|
|
84
|
+
_BdPolygon(*[pts[i] for i in path], align=None) for path in paths
|
|
85
|
+
]
|
|
86
|
+
outer = faces[0]
|
|
87
|
+
for hole in faces[1:]:
|
|
88
|
+
outer -= hole
|
|
89
|
+
return outer
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_FONT_STYLES = {
|
|
93
|
+
"bold": FontStyle.BOLD,
|
|
94
|
+
"italic": FontStyle.ITALIC,
|
|
95
|
+
"bold italic": FontStyle.BOLDITALIC,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_HALIGN = {"left": Align.MIN, "center": Align.CENTER, "right": Align.MAX}
|
|
99
|
+
_VALIGN = {
|
|
100
|
+
"baseline": Align.MIN,
|
|
101
|
+
"bottom": Align.MIN,
|
|
102
|
+
"center": Align.CENTER,
|
|
103
|
+
"top": Align.MAX,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def text(
|
|
108
|
+
text: str,
|
|
109
|
+
size: float = 10,
|
|
110
|
+
font: str | None = None,
|
|
111
|
+
halign: str = "left",
|
|
112
|
+
valign: str = "baseline",
|
|
113
|
+
spacing: float = 1,
|
|
114
|
+
direction: str = "ltr",
|
|
115
|
+
language: str | None = None,
|
|
116
|
+
script: str | None = None,
|
|
117
|
+
segments: int | None = None,
|
|
118
|
+
) -> Shape:
|
|
119
|
+
kwargs: dict[str, object] = {
|
|
120
|
+
"align": (_HALIGN[halign], _VALIGN[valign]),
|
|
121
|
+
}
|
|
122
|
+
if font is not None:
|
|
123
|
+
font_path = find_font_path(font)
|
|
124
|
+
if font_path is not None:
|
|
125
|
+
kwargs["font_path"] = font_path
|
|
126
|
+
else:
|
|
127
|
+
family, style = parse_font_spec(font)
|
|
128
|
+
kwargs["font"] = family
|
|
129
|
+
if style is not None:
|
|
130
|
+
kwargs["font_style"] = _FONT_STYLES.get(
|
|
131
|
+
style.lower(), FontStyle.REGULAR
|
|
132
|
+
)
|
|
133
|
+
return _BdText(text, font_size=size, **kwargs)
|
solid123d/render.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Drop-in replacements for SolidPython's render functions.
|
|
2
|
+
|
|
3
|
+
There is no OpenSCAD source to render, so ``scad_render_to_file`` exports
|
|
4
|
+
real geometry instead: a ``.scad`` filename is transparently rewritten to
|
|
5
|
+
``.step``. ``.step``/``.stp``/``.stl`` filenames are exported as-is.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import warnings
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from build123d import Shape
|
|
12
|
+
from build123d import export_step as _export_step
|
|
13
|
+
from build123d import export_stl as _export_stl
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def scad_render_to_file(
|
|
17
|
+
scad_object: Shape,
|
|
18
|
+
filepath: str | Path | None = None,
|
|
19
|
+
out_dir: str | Path = "",
|
|
20
|
+
file_header: str = "",
|
|
21
|
+
include_orig_code: bool = True,
|
|
22
|
+
filename: str | Path | None = None,
|
|
23
|
+
**kwargs: object,
|
|
24
|
+
) -> str:
|
|
25
|
+
path = Path(filepath if filepath is not None else (filename or "output.step"))
|
|
26
|
+
if out_dir:
|
|
27
|
+
path = Path(out_dir) / path
|
|
28
|
+
suffix = path.suffix.lower()
|
|
29
|
+
if suffix == ".scad":
|
|
30
|
+
path = path.with_suffix(".step")
|
|
31
|
+
warnings.warn(
|
|
32
|
+
f"solid123d cannot write OpenSCAD source; exporting STEP to "
|
|
33
|
+
f"{path} instead",
|
|
34
|
+
stacklevel=2,
|
|
35
|
+
)
|
|
36
|
+
suffix = ".step"
|
|
37
|
+
if path.parent != Path("."):
|
|
38
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
if suffix == ".stl":
|
|
40
|
+
_export_stl(scad_object, str(path))
|
|
41
|
+
elif suffix in (".step", ".stp"):
|
|
42
|
+
_export_step(scad_object, str(path))
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"unsupported export format: {path.suffix}")
|
|
45
|
+
return str(path)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def scad_render(scad_object: Shape, file_header: str = "") -> str:
|
|
49
|
+
raise NotImplementedError(
|
|
50
|
+
"solid123d builds real geometry, not OpenSCAD source; use "
|
|
51
|
+
"scad_render_to_file() to export STEP/STL, or pass the object to "
|
|
52
|
+
"build123d viewers/exporters directly"
|
|
53
|
+
)
|
solid123d/transforms.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""OpenSCAD transformations as callables: ``translate(v)(shape, ...)``.
|
|
2
|
+
|
|
3
|
+
Each function returns a callable that accepts one or more shapes
|
|
4
|
+
(children are implicitly unioned, as in an OpenSCAD block) and returns
|
|
5
|
+
a transformed build123d shape.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable, Sequence
|
|
9
|
+
|
|
10
|
+
from build123d import Axis, Color, Kind, Plane, Pos, Shape
|
|
11
|
+
from build123d import mirror as _bd_mirror
|
|
12
|
+
from build123d import offset as _bd_offset
|
|
13
|
+
from build123d import scale as _bd_scale
|
|
14
|
+
|
|
15
|
+
from ._common import group, vec3
|
|
16
|
+
|
|
17
|
+
Applier = Callable[..., Shape]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def translate(v: Sequence[float]) -> Applier:
|
|
21
|
+
x, y, z = vec3(v)
|
|
22
|
+
|
|
23
|
+
def apply(*children: Shape) -> Shape:
|
|
24
|
+
return Pos(x, y, z) * group(children)
|
|
25
|
+
|
|
26
|
+
return apply
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def rotate(
|
|
30
|
+
a: float | Sequence[float] | None = None,
|
|
31
|
+
v: Sequence[float] | None = None,
|
|
32
|
+
) -> Applier:
|
|
33
|
+
def apply(*children: Shape) -> Shape:
|
|
34
|
+
shape = group(children)
|
|
35
|
+
if a is not None and not isinstance(a, (int, float)):
|
|
36
|
+
# rotate([x, y, z]): about global X, then Y, then Z (OpenSCAD order)
|
|
37
|
+
ax, ay, az = vec3(a)
|
|
38
|
+
for axis, angle in ((Axis.X, ax), (Axis.Y, ay), (Axis.Z, az)):
|
|
39
|
+
if angle:
|
|
40
|
+
shape = shape.rotate(axis, angle)
|
|
41
|
+
return shape
|
|
42
|
+
angle = float(a) if a is not None else 0.0
|
|
43
|
+
if v is not None:
|
|
44
|
+
return shape.rotate(Axis((0, 0, 0), tuple(vec3(v))), angle)
|
|
45
|
+
return shape.rotate(Axis.Z, angle)
|
|
46
|
+
|
|
47
|
+
return apply
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def scale(v: float | Sequence[float]) -> Applier:
|
|
51
|
+
# OpenSCAD pads a short scale vector with 1 (identity), not 0
|
|
52
|
+
factors = vec3(v, default=1.0)
|
|
53
|
+
|
|
54
|
+
def apply(*children: Shape) -> Shape:
|
|
55
|
+
return _bd_scale(group(children), by=factors)
|
|
56
|
+
|
|
57
|
+
return apply
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def mirror(v: Sequence[float]) -> Applier:
|
|
61
|
+
normal = vec3(v)
|
|
62
|
+
|
|
63
|
+
def apply(*children: Shape) -> Shape:
|
|
64
|
+
return _bd_mirror(
|
|
65
|
+
group(children), about=Plane(origin=(0, 0, 0), z_dir=normal)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return apply
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resize(
|
|
72
|
+
newsize: Sequence[float],
|
|
73
|
+
auto: bool | Sequence[bool] = False,
|
|
74
|
+
) -> Applier:
|
|
75
|
+
target = vec3(newsize)
|
|
76
|
+
|
|
77
|
+
def apply(*children: Shape) -> Shape:
|
|
78
|
+
shape = group(children)
|
|
79
|
+
bbox = shape.bounding_box()
|
|
80
|
+
current = (bbox.size.X, bbox.size.Y, bbox.size.Z)
|
|
81
|
+
autos = (
|
|
82
|
+
(auto, auto, auto) if isinstance(auto, bool) else tuple(auto)
|
|
83
|
+
)
|
|
84
|
+
factors = [
|
|
85
|
+
t / c if t != 0 and c != 0 else 0.0
|
|
86
|
+
for t, c in zip(target, current)
|
|
87
|
+
]
|
|
88
|
+
first = next((f for f in factors if f != 0), 1.0)
|
|
89
|
+
resolved = tuple(
|
|
90
|
+
f if f != 0 else (first if autos[i] else 1.0)
|
|
91
|
+
for i, f in enumerate(factors)
|
|
92
|
+
)
|
|
93
|
+
return _bd_scale(shape, by=resolved)
|
|
94
|
+
|
|
95
|
+
return apply
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def color(c: str | Sequence[float], alpha: float = 1.0) -> Applier:
|
|
99
|
+
if isinstance(c, str):
|
|
100
|
+
col = Color(c, alpha=alpha)
|
|
101
|
+
else:
|
|
102
|
+
vals = [float(x) for x in c]
|
|
103
|
+
if len(vals) == 3:
|
|
104
|
+
vals.append(alpha)
|
|
105
|
+
col = Color(*vals)
|
|
106
|
+
|
|
107
|
+
def apply(*children: Shape) -> Shape:
|
|
108
|
+
shape = group(children)
|
|
109
|
+
shape.color = col
|
|
110
|
+
return shape
|
|
111
|
+
|
|
112
|
+
return apply
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def offset(
|
|
116
|
+
r: float | None = None,
|
|
117
|
+
delta: float | None = None,
|
|
118
|
+
chamfer: bool = False,
|
|
119
|
+
) -> Applier:
|
|
120
|
+
amount = r if r is not None else delta
|
|
121
|
+
if amount is None:
|
|
122
|
+
raise ValueError("offset() requires r= or delta=")
|
|
123
|
+
kind = Kind.ARC if r is not None else Kind.INTERSECTION
|
|
124
|
+
|
|
125
|
+
def apply(*children: Shape) -> Shape:
|
|
126
|
+
return _bd_offset(group(children), float(amount), kind=kind)
|
|
127
|
+
|
|
128
|
+
return apply
|
solid123d/utils.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Equivalents of the common ``solid.utils`` directional helpers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from build123d import Shape
|
|
6
|
+
|
|
7
|
+
from .transforms import translate
|
|
8
|
+
|
|
9
|
+
Applier = Callable[..., Shape]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def up(z: float) -> Applier:
|
|
13
|
+
return translate([0, 0, z])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def down(z: float) -> Applier:
|
|
17
|
+
return translate([0, 0, -z])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def right(x: float) -> Applier:
|
|
21
|
+
return translate([x, 0, 0])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def left(x: float) -> Applier:
|
|
25
|
+
return translate([-x, 0, 0])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def forward(y: float) -> Applier:
|
|
29
|
+
return translate([0, y, 0])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def back(y: float) -> Applier:
|
|
33
|
+
return translate([0, -y, 0])
|
|
@@ -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,13 @@
|
|
|
1
|
+
solid123d/__init__.py,sha256=NZd_91C_UWjEVaaZxdhg8RJI8Obm1nYLl98oNvWqrcY,1316
|
|
2
|
+
solid123d/_common.py,sha256=rB9calVf6avQziTmiwRk64JDm-B0NGaBm6GJgSqTIqs,1282
|
|
3
|
+
solid123d/booleans.py,sha256=NKH_YbGkieXLcYtuL5P0VwD-0-QQooKU78g67S187PM,1714
|
|
4
|
+
solid123d/extrusions.py,sha256=acr6zcuTivfYj6ZXfHD4oXc7y1gosfOaq0UCxWZmN0A,1821
|
|
5
|
+
solid123d/fonts.py,sha256=zxPjPRcVPvh4nDqPOmQ1Q3iIsj-EtHBTLBVut1Voi58,3519
|
|
6
|
+
solid123d/primitives.py,sha256=mkmwPfkLai6v-m3R_M7Qf8WgFdTnG1iKzF8CbEf0Q1M,3986
|
|
7
|
+
solid123d/render.py,sha256=kFyxcURTuh_6y63oQItFX79GZALCyij6vQRqX6loC-g,1768
|
|
8
|
+
solid123d/transforms.py,sha256=cabcGBNDWJ7nhl4Hz8RYYZTU9ydBW7k0vKni00geho8,3592
|
|
9
|
+
solid123d/utils.py,sha256=lJV0zJUty968N6dJmadB4O6Lixb0I1c92ldAWRZrEgY,598
|
|
10
|
+
solid123d-0.1.0.dist-info/METADATA,sha256=EyYeTYzssMZgVZkaouXe-kYz6g5Qyy0QucHzJijYopU,3906
|
|
11
|
+
solid123d-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
12
|
+
solid123d-0.1.0.dist-info/licenses/LICENSE,sha256=rKfFnDmiHmGemb3Gk1_n_a-oCJYLysQuALeK3lmGn94,1067
|
|
13
|
+
solid123d-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|