# fichier : cgal_pycad.py
from fractions import Fraction
from typing import List, Optional, Union
try:
from . import _cgal_pycad
except ImportError:
# Fallback for local development if .so is in the same directory but not installed as package
import _cgal_pycad
# ==============================
# Utilitaires de conversion exactes
# ==============================
def _to_fraction(val: Union[int, Fraction, tuple, float]) -> Fraction:
if isinstance(val, Fraction):
return val
elif isinstance(val, int):
return Fraction(val)
elif isinstance(val, tuple) and len(val) == 2:
num, denom = val
return Fraction(num, denom)
elif isinstance(val, float):
raise TypeError("Floats are strictly forbidden in exact mode. Use Fraction or (num, den).")
else:
raise TypeError(f"Unsupported type {type(val)}; must be int, Fraction, or (num, denom)")
def _to_exact_str(val: Union[int, Fraction, tuple]) -> str:
f = _to_fraction(val)
return f"{f.numerator}/{f.denominator}"
def _to_tolerance_str(val: Union[int, Fraction, tuple, float]) -> str:
if isinstance(val, float):
f = Fraction(val).limit_denominator(1000000)
return f"{f.numerator}/{f.denominator}"
return _to_exact_str(val)
def _to_fraction_list(vals: List[Union[int, Fraction, tuple]]) -> List[Fraction]:
return [_to_fraction(v) for v in vals]
def _to_exact_str_list(vals: List[Union[int, Fraction, tuple]]) -> List[str]:
return [_to_exact_str(v) for v in vals]
# ==============================
# Classe wrapper Solid
# ==============================
[docs]
class Solid:
"""
Wrapper class for CGAL exact Nef Polyhedrons.
This class represents a 3D solid using exact rational arithmetic. It is immutable;
transformations and boolean operations return new Solid instances.
Attributes:
_solid: The underlying C++ object (implementation detail).
"""
def __init__(self, base: _cgal_pycad.Solid = None):
"""
Initialize a Solid. Users should generally use primitive functions (cube, sphere)
instead of calling this directly.
Args:
base: Internal C++ Solid object. If None, creates an empty solid.
"""
if base is None:
self._solid = _cgal_pycad.Solid()
else:
self._solid = base
# ------------------------
# Transformations exactes
# ------------------------
[docs]
def translate(self, vec: List[Union[int, Fraction, tuple]]) -> "Solid":
"""
Translate the solid by a vector.
Args:
vec: A 3-element list/tuple [dx, dy, dz]. Elements must be exact types
(int, Fraction, or (num, den) tuple). Floats are forbidden.
Returns:
A new translated Solid instance.
Raises:
ValueError: If vec is not length 3.
TypeError: If elements are floats.
Example:
>>> s = cube([10, 10, 10])
>>> s_moved = s.translate([5, 0, 0])
"""
if len(vec) != 3: raise ValueError("Translation vector must be size 3")
strs = _to_exact_str_list(vec)
return Solid(self._solid.translate(strs[0], strs[1], strs[2]))
[docs]
def rotate(self, axis: List[Union[int, Fraction, tuple]], angle: Union[int, Fraction, tuple, float]) -> "Solid":
"""
Rotate the solid around an axis by an angle.
Args:
axis: A 3-element vector [x, y, z] defining the axis of rotation.
angle: The rotation angle in degrees. Can be exact (int, Fraction) or
float (automatically converted to high-precision rational).
Returns:
A new rotated Solid instance. The result is a valid rational Nef Polyhedron,
approximating the rotation matrix to high precision if necessary.
Raises:
ValueError: If axis is not length 3.
Example:
>>> s = cube([10, 10, 10])
>>> # Rotate 45 degrees around Z axis
>>> s_rot = s.rotate([0, 0, 1], 45)
"""
if len(axis) != 3: raise ValueError("Rotation axis must be size 3")
ax_strs = _to_exact_str_list(axis)
ang_str = _to_tolerance_str(angle)
return Solid(self._solid.rotate(ax_strs[0], ax_strs[1], ax_strs[2], ang_str))
[docs]
def mirror(self, plane: List[Union[int, Fraction, tuple]]) -> "Solid":
"""
Mirror the solid across a plane defined by ax + by + cz + d = 0.
Args:
plane: A 4-element list [a, b, c, d].
Returns:
A new mirrored Solid instance.
Raises:
ValueError: If plane is not length 4.
"""
if len(plane) != 4: raise ValueError("Plane must be defined by 4 coefficients [a,b,c,d]")
strs = _to_exact_str_list(plane)
return Solid(self._solid.mirror(strs[0], strs[1], strs[2], strs[3]))
[docs]
def scale(self, vec: List[Union[int, Fraction, tuple]]) -> "Solid":
"""
Scale the solid by factors along X, Y, Z.
Args:
vec: A 3-element list [sx, sy, sz].
Returns:
A new scaled Solid instance.
"""
if len(vec) != 3: raise ValueError("Scale vector must be size 3")
strs = _to_exact_str_list(vec)
return Solid(self._solid.scale(strs[0], strs[1], strs[2]))
[docs]
def resize(self, x: Optional[Union[int, Fraction, tuple]]=None,
y: Optional[Union[int, Fraction, tuple]]=None,
z: Optional[Union[int, Fraction, tuple]]=None):
"""
Resize the solid to specific dimensions (Bounding Box).
**Not Implemented**: Currently issues a warning and returns self.
Args:
x: Target size in X.
y: Target size in Y.
z: Target size in Z.
Returns:
self (unchanged).
"""
print("Warning: resize() not fully implemented in backend yet.")
return self
# ------------------------
# Booléens exacts
# ------------------------
[docs]
def union(self, other: "Solid") -> "Solid":
"""
Compute the boolean union of this solid and another.
Args:
other: The other Solid to unite with.
Returns:
A new Solid representing the union.
"""
return Solid(self._solid.union(other._solid))
[docs]
def intersect(self, other: "Solid") -> "Solid":
"""
Compute the boolean intersection of this solid and another.
Args:
other: The other Solid to intersect with.
Returns:
A new Solid representing the intersection.
"""
return Solid(self._solid.intersect(other._solid))
[docs]
def difference(self, other: "Solid") -> "Solid":
"""
Compute the boolean difference (self - other).
Args:
other: The Solid to subtract.
Returns:
A new Solid representing the difference.
"""
return Solid(self._solid.difference(other._solid))
# ------------------------
# Surcharge d’opérateurs
# ------------------------
def __add__(self, other: "Solid"):
"""Operator overload for union (self + other)."""
return self.union(other)
def __mul__(self, other: "Solid"):
"""Operator overload for intersection (self * other)."""
return self.intersect(other)
def __sub__(self, other: "Solid"):
"""Operator overload for difference (self - other)."""
return self.difference(other)
# ------------------------
# Export
# ------------------------
[docs]
def to_stl(self, path: str):
"""
Export the solid to an STL file.
Args:
path: Target file path (e.g., "output.stl").
"""
self._solid.to_stl(path)
[docs]
def to_off(self, path: str):
"""
Export the solid to an OFF file.
Args:
path: Target file path (e.g., "output.off").
"""
self._solid.to_off(path)
# ==============================
# Primitives OpenSCAD-style
# ==============================
[docs]
def cube(size: List[Union[int, Fraction, tuple]], center=False) -> Solid:
"""
Create a cube (rectangular algorithm).
Args:
size: [x, y, z] dimensions. Must be strict exact types (int, Fraction).
center: If True, the cube is centered at the origin. Otherwise, it starts at [0,0,0] extends to +x,+y,+z.
Returns:
A Solid representing the cube.
Example:
>>> c = cube([10, 20, 5], center=True)
"""
if len(size) != 3: raise ValueError("Cube size must be [x,y,z]")
strs = _to_exact_str_list(size)
return Solid(_cgal_pycad.cube(strs[0], strs[1], strs[2], center))
[docs]
def sphere(r: Union[int, Fraction, tuple], tolerance: Union[int, Fraction, tuple, float]=Fraction(1,1000)) -> Solid:
"""
Create a sphere approximation using a convex hull of points.
Args:
r: Radius. Strict exact type.
tolerance: Maximum distance error from ideal sphere surface.
Floats allowed here (convenience). Controls resolution.
Smaller tolerance = more vertices = higher quality.
Returns:
A Solid representing the polyhedral sphere.
"""
r_str = _to_exact_str(r)
tol_str = _to_tolerance_str(tolerance)
return Solid(_cgal_pycad.sphere(r_str, tol_str))
[docs]
def cylinder(r: Union[int, Fraction, tuple], h: Union[int, Fraction, tuple],
tolerance: Union[int, Fraction, tuple, float]=Fraction(1,1000), center=False) -> Solid:
"""
Create a cylinder approximation.
Args:
r: Radius.
h: Height.
tolerance: Error tolerance for circular approximation.
center: If True, centered along Z (from -h/2 to h/2). If False, from 0 to h.
Returns:
A Solid representing the cylinder.
"""
r_str = _to_exact_str(r)
h_str = _to_exact_str(h)
tol_str = _to_tolerance_str(tolerance)
return Solid(_cgal_pycad.cylinder(r_str, h_str, tol_str, center))
[docs]
def cone(r1: Union[int, Fraction, tuple], r2: Union[int, Fraction, tuple], h: Union[int, Fraction, tuple],
tolerance: Union[int, Fraction, tuple, float]=Fraction(1,1000), center=False) -> Solid:
"""
Create a cone (truncated).
Args:
r1: Bottom radius.
r2: Top radius.
h: Height.
tolerance: Error tolerance.
center: If True, centered along Z.
Returns:
A Solid representing the cone.
"""
r1_str = _to_exact_str(r1)
r2_str = _to_exact_str(r2)
h_str = _to_exact_str(h)
tol_str = _to_tolerance_str(tolerance)
return Solid(_cgal_pycad.cone(r1_str, r2_str, h_str, tol_str, center))
# ==============================
# Functional Wrappers (DSL)
# ==============================
[docs]
class Modifier:
"""Helper for functional-style transformations (e.g. translate(v)(obj))."""
def __init__(self, func, *args):
self.func = func
self.args = args
def __call__(self, solid: Solid) -> Solid:
return getattr(solid, self.func)(*self.args)
[docs]
def translate(vec: List[Union[int, Fraction, tuple]]) -> Modifier:
"""
Functional wrapper for translation.
Usage:
>>> obj = cube([10,10,10])
>>> moved = translate([5,0,0])(obj)
"""
return Modifier("translate", vec)
[docs]
def rotate(axis: List[Union[int, Fraction, tuple]], angle: Union[int, Fraction, tuple, float]) -> Modifier:
"""
Functional wrapper for rotation.
Usage:
>>> obj = cube([10,10,10])
>>> rotated = rotate([0,0,1], 45)(obj)
"""
return Modifier("rotate", axis, angle)
[docs]
def scale(vec: List[Union[int, Fraction, tuple]]) -> Modifier:
"""Functional wrapper for scaling."""
return Modifier("scale", vec)
[docs]
def mirror(plane: List[Union[int, Fraction, tuple]]) -> Modifier:
"""Functional wrapper for mirroring."""
return Modifier("mirror", plane)
[docs]
def resize(x=None, y=None, z=None) -> Modifier:
"""Functional wrapper for resizing (Not Implemented)."""
return Modifier("resize", x, y, z)