cgal_pycad — CGAL PyCAD 0.2.0 documentation

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

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

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

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

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

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

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

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

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

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

        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)

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

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

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

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

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

    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)

    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)

    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)

    def scale(vec: List[Union[int, Fraction, tuple]]) -> Modifier:
        """Functional wrapper for scaling."""
        return Modifier("scale", vec)

    def mirror(plane: List[Union[int, Fraction, tuple]]) -> Modifier:
        """Functional wrapper for mirroring."""
        return Modifier("mirror", plane)

    def resize(x=None, y=None, z=None) -> Modifier:
        """Functional wrapper for resizing (Not Implemented)."""
        return Modifier("resize", x, y, z)