Metadata-Version: 2.4
Name: ScadPy
Version: 0.2.11
Summary: Simplified 2D/3D modeling for Python with fluent API and boolean operations
Author: m-fabregue
License: # MIT License
        
        Copyright (c) 2026 m-fabregue
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
        ---
        
        ## Third-party dependencies
        
        ScadPy depends on the following third-party libraries, each distributed under
        their own terms:
        
        | Package | License |
        |---------|---------|
        | [trimesh](https://github.com/mikedh/trimesh) | MIT |
        | [Shapely](https://github.com/shapely/shapely) | BSD 3-Clause |
        | [typeguard](https://github.com/agronholm/typeguard) | MIT |
        | [IPython](https://github.com/ipython/ipython) | BSD 3-Clause |
        | [PySide6](https://doc.qt.io/qtforpython) | LGPL v3 |
        | [triangle](https://github.com/drufat/triangle) | See note below |
        
        **Note on `triangle`:** The `triangle` package wraps Jonathan Shewchuk's
        Triangle library, which restricts commercial use without written permission
        from the author. Users intending commercial applications should review
        [Triangle's license](https://www.cs.cmu.edu/~quake/triangle.html) and
        consider replacing the `triangle` dependency accordingly.
License-File: LICENSE.md
Keywords: 2d,3d,modeling,python,scad,shape,solid
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.12
Requires-Dist: ipython~=8.30
Requires-Dist: manifold3d==3.2.1
Requires-Dist: matplotlib==3.10.6
Requires-Dist: pyside6~=6.10
Requires-Dist: triangle==20250106
Requires-Dist: trimesh[easy]~=4.10
Requires-Dist: typeguard~=4.4
Provides-Extra: dev
Requires-Dist: coverage; extra == 'dev'
Requires-Dist: furo==2025.7.19; extra == 'dev'
Requires-Dist: genbadge[coverage]; extra == 'dev'
Requires-Dist: interrogate; extra == 'dev'
Requires-Dist: numpydoc; extra == 'dev'
Requires-Dist: sphinx; extra == 'dev'
Requires-Dist: sphinx-autodoc-typehints; extra == 'dev'
Requires-Dist: sphinx-pyproject; extra == 'dev'
Description-Content-Type: text/markdown

# ScadPy

[![PyPI](https://img.shields.io/pypi/v/scadpy)](https://pypi.org/project/scadpy/)
[![CI](https://img.shields.io/github/actions/workflow/status/m-fabregue/scadpy/ci.yml?branch=main&label=CI)](https://github.com/m-fabregue/scadpy/actions)
![coverage](https://m-fabregue.github.io/scadpy/_static/badges/coverage.svg)
![doc coverage](https://m-fabregue.github.io/scadpy/_static/badges/interrogate.svg)

**Programmatic CAD in Pure Python.** — [Documentation](https://m-fabregue.github.io/scadpy/)

ScadPy provides a fluent, type-safe API for 2D and 3D parametric modeling,
built on [Shapely](https://shapely.readthedocs.io) and
[trimesh](https://trimesh.org).
Write designs with the conciseness of OpenSCAD and the full power of Python.

## Installation

```bash
pip install scadpy
```

Requirements: Python ≥ 3.12.

## Quick examples

```python
# 2D — chamfered mounting plate
from scadpy import rectangle, circle, text
import numpy as np

PLATE_WIDTH   = 80
PLATE_HEIGHT  = 50
PLATE_THICKNESS = 10
HOLE_RADIUS   = 4
HOLE_MARGIN   = 10
CHAMFER_SIZE  = 8

base  = rectangle([PLATE_WIDTH, PLATE_HEIGHT])
plate = base.chamfer(CHAMFER_SIZE)

for position, normal in zip(base.vertex_coordinates, base.vertex_normals):
    hole_center = position - HOLE_MARGIN * np.sqrt(2) * normal
    plate -= circle(HOLE_RADIUS).translate(hole_center)

plate.to_screen()
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/m-fabregue/scadpy/main/docs/_static/examples/plate_dark.png">
  <img src="https://raw.githubusercontent.com/m-fabregue/scadpy/main/docs/_static/examples/plate_light.png" alt="chamfered mounting plate" width="400">
</picture>

```python
# 3D — extruded mounting plate with label (continues from above)
TEXT_THICKNESS = 2

extruded_plate = plate.linear_extrude(PLATE_THICKNESS)
label = text("ScadPy", size=15).linear_extrude(TEXT_THICKNESS)
extruded_plate |= label.translate(z(PLATE_THICKNESS))
extruded_plate.to_screen()
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/m-fabregue/scadpy/main/docs/_static/examples/extruded_plate_dark.png">
  <img src="https://raw.githubusercontent.com/m-fabregue/scadpy/main/docs/_static/examples/extruded_plate_light.png" alt="chamfered mounting plate" width="400">
</picture>

```python
# 3D — parametric ball bearing
from scadpy import circle, rectangle, sphere, x, y, GRAY, ORANGE
import numpy as np

BALL_RADIUS    = 3
RACE_RADIUS    = 15
NB_BALLS       = 11
CLEARANCE      = 0.1
RING_HEIGHT    = 7
RACE_THICKNESS = 10

groove  = circle(BALL_RADIUS + CLEARANCE) | rectangle([BALL_RADIUS, RING_HEIGHT])
race    = rectangle([RACE_THICKNESS, RING_HEIGHT]) - groove
bearing = race.radial_extrude(axis=y(), pivot=x(RACE_RADIUS)).color(GRAY)

ball = sphere(BALL_RADIUS).color(ORANGE)
for angle in np.linspace(0, 360, NB_BALLS, endpoint=False):
    bearing += ball.rotate(angle, axis=y(), pivot=x(RACE_RADIUS))

bearing.to_screen()
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/m-fabregue/scadpy/main/docs/_static/examples/bearing_dark.png">
  <img src="https://raw.githubusercontent.com/m-fabregue/scadpy/main/docs/_static/examples/bearing_light.png" alt="parametric ball bearing" width="400">
</picture>

```python
# 3D — dice
from scadpy import cuboid, sphere, x, y, z

SIZE = 20
dice = cuboid(SIZE)
pip  = sphere(SIZE / 12).translate(z(SIZE / 2))

one   = pip
two   = pip.translate([SIZE / 4, SIZE / 4, 0]) + pip.translate([-SIZE / 4, -SIZE / 4, 0])
three = one + two
four  = two + two.rotate(90, z())
five  = one + four
six   = four + pip.translate(x(SIZE / 4)) + pip.translate(x(-SIZE / 4))

dice -= (
    one
    + two.rotate(90, x())
    + three.rotate(90, y())
    + four.rotate(-90, y())
    + five.rotate(-90, x())
    + six.rotate(-180, x())
)

dice.to_screen()
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/m-fabregue/scadpy/main/docs/_static/examples/dice_dark.png">
  <img src="https://raw.githubusercontent.com/m-fabregue/scadpy/main/docs/_static/examples/dice_light.png" alt="chamfered mounting plate" width="400">
</picture>

```python
# 3D — storage box
from scadpy import square, x, z

SIZE_OUTER      = 20
SIZE_INNER      = 18
FILLET          = 1
BASE_HEIGHT     = 10
CUT_HEIGHT      = 8
CAP_HEIGHT_OUTER = 1.5
CAP_HEIGHT_INNER = 3
CAP_OFFSET_X    = 25
CUT_OFFSET_Z    = 2

outer_base = square(SIZE_OUTER).fillet(FILLET).linear_extrude(BASE_HEIGHT)
inner_cut  = square(SIZE_INNER).linear_extrude(CUT_HEIGHT).translate(z(CUT_OFFSET_Z))
base       = outer_base - inner_cut

cap_outer = square(SIZE_OUTER).fillet(FILLET).linear_extrude(CAP_HEIGHT_OUTER)
cap_inner = square(SIZE_INNER).linear_extrude(CAP_HEIGHT_INNER)
cap       = (cap_outer | cap_inner).translate(x(CAP_OFFSET_X))

storage_box = base + cap
storage_box.to_screen()
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/m-fabregue/scadpy/main/docs/_static/examples/storage_box_dark.png">
  <img src="https://raw.githubusercontent.com/m-fabregue/scadpy/main/docs/_static/examples/storage_box_light.png" alt="chamfered mounting plate" width="400">
</picture>

## Cheat sheet

*Parameters shown in `# comments` are optional, with their default values.*

**2D — Shape**

```python
from scadpy import *

# primitives
circle(radius=3)                                # segment_count=64
polygon(points=[(-2, -2), (2, -2), (0, 2)])
rectangle(size=[6, 3])
Shape.from_dxf("file.dxf")
Shape.from_svg("file.svg")
square(size=4)

# boolean operations
s = square(size=4);  c = circle(radius=3)
s | c    # union
s - c    # difference
s & c    # intersection
s ^ c    # symmetric difference
s + c    # concat (no merge)

# transforms
s.chamfer(size=0.8)              # vertex_filter=None, epsilon=1e-8
s.color(color=RED)
s.convexify()                    # part_filter=None
s.fill()                         # part_filter=None
s.fillet(size=0.8)               # vertex_filter=None, segment_count=32, epsilon=1e-8
s.grow(distance=0.5)             # part_filter=None
s.linear_cut(axis=x())          # pivot=0
s.linear_slice(thickness=2, direction=x())  # pivot=0, part_filter=None
s.mirror(normal=[1, 0])          # pivot=0
s.pull(distance=1.0)             # pivot=0, vertex_filter=None
s.push(distance=1.0)             # pivot=0, vertex_filter=None
s.radial_slice(start=0, end=180) # pivot=0, part_filter=None
s.resize(size=[6, None])         # auto=False, pivot=None, vertex_filter=None
s.rotate(angle=30)               # pivot=0, vertex_filter=None
s.scale(scale=[2, 0.5])          # pivot=0, vertex_filter=None
s.shrink(distance=0.5)           # part_filter=None
s.translate(translation=[2, 1])  # vertex_filter=None

# topology — coordinates & attributes
s.are_vertices_convex            # (n_vertices,)   — convexity mask
s.directed_edge_directions       # (2*n_edges, 2)
s.edge_lengths                   # (n_edges,)
s.edge_midpoints                 # (n_edges,  2)
s.edge_normals                   # (n_edges,  2)
s.ring_types                     # (n_rings,)  — "exterior"|"interior"
s.vertex_angles                  # (n_vertices,)   — interior angles (°)
s.vertex_coordinates             # (n_vertices, 2)
s.vertex_normals                 # (n_vertices, 2) — outward unit normals

# topology — bridges (*_to_*)
s.directed_edge_to_edge             # directed_edge → edge
s.directed_edge_to_vertex           # directed_edge → [start, end]
s.edge_to_vertex                    # edge          → [start, end]
s.ring_to_part                      # ring          → part
s.vertex_to_incoming_directed_edge  # vertex        → directed_edge
s.vertex_to_outgoing_directed_edge  # vertex        → directed_edge
s.vertex_to_neighbor_vertex       # vertex        → [prev, next]
s.vertex_to_part                    # vertex        → part
s.vertex_to_ring                    # vertex        → ring

# extrusions → Solid
s.linear_extrude(height=3)
s.radial_extrude(axis=y(), pivot=x(5))  # start=0, end=360, segment_count=64

# export
s.to_dxf_file("output.dxf")
s.to_html_file("output.html")
s.to_screen()
s.to_svg_file("output.svg")
```

**3D — Solid**

```python
from scadpy import *

# primitives
cone(radius=2, height=4)         # section_count=32
cuboid(size=[4, 3, 2])
cylinder(radius=2, height=4)     # section_count=32
polyhedron(vertices=vertices, faces=faces)
sphere(radius=3)                 # subdivision_count=4
Solid.from_stl("model.stl")

# boolean operations
a = cuboid(size=[4, 3, 2]);  b = sphere(radius=2)
a | b    # union
a - b    # difference
a & b    # intersection
a ^ b    # symmetric difference
a + b    # concat (no merge)

# transforms
a.color(color=RED)
a.convexify()                    # part_filter=None
a.mirror(normal=[1, 0, 0])       # pivot=0
a.pull(distance=1.0)             # pivot=0, vertex_filter=None
a.push(distance=1.0)             # pivot=0, vertex_filter=None
a.resize(size=[6, None, None])   # auto=False, pivot=None, vertex_filter=None
a.rotate(angle=30, axis=z())    # pivot=0, vertex_filter=None
a.scale(scale=[2, 1, 0.5])       # pivot=0, vertex_filter=None
a.translate(translation=[1, 0, 0])  # vertex_filter=None

# topology — coordinates & bridges (*_to_*)
a.triangle_to_vertex    # triangle → [v0, v1, v2]
a.vertex_coordinates    # (n_vertices,  3)
a.vertex_to_part        # vertex   → part

# export
a.to_html_file("output.html")
a.to_screen()
a.to_stl_file("output.stl")
```

## Roadmap

- Improve documentation
- Richer topology for Shape and Solid
- Richer transformations for Shape and Solid
- Chamfer and fillet on Solid
- New assembly types: `PointCloud2d`, `Wire2d`, `PointCloud3d`, `Wire3d`
- Better error messages
- More import/export formats

## Development

```bash
# Create and activate venv
python3 -m venv .venv
source .venv/bin/activate

# Install with dev dependencies
pip install -e .[dev]

# Run doctests & generate documentation
cd docs && make doctest && make html
```

## License

See [LICENSE.md](LICENSE.md).
