# Import: from scadpy import * # Color constants: BEIGE, BLACK, BLUE, BROWN, DARK_GRAY, DEFAULT_COLOR, DEFAULT_OPACITY, GRAY, GREEN, ORANGE, RED, WHITE, YELLOW [Assembly] type TopologyFilter[A] = NDArray[np.bool_] | Callable[[A], NDArray[np.bool_]] Vertex/topology filter type used by transforms such as translate, rotate, chamfer, etc. A TopologyFilter restricts a transform to a subset of vertices. Pass either: - A boolean NumPy array of shape (n_vertices,) — True = apply transform to that vertex. - A callable that receives the assembly and returns such a boolean array. Examples: s.translate(x(3), vertex_filter=s.are_vertices_convex) s.chamfer(1.0, vertex_filter=lambda s: s.vertex_angles > 90) circle(radius: float, segment_count: int = 64) -> Shape Create a circle approximated by a polygon. >>> # circle of radius 5 with default resolution (64 vertices) >>> x = circle(5) >>> len(x.vertex_coordinates) 64 >>> # circle of radius 3 with low resolution >>> x = circle(3, segment_count=8) >>> coords = x.vertex_coordinates[:4] >>> coords.round(2) array([[ 3. , 0. ], [ 2.12, 2.12], [ 0. , 3. ], [-2.12, 2.12]]) >>> # invalid circle (radius <= 0) >>> circle(0) Traceback (most recent call last): ... ValueError: Circle radius must be strictly positive. polygon(points: Iterable[Iterable[float]]) -> Shape Create a 2D polygon shape from a sequence of points. This function constructs a Shape from the given 2D points. The polygon is automatically closed by connecting the last point to the first. If the polygon is self-intersecting, it may be split into multiple parts. >>> # simple triangle >>> p = polygon([[0, 0], [1, 0], [0, 1]]) >>> p.vertex_coordinates array([[0., 0.], [1., 0.], [0., 1.]]) >>> # square defined manually >>> # equivalent to square(2) >>> p = polygon([[-1, -1], [1, -1], [1, 1], [-1, 1]]) >>> p.vertex_coordinates array([[-1., -1.], [ 1., -1.], [ 1., 1.], [-1., 1.]]) >>> # self-intersecting polygon generate multiple parts >>> p = polygon([[0, 0], [2, 2], [0, 2], [2, 0]]) >>> vtop = p.vertex_to_part[:, np.newaxis] >>> stacked = np.hstack([vtop, p.vertex_coordinates]) >>> stacked array([[0., 2., 0.], [0., 1., 1.], [0., 0., 0.], [1., 2., 2.], [1., 0., 2.], [1., 1., 1.]]) rectangle(size: Iterable[float]) -> Shape Create a rectangle centered at the origin. >>> # rectangle 4 units wide and 2 units tall >>> x = rectangle([4, 2]) >>> x.vertex_coordinates array([[-2., -1.], [ 2., -1.], [ 2., 1.], [-2., 1.]]) >>> # equivalent to square(10) >>> x = rectangle([10, 10]) >>> x.vertex_coordinates array([[-5., -5.], [ 5., -5.], [ 5., 5.], [-5., 5.]]) >>> # if one or no dimension is provided, >>> # the missing value defaults to 1.0 >>> x = rectangle([5]) >>> x.vertex_coordinates array([[-2.5, -0.5], [ 2.5, -0.5], [ 2.5, 0.5], [-2.5, 0.5]]) square(size: float) -> Shape Creates a square centered at the origin. This function is a convenience wrapper around rectangle, with equal width and height. >>> # square of size 1x1 >>> x = square(1) >>> x.vertex_coordinates array([[-0.5, -0.5], [ 0.5, -0.5], [ 0.5, 0.5], [-0.5, 0.5]]) >>> # square of size 5x5 >>> x = square(5) >>> x.vertex_coordinates array([[-2.5, -2.5], [ 2.5, -2.5], [ 2.5, 2.5], [-2.5, 2.5]]) text(content: str, font: str | None = None, size: float = 10, curve_segments: int = 12) -> Shape Create a 2D shape from a text string. Each glyph is traced from the font outlines and converted to a polygon. Holes (e.g. inside ``o``, ``e``, ``a``) are handled via symmetric difference (XOR) across all contours. >>> text("ScadPy", font="DejaVu Sans", size=20) resize_sweep(end_size: list[float | None], start_size: list[float | None] | None = None) -> Callable[[NDArray[np.float64], float], NDArray[np.float64]] Return a strategy that linearly resizes the cross-section along the path. >>> path = np.column_stack([np.zeros(10), np.zeros(10), np.linspace(0, 20, 10)]) >>> circle(5).path_extrude(path, strategy=resize_sweep(end_size=[2, 8])) # squish to 2×8 >>> circle(5).path_extrude(path, strategy=resize_sweep(end_size=[None, 2])) # proportional height reverse_sweep(strategy: Callable[[NDArray[np.float64], float], NDArray[np.float64]]) -> Callable[[NDArray[np.float64], float], NDArray[np.float64]] Wrap a strategy so it runs in reverse (``t=0`` becomes ``t=1``). >>> path = np.column_stack([np.zeros(10), np.zeros(10), np.linspace(0, 20, 10)]) >>> circle(5).path_extrude(path, strategy=reverse_sweep(scale_sweep(end=0.2))) # taper from tip rotate_sweep(angle: float, start_angle: float = 0.0) -> Callable[[NDArray[np.float64], float], NDArray[np.float64]] Return a strategy that linearly rotates the cross-section along the path. >>> path = np.column_stack([np.zeros(20), np.zeros(20), np.linspace(0, 30, 20)]) >>> rectangle([4, 2]).path_extrude(path, strategy=rotate_sweep(angle=360)) # full twist >>> rectangle([4, 2]).path_extrude(path, strategy=rotate_sweep(angle=90)) # quarter twist scale_sweep(end: float | list[float], start: float | list[float] = 1.0) -> Callable[[NDArray[np.float64], float], NDArray[np.float64]] Return a strategy that linearly scales the cross-section along the path. >>> path = np.column_stack([np.zeros(10), np.zeros(10), np.linspace(0, 20, 10)]) >>> circle(5).path_extrude(path, strategy=scale_sweep(end=0.2)) # taper to 20% >>> circle(3).path_extrude(path, strategy=scale_sweep(end=[2.0, 0.5])) # squash Y [Shape] A 2D assembly of Polygon parts. ``Shape`` is the central 2D modeling object in ScadPy. It wraps one or more colored Shapely polygons and exposes a fluent API for boolean operations, geometric transforms, topology queries, and 2D→3D extrusions. Use the primitives (circle, rectangle, square, …) or importers (from_dxf, from_svg) to create shapes; do not instantiate this class directly. .dimensions() -> int Return the number of spatial dimensions: always ``2``. >>> Shape.dimensions() 2 .vertex_coordinates -> NDArray[np.float64] For each vertex in the shape, return its coordinates. >>> polygon = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]) >>> Shape.from_geometry(polygon).vertex_coordinates array([[0., 0.], [2., 0.], [2., 2.], [0., 2.]]) .vertex_to_part -> NDArray[np.int64] For each vertex in the shape, return its part index. >>> p1 = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]) >>> p2 = Polygon([(10, 10), (12, 10), (12, 12), (10, 12)]) >>> Shape.from_geometries([p1, p2]).vertex_to_part array([0, 0, 0, 0, 1, 1, 1, 1]) .recoordinate(vertex_coordinates: NDArray[np.float64]) -> Shape Rebuild this shape with new vertex coordinates. .is_empty -> bool Return whether the shape has no vertices. >>> Shape.from_parts([]).is_empty True >>> polygon = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]) >>> Shape.from_geometry(polygon).is_empty False .bounds -> NDArray[np.float64] Return the axis-aligned bounding box of the shape. >>> polygon = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]) >>> Shape.from_geometry(polygon).bounds array([0., 0., 2., 2.]) .bounding_box -> Shape Return the axis-aligned bounding box of the shape as a rectangle. >>> polygon = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]) >>> Shape.from_geometry(polygon).bounding_box.bounds array([0., 0., 2., 2.]) .centroid -> NDArray[np.float64] Return the geometric centroid of the shape, weighted by part area. >>> polygon = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]) >>> Shape.from_geometry(polygon).centroid array([1., 1.]) .__add__(other: Shape) -> Shape [+] Concatenate two shapes. .__or__(other: Shape) -> Shape [|] Unite two shapes. .__and__(other: Shape) -> Shape [&] Intersect two shapes. .__sub__(other: Shape) -> Shape [-] Subtract a shape from this shape. .__xor__(other: Shape) -> Shape [^] Compute symmetric difference with another shape. .concat(shapes: Sequence[Shape]) -> Shape Concatenate this shape with others (no boolean merge). >>> square(4).concat([circle(2).translate([3, 2])]) .unify(shapes: Sequence[Shape]) -> Shape Unite this shape with others. >>> square(4).unify([circle(2).translate([2, 0])]) .intersect(shapes: Sequence[Shape]) -> Shape Intersect this shape with others. >>> square(4).intersect([circle(2).translate([1, 1])]) .subtract(other: Shape) -> Shape Subtract a shape from this shape. >>> square(4).subtract(circle(1)) .exclude(shapes: Sequence[Shape]) -> Shape Compute the symmetric difference of this shape with others. >>> square(4).exclude([circle(2).translate([1, 1])]) .part_colors -> NDArray[np.float64] .ring_to_part -> NDArray[np.int64] For each ring in the shape, return its part index. >>> # square with a hole (2 rings in part 0) >>> # unioned with a separate square (1 ring in part 1) >>> shape = (square(2) - square(1)) | square(1).translate([5, 0]) >>> shape.ring_to_part array([0, 0, 1]) .ring_types -> NDArray[np.object_] For each ring in the shape, return its type ('exterior' or 'interior'). >>> # square with a hole (exterior + interior) >>> # unioned with a separate square (exterior only) >>> shape = (square(2) - square(1)) | square(1).translate([5, 0]) >>> shape.ring_types array(['exterior', 'interior', 'exterior'], dtype=object) .vertex_to_ring -> NDArray[np.int64] For each vertex in the shape, return the index of the ring it belongs to. >>> # two separate triangles: 3 vertices in ring 0, 3 in ring 1 >>> shape = ( ... polygon([(0, 0), (1, 0), (0.5, 1)]) ... | polygon([(5, 0), (6, 0), (5.5, 1)]) ... ) >>> shape.vertex_to_ring array([0, 0, 0, 1, 1, 1]) >>> # square with a hole: 4 exterior vertices in ring 0, >>> # 4 interior vertices in ring 1 >>> (square(4) - square(2)).vertex_to_ring array([0, 0, 0, 0, 1, 1, 1, 1]) .vertex_to_neighbor_vertex -> NDArray[np.int64] For each vertex in the shape, return its two neighbor vertex indices (prev, next). >>> triangle = polygon([(0, 0), (1, 0), (0.5, 1)]) >>> triangle.vertex_to_neighbor_vertex array([[2, 1], [0, 2], [1, 0]]) .vertex_angles -> NDArray[np.float64] For each vertex in the shape, return its interior angle in degrees. >>> # arrow: 5 convex vertices + 2 concave vertices, >>> # all with different angles >>> arrow = polygon( ... [(0, 1), (3, 0), (5, 2), (3, 4), ... (0, 3), (1, 2.5), (1, 1.5)] ... ) >>> arrow.vertex_angles.round(2).tolist() [135.0, 63.43, 90.0, 63.43, 135.0, 63.43, 63.43] .are_vertices_convex -> NDArray[np.bool_] For each vertex in the shape, return whether it is convex. >>> # arrow: 5 convex vertices (tip and sides) >>> # + 2 concave vertices (the tail notch) >>> arrow = polygon( ... [(0, 1), (3, 0), (5, 2), (3, 4), ... (0, 3), (1, 2.5), (1, 1.5)] ... ) >>> arrow.are_vertices_convex.tolist() [True, True, True, True, True, False, False] .vertex_normals -> NDArray[np.float64] For each vertex in the shape, return its outward unit normal. >>> # arrow: 5 convex vertices (normals point outward) >>> # + 2 concave vertices (normals point inward, >>> # into the tail notch — x component is positive) >>> arrow = polygon( ... [(0, 1), (3, 0), (5, 2), (3, 4), ... (0, 3), (1, 2.5), (1, 1.5)] ... ) >>> arrow.vertex_normals.round(4) array([[-0.9975, -0.0709], [ 0.2298, -0.9732], [ 1. , 0. ], [ 0.2298, 0.9732], [-0.9975, 0.0709], [ 0.8507, 0.5257], [ 0.8507, -0.5257]]) .vertex_to_outgoing_directed_edge -> NDArray[np.int64] For each vertex in the shape, return the index of its outgoing directed edge. >>> # triangle: vertices (2,0,1), (0,1,2), (1,2,0) >>> # outgoing: 0→1, 1→2, 2→0 >>> triangle = polygon([(0, 0), (1, 0), (0.5, 1)]) >>> triangle.vertex_to_outgoing_directed_edge array([0, 2, 4]) .vertex_to_incoming_directed_edge -> NDArray[np.int64] For each vertex in the shape, return the index of its incoming directed edge. >>> # triangle: vertices (2,0,1), (0,1,2), (1,2,0) >>> # incoming: 2→0, 0→1, 1→2 >>> triangle = polygon([(0, 0), (1, 0), (0.5, 1)]) >>> triangle.vertex_to_incoming_directed_edge array([4, 0, 2]) .directed_edge_to_vertex -> NDArray[np.int64] For each directed edge in the shape, return the indices of its start and end vertices. >>> # triangle: 3 edges → 6 directed edges >>> # (forward/backward interleaved) >>> triangle = polygon([(0, 0), (1, 0), (0.5, 1)]) >>> triangle.directed_edge_to_vertex array([[0, 1], [1, 0], [1, 2], [2, 1], [2, 0], [0, 2]]) >>> # square: 4 edges → 8 directed edges >>> square(1).directed_edge_to_vertex.shape (8, 2) .directed_edge_to_edge -> NDArray[np.int64] For each directed edge in the shape, return the index of its parent undirected edge. >>> # triangle: 3 edges → 6 directed edges >>> triangle = polygon([(0, 0), (1, 0), (0.5, 1)]) >>> triangle.directed_edge_to_edge array([0, 0, 1, 1, 2, 2]) >>> # square: 4 edges → 8 directed edges >>> square(1).directed_edge_to_edge array([0, 0, 1, 1, 2, 2, 3, 3]) .directed_edge_directions -> NDArray[np.float64] For each directed edge in the shape, return its unit direction vector. >>> # unit square: 4 edges → 8 directed edges, >>> # forward then backward interleaved >>> square(1).directed_edge_directions.round(4) array([[ 1., 0.], [-1., 0.], [ 0., 1.], [ 0., -1.], [-1., 0.], [ 1., 0.], [ 0., -1.], [ 0., 1.]]) .edge_to_vertex -> NDArray[np.int64] For each edge in the shape, return the indices of its start and end vertices. >>> # triangle: 3 vertices, 3 edges >>> triangle = polygon([(0, 0), (1, 0), (0.5, 1)]) >>> triangle.edge_to_vertex array([[0, 1], [1, 2], [2, 0]]) >>> # square: 4 vertices, 4 edges >>> square(1).edge_to_vertex.shape (4, 2) .edge_lengths -> NDArray[np.float64] For each edge in the shape, return its length. >>> square(2).edge_lengths array([2., 2., 2., 2.]) .edge_midpoints -> NDArray[np.float64] For each edge in the shape, return the midpoint between its two vertices. >>> square(2).edge_midpoints array([[ 0., -1.], [ 1., 0.], [ 0., 1.], [-1., 0.]]) .edge_normals -> NDArray[np.float64] For each edge in the shape, return its outward unit normal. >>> # square(2) centered at origin: 4 edges, >>> # each normal points outward >>> square(2).edge_normals.round(4) array([[ 0., -1.], [ 1., -0.], [ 0., 1.], [-1., -0.]]) .from_parts(parts: Sequence[Part[Polygon]]) -> Shape Map a sequence of parts to a shape, repairing and orienting each polygon. >>> Shape.from_parts( ... [Part.from_geometry(Polygon([(0, 0), (4, 0), (4, 4), (0, 4)]))] ... ) .from_geometries(geometries: Sequence[Polygon]) -> Shape Map a sequence of polygons to a shape. >>> Shape.from_geometries( ... [Polygon([(0, 0), (4, 0), (4, 4), (0, 4)])] ... ) .from_geometry(geometry: Polygon) -> Shape Map a single polygon to a shape. >>> Shape.from_geometry( ... Polygon([(0, 0), (4, 0), (4, 4), (0, 4)]) ... ) .from_svg(source: str | Path) -> Shape Load a 2D shape from an SVG file or URL. >>> Shape.from_svg("https://upload.wikimedia.org/wikipedia/commons/0/04/Pentagon.svg") .from_dxf(source: str | Path) -> Shape Load a 2D shape from a DXF file or URL. >>> Shape.from_dxf("https://raw.githubusercontent.com/mikedh/trimesh/main/models/2D/wrench.dxf") .translate(translation: float | Iterable[float], vertex_filter: TopologyFilter[Shape] | None = None) -> Shape Translate this shape. >>> square(4).translate([3, 2]) .scale(scale: float | Iterable[float], pivot: float | Iterable[float] = 0, vertex_filter: TopologyFilter[Shape] | None = None) -> Shape Scale this shape. >>> square(4).scale(2, pivot=[2, 2]) .resize(size: Iterable[float | None], auto: bool = False, pivot: float | Iterable[float] | None = None, vertex_filter: TopologyFilter[Shape] | None = None) -> Shape Resize this shape to target dimensions. >>> # resize to an exact size on both axes >>> rectangle([4, 2]).resize([6, 6]) >>> # freeze one axis (None) to leave it unchanged >>> rectangle([4, 2]).resize([6, None]) >>> # scale frozen axes proportionally with auto=True >>> rectangle([4, 2]).resize([6, None], auto=True) .mirror(normal: float | Iterable[float], pivot: float | Iterable[float] = 0) -> Shape Mirror this shape. >>> square(4).mirror([1, 0], pivot=[2, 0]) .pull(distance: float, pivot: float | Iterable[float] = 0, vertex_filter: TopologyFilter[Shape] | None = None) -> Shape Move vertices of this shape toward a pivot point. >>> square(4).pull(1.0, pivot=[2, 2], vertex_filter=square(4).vertex_coordinates[:, 0] < 1) .push(distance: float, pivot: float | Iterable[float] = 0, vertex_filter: TopologyFilter[Shape] | None = None) -> Shape Move vertices of this shape away from a pivot point. >>> square(4).push(1.0, pivot=[2, 2], vertex_filter=square(4).vertex_coordinates[:, 0] < 1) .color(color: Color) -> Shape Set the color of all parts in this shape. >>> square(4).color(RED) .chamfer(size: float | np.ndarray, vertex_filter: TopologyFilter[Shape] | None = None, epsilon: float = 1e-08) -> Shape Chamfer the vertices of this shape. >>> sq = square(4) >>> l_shape = polygon( ... [(0, 0), (4, 0), (4, 2), (2, 2), (2, 4), (0, 4)] ... ) >>> arrow = polygon( ... [(0, 1), (3, 0), (5, 2), (3, 4), ... (0, 3), (1, 2.5), (1, 1.5)] ... ) >>> # all vertices >>> sq.chamfer(1.0) >>> # convex vertices only >>> l_shape.chamfer( ... 0.5, vertex_filter=lambda s: s.are_vertices_convex ... ) .fillet(size: float | np.ndarray, vertex_filter: TopologyFilter[Shape] | None = None, segment_count: int = 32, epsilon: float = 1e-08) -> Shape Fillet the vertices of this shape. >>> sq = square(4) >>> l_shape = polygon( ... [(0, 0), (4, 0), (4, 2), (2, 2), (2, 4), (0, 4)] ... ) >>> arrow = polygon( ... [(0, 1), (3, 0), (5, 2), (3, 4), ... (0, 3), (1, 2.5), (1, 1.5)] ... ) >>> # all vertices >>> sq.fillet(1.0) >>> # convex vertices only >>> l_shape.fillet( ... 0.5, vertex_filter=lambda s: s.are_vertices_convex ... ) .convexify() -> Shape Compute the convex hull of each part of this shape. >>> a = square(5) >>> b = square(2).translate(10) >>> c = square(3).translate([4, 8]) >>> (a + b + c).convexify() .fill() -> Shape Fill holes in this shape. >>> (square(10) - circle(3)).fill() .grow(distance: float) -> Shape Grow this shape outward by a given distance. >>> square(10).grow(2) >>> # shrink with negative distance >>> square(10).grow(-2) .linear_cut(axis: float | Iterable[float], pivot: float | Iterable[float] = 0) -> Shape Cut this shape with a half-plane. >>> shape = square(6) - circle(2) >>> # vertical cut along the Y-axis >>> shape.linear_cut([0, 1], pivot=[-1, 0]) >>> # diagonal cut >>> shape.linear_cut([1, 1]) .linear_extrude(height: float, intermediate_sections: int | None = None, strategy: list[Callable[[NDArray[np.float64], float], NDArray[np.float64]]] | Callable[[NDArray[np.float64], float], NDArray[np.float64]] | None = None) -> Solid Extrude this shape linearly along the Z-axis. >>> # simple box >>> square(10).linear_extrude(5) >>> # tube from a hollow circle >>> (circle(5) - circle(3)).linear_extrude(10) >>> # tapered box using scale_sweep strategy >>> square(10).linear_extrude(5, intermediate_sections=10, strategy=scale_sweep(0.5)) .linear_slice(thickness: float, direction: float | Iterable[float], pivot: float | Iterable[float] = 0) -> Shape Slice this shape with a linear band. >>> shape = square(10) - circle(3) >>> # horizontal slice through center >>> shape.linear_slice(3, [1, 0]) >>> # diagonal slice >>> shape.linear_slice(2, [1, 1]) .radial_extrude(axis: float | Iterable[float], start: float = 0, end: float = 360, pivot: float | Iterable[float] = 0, segment_count: int = 64) -> Solid Extrude this shape radially around an axis. >>> # torus: circle profile offset from the Y-axis, revolved 360° >>> circle(0.5).translate([2, 0]).radial_extrude([0, 1]) >>> # partial torus (270°) >>> circle(0.5).translate([2, 0]).radial_extrude([0, 1], end=270) .path_extrude(path: ArrayLike, fillet_segments: int | None = None, min_fillet_radius: float | None = None, intermediate_sections: int | None = None, strategy: list[Callable[[NDArray[np.float64], float], NDArray[np.float64]]] | Callable[[NDArray[np.float64], float], NDArray[np.float64]] | None = None) -> Solid Sweep this shape along a 3D path. >>> path = np.array([ ... [0, 0, 0], [10, 0, 0], [10, 4, 0], [10, 4, 8], ... [6, 4, 8], [6, 9, 8], [6, 9, 3], [0, 9, 3], ... [0, 0, 3], [0, 0, 10], ... ], dtype=float) >>> # hollow tube swept along a 3D polyline with light filleting >>> (circle(0.5, segment_count=6) - circle(0.4, segment_count=6)).path_extrude( ... path, fillet_segments=2, ... ) >>> # same path with scale, twist and intermediate sections >>> (circle(0.5, segment_count=6) - circle(0.4, segment_count=6)).path_extrude( ... path, ... fillet_segments=32, ... min_fillet_radius=100, ... intermediate_sections=50, ... strategy=[reverse_sweep(scale_sweep(3)), rotate_sweep(360)], ... ) .radial_slice(start: float = 0, end: float = 360, pivot: float | Iterable[float] = 0) -> Shape Slice this shape to keep only a radial sector. >>> shape = square(10) - circle(3) >>> # quarter slice >>> shape.radial_slice(0, 90) >>> # three-quarter slice >>> shape.radial_slice(45, 315) .rotate(angle: float, pivot: float | Iterable[float] = 0, vertex_filter: TopologyFilter[Shape] | None = None) -> Shape Rotate this shape. >>> square(4).rotate(45, pivot=[2, 2]) .shrink(distance: float) -> Shape Shrink this shape inward by a given distance. >>> square(10).shrink(2) >>> # negative distance expands outward >>> square(10).shrink(-2) .linear_pattern(counts: int | Sequence[int], steps: NDArray[np.float64] | Sequence[NDArray[np.float64]]) -> Shape Repeat this shape in a linear or grid pattern. >>> circle(1).linear_pattern(counts=4, steps=x(3)) >>> circle(1).linear_pattern(counts=[3, 2], steps=[x(3), y(3)]) .radial_pattern(count: int, angle: float = 360, pivot: float | Iterable[float] = 0) -> Shape Repeat this shape in a radial pattern around the origin. >>> circle(1).translate([3, 0]).radial_pattern(count=6) .to_screen(background_color: Color = WHITE, foreground_color: Color = BLACK) -> None Display a shape in a Qt-based window. >>> square(4).to_screen() .to_html(background_color: Color = WHITE, foreground_color: Color = BLACK) -> HTML Render a shape as an SVG HTML object. >>> html = square(4).to_html() >>> isinstance(html, HTML) True .to_html_file(path: str, background_color: Color = WHITE, foreground_color: Color = BLACK) -> int Save a shape as an HTML file. >>> square(4).to_html_file(path="output.html") .to_svg() -> str Export a shape to an SVG string. >>> svg = (square(4) - circle(1)).to_svg() >>> svg.startswith(" int Save a shape as an SVG file. >>> (square(4) - circle(1)).to_svg_file(path="output.svg") .to_dxf() -> str Export a shape to a DXF string. >>> dxf = (square(4) - circle(1)).to_dxf() >>> dxf.startswith("999") True .to_dxf_file(path: str | Path) -> int Save a shape as a DXF file. >>> (square(4) - circle(1)).to_dxf_file(path="output.dxf") cone(radius: float, height: float, section_count: int = 32) -> Solid Create a cone centered at the origin, apex pointing along +z. >>> cone(radius=2, height=4) >>> cone(radius=2, height=4, section_count=6) cuboid(size: float | Iterable[float]) -> Solid Create a box (rectangular cuboid) centered at the origin. >>> cuboid(4) >>> cuboid([4, 2, 1]) cylinder(radius: float, height: float, section_count: int = 32) -> Solid Create a cylinder centered at the origin, aligned along the z-axis. >>> cylinder(radius=2, height=4) >>> cylinder(radius=2, height=4, section_count=6) polyhedron(vertices: NDArray[np.float64], faces: NDArray[np.int64]) -> Solid Create a solid from raw vertex coordinates and triangular face indices. This is the base primitive constructor. All other solid primitives ultimately call this function with numpy-computed geometry. >>> polyhedron( ... vertices=np.array([ ... [ 1.0, 1.0, 1.0], ... [ 1.0, -1.0, -1.0], ... [-1.0, 1.0, -1.0], ... [-1.0, -1.0, 1.0], ... ]), ... faces=np.array([[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]], dtype=np.int64), ... ) sphere(radius: float, subdivision_count: int = 4) -> Solid Create a sphere approximated by an icosphere mesh. >>> sphere(radius=2) >>> sphere(radius=2, subdivision_count=1) [Solid] A 3D assembly of Trimesh parts. ``Solid`` is the central 3D modeling object in ScadPy. It wraps one or more colored Trimesh meshes and exposes a fluent API for boolean operations, geometric transforms, topology queries, and 3D export. Use the primitives (cuboid, cylinder, sphere, …) or importers (from_stl) to create solids; do not instantiate this class directly. .dimensions() -> int Return the number of spatial dimensions: always ``3``. >>> Solid.dimensions() 3 .vertex_coordinates -> NDArray[np.float64] For each vertex in the solid, return its coordinates. >>> vertex_coordinates = cuboid(2).vertex_coordinates >>> vertex_coordinates.shape (8, 3) .vertex_to_part -> NDArray[np.int64] For each vertex in the solid, return its part index. >>> solid = cuboid(2) + cuboid(2).translate(5) >>> solid.vertex_to_part array([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]) .is_empty -> bool Return whether the solid has no vertices. >>> Solid.from_parts([]).is_empty True >>> cuboid(2).is_empty False .bounds -> NDArray[np.float64] Return the axis-aligned bounding box of the solid. >>> cuboid(2).bounds array([-1., -1., -1., 1., 1., 1.]) .bounding_box -> Solid Return the axis-aligned bounding box of the solid as a cuboid. >>> cuboid(2).bounding_box.bounds array([-1., -1., -1., 1., 1., 1.]) .centroid -> NDArray[np.float64] Return the geometric centroid of the solid, weighted by part volume. >>> cuboid(2).centroid array([0., 0., 0.]) .part_colors -> NDArray[np.float64] For each part in the solid, return its RGBA color. >>> colors = cuboid(2).part_colors >>> colors.shape (1, 4) >>> bool(colors[0, 3] == DEFAULT_OPACITY) True .triangle_to_vertex -> NDArray[np.int64] For each triangle in the solid, return the indices of its three vertices. >>> triangle_to_vertex = cuboid(2).triangle_to_vertex >>> triangle_to_vertex.shape[1] 3 .__add__(other: Solid) -> Solid [+] Concatenate two solids. .__or__(other: Solid) -> Solid [|] Unite two solids. .__and__(other: Solid) -> Solid [&] Intersect two solids. >>> (cuboid(4) & sphere(3)).is_empty False .__sub__(other: Solid) -> Solid [-] Subtract a solid from this solid. .__xor__(other: Solid) -> Solid [^] Compute symmetric difference with another solid. >>> (cuboid(4) ^ sphere(3)).is_empty False .concat(solids: Sequence[Solid]) -> Solid Concatenate this solid with others without any boolean operation. >>> cuboid(4).concat([sphere(radius=2).translate([3, 2, 0])]) .unify(solids: Sequence[Solid]) -> Solid Unite this solid with others using boolean union. >>> cuboid(4).unify([sphere(radius=2).translate(x(2))]) .intersect(solids: Sequence[Solid]) -> Solid Compute the intersection of this solid with others. >>> cuboid(4).intersect([sphere(radius=2).translate(1)]) .subtract(to_subtract: Solid) -> Solid Subtract a solid from this solid using boolean difference. >>> cuboid(4).subtract(sphere(radius=2)) .exclude(solids: Sequence[Solid]) -> Solid Compute the symmetric difference (XOR) of this solid with others. >>> cuboid(4).exclude([cuboid(4).translate(x(2))]) .translate(translation: float | Iterable[float], vertex_filter: TopologyFilter[Solid] | None = None) -> Solid Translate this solid by a given vector. >>> cuboid(4).translate([3, 2, 1]) .scale(scale: float | Iterable[float], pivot: float | Iterable[float] = 0, vertex_filter: TopologyFilter[Solid] | None = None) -> Solid Scale this solid by a given factor, relative to a pivot point. >>> cuboid(4).scale(2, pivot=[2, 2, 2]) .resize(size: Iterable[float | None], auto: bool = False, pivot: float | Iterable[float] | None = None, vertex_filter: TopologyFilter[Solid] | None = None) -> Solid Resize this solid to fit target dimensions. >>> # resize to an exact size on all axes: >>> cuboid([4, 2, 1]).resize([6, 6, 6]) >>> # freeze two axes (``None``) and scale only the first: >>> cuboid([4, 2, 1]).resize([6, None, None]) >>> # scale frozen axes proportionally with ``auto=True``: >>> cuboid([4, 2, 1]).resize([6, None, None], auto=True) .mirror(normal: float | Iterable[float], pivot: float | Iterable[float] = 0) -> Solid Mirror this solid across a plane defined by a normal vector and a pivot point. >>> cuboid(4).mirror([1, 0, 0], pivot=[2, 0, 0]) .pull(distance: float, pivot: float | Iterable[float] = 0, vertex_filter: TopologyFilter[Solid] | None = None) -> Solid Move a subset of vertices of this solid toward a pivot point by a given distance. >>> cuboid(4).pull(distance=1.0, pivot=[2, 2, 2], vertex_filter=cuboid(4).vertex_coordinates[:, 0] < 1) .push(distance: float, pivot: float | Iterable[float] = 0, vertex_filter: TopologyFilter[Solid] | None = None) -> Solid Move a subset of vertices of this solid away from a pivot point by a given distance. >>> cuboid(4).push(distance=1.0, pivot=[2, 2, 2], vertex_filter=cuboid(4).vertex_coordinates[:, 0] < 1) .rotate(angle: float, axis: float | Iterable[float], pivot: float | Iterable[float] = 0, vertex_filter: TopologyFilter[Solid] | None = None) -> Solid Rotate this solid by a given angle around an axis passing through a pivot point. >>> cuboid(4).rotate(angle=45, axis=[0, 0, 1], pivot=[2, 2, 2]) .color(color: Color) -> Solid Set the color of all parts in this solid. >>> cuboid(4).color(RED) .convexify(part_filter: TopologyFilter[Solid] | None = None) -> Solid Create a new solid whose selected parts are replaced by their convex hull. >>> (cuboid(4) + sphere(radius=2).translate([3, 3, 3])).convexify() .recoordinate(vertex_coordinates: NDArray[np.float64]) -> Solid Rebuild this solid with new vertex coordinates, preserving topology and colors. >>> cuboid(4).recoordinate(cuboid(4).vertex_coordinates + [2.0, 1.0, 0.0]) .linear_pattern(counts: int | Sequence[int], steps: NDArray[np.float64] | Sequence[NDArray[np.float64]]) -> Solid Repeat this solid in a linear or grid pattern. >>> sphere(1).linear_pattern(counts=4, steps=x(3)) >>> sphere(1).linear_pattern(counts=[3, 2], steps=[x(3), y(3)]) >>> sphere(1).linear_pattern(counts=[3, 2, 4], steps=[x(3), y(3), z(4)]) .radial_pattern(count: int, axis: float | Iterable[float], angle: float = 360, pivot: float | Iterable[float] = 0) -> Solid Repeat this solid in a radial pattern around an axis. >>> sphere(1).translate(x(3)).radial_pattern(count=6, axis=z()) .from_parts(parts: Sequence[Part[Trimesh]]) -> Solid Assemble a Solid from a sequence of Part. This is the low-level constructor used internally. In most cases you should use primitives or boolean operations instead. >>> Solid.from_parts([]).is_empty True .from_geometries(geometries: Sequence[Trimesh]) -> Solid Map a sequence of Trimesh geometries to a solid. >>> Solid.from_geometries([cuboid(4)._parts[0].geometry]) .from_geometry(geometry: Trimesh) -> Solid Map a single Trimesh geometry to a solid. >>> Solid.from_geometry(cuboid(4)._parts[0].geometry) .from_stl(source: str | Path) -> Solid Load a solid from an STL file. >>> Solid.from_stl("model.stl") .to_html(background_color: Color = WHITE, foreground_color: Color = BLACK) -> HTML Render this solid as an interactive HTML widget. >>> html = cuboid(4).to_html() >>> isinstance(html, HTML) True .to_html_file(path: str, background_color: Color = WHITE, foreground_color: Color = BLACK) -> int Save this solid as an HTML file. >>> cuboid(4).to_html_file(path="output.html") .to_screen(background_color: Color = WHITE, foreground_color: Color = BLACK) -> None Display this solid in an interactive viewer. >>> cuboid(4).to_screen() .to_stl_file(path: str | Path) -> int Export this solid to an STL file. >>> cuboid(4).to_stl_file(path="output.stl") x(n: float = 1) -> NDArray[np.float64] Create a vector with ``n`` on the X-axis and ``nan`` on Y and Z. ``nan`` acts as a sentinel meaning "keep the current value" when this vector is passed to transforms such as translate_shape or rotate_solid. Only the X component is specified; the other axes are left untouched. >>> x(5) array([ 5., nan, nan]) >>> x(-2.3) array([-2.3, nan, nan]) y(n: float = 1) -> NDArray[np.float64] Create a vector with ``n`` on the Y-axis and ``nan`` on X and Z. ``nan`` acts as a sentinel meaning "keep the current value" when this vector is passed to transforms such as translate_shape or rotate_solid. Only the Y component is specified; the other axes are left untouched. >>> y(5) array([nan, 5., nan]) >>> y(-2.3) array([ nan, -2.3, nan]) z(n: float = 1) -> NDArray[np.float64] Create a vector with ``n`` on the Z-axis and ``nan`` on X and Y. ``nan`` acts as a sentinel meaning "keep the current value" when this vector is passed to transforms such as translate_shape or rotate_solid. Only the Z component is specified; the other axes are left untouched. >>> z(5) array([nan, nan, 5.]) >>> z(-2.3) array([ nan, nan, -2.3])