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 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
@@ -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())))
@@ -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
+ )
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.