Source code for cgal_pycad

# 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)