Skip to content

Latest commit

 

History

History
165 lines (141 loc) · 9.01 KB

File metadata and controls

165 lines (141 loc) · 9.01 KB

Learn PicoPie in Y minutes

The whole API as one annotated script (learn-x-in-y-minutes style). Every line is real, runnable code. Headless by default; the viewer bits need a display.

# ============================================================================
# INSTALL    pip install "picopie[viz]"      (or: uv add "picopie[viz]")
#            Self-contained wheels: native runtime + deps bundled. No .NET/compiler.
# ============================================================================

import math
import numpy as np
import picopie
from picopie import (Voxels, Mesh, Lattice, ScalarField, VectorField,
                    Metadata, save_vdb, load_vdb)

# ---- SESSION ---------------------------------------------------------------
# One session = one native instance with a fixed voxel size (the resolution, mm).
picopie.init(voxel_size_mm=0.3)        # call once before modeling
picopie.version()                      # "26.2.0"
picopie.voxel_size()                   # 0.3
# ... or scope it:  with picopie.session(0.3): ...     # auto init + shutdown

# ---- VOXELS: the core object (a signed-distance / level-set volume) ---------
# value <= 0 is INSIDE. Two primitives: sphere and capsule.
ball = Voxels.sphere(center=(0, 0, 0), radius=10)
rod  = Voxels.capsule((-15, 0, 0), (15, 0, 0), radius=3)          # rounded cylinder
cone = Voxels.capsule((0, 0, -8), (0, 0, 8), radius=6, radius2=1) # tapered

# ---- BOOLEANS --------------------------------------------------------------
# Operators return a NEW volume (a, b unchanged):
union = ball + rod        # or ball | rod
cut   = ball - rod
both  = ball & rod
# Trailing-underscore methods mutate IN PLACE (faster, no copy):
part = Voxels.sphere(radius=10)
part.bool_subtract_(Voxels.sphere(center=(8, 0, 0), radius=8))    # also: part -= other
# Convention everywhere:  name_()  = in place;  name()  = returns new.

# ---- OFFSET / SHELL --------------------------------------------------------
grown = part.offset(2.0)              # grow 2 mm (new);  part.offset_(-1) shrinks in place
part.shell_(1.5)                      # hollow to a 1.5 mm wall (in place)
part.double_offset_(2.0, -2.0)        # grow-then-shrink = rounding/fillet (NOT a hollow)

# ---- IMPLICIT MODELING: define a shape by f(x,y,z) <= 0 --------------------
def gyroid(x, y, z):
    k = 2 * math.pi / 6.0             # 6 mm cell
    g = (math.sin(k*x)*math.cos(k*y) + math.sin(k*y)*math.cos(k*z)
         + math.sin(k*z)*math.cos(k*x))
    return max(abs(g) - 0.4,                                  # thin walls ...
               math.sqrt(x*x + y*y + z*z) - 12)              # ... clipped to a sphere
v = Voxels()
v.render_implicit_(gyroid, ((-13, -13, -13), (13, 13, 13)))  # sdf runs once PER VOXEL
# Compose with max()=intersect, min()=union, -f=complement, INSIDE one callback.
# (Keep the bbox tight: a pure-Python sdf over a big/fine box is the slow path.)

# ---- QUERIES ---------------------------------------------------------------
part.volume_mm3()                     # fast raw volume
vol, bbox = part.calculate_properties()   # accurate volume + surface bbox (matches C#)
part.is_inside((0, 0, 0))             # bool
part.closest_point((20, 0, 0))       # nearest surface point -> np.array (or None)
part.surface_normal((10, 0, 0))      # outward normal at a surface point
part.ray_cast((20, 0, 0), (-1, 0, 0))   # first surface hit along a ray (or None)
part.bounding_box()                   # voxel-grid bbox (looser than the surface bbox)

# ---- MESH <-> VOXELS -------------------------------------------------------
mesh = part.to_mesh()                 # marching cubes
mesh.vertices                         # (N,3) float32 NumPy  | mesh.triangles (M,3) int32
mesh.save_stl("part.stl")             # or .save_obj(...)
back = Voxels.from_mesh(mesh)         # voxelize a closed mesh
imported = Mesh.load_stl("in.stl")    # binary or ASCII; also Mesh.load_obj
tet = Mesh.from_arrays(np.zeros((3, 3), np.float32), np.array([[0, 1, 2]], np.int32))

# ---- LATTICE (beams + nodes) -> voxels -------------------------------------
lat = Lattice()
lat.add_sphere((-10, 0, 0), 2.0); lat.add_sphere((10, 0, 0), 2.0)
lat.add_beam((-10, 0, 0), (10, 0, 0), 1.0, 1.0)     # start, end, r_start, r_end
struts = lat.to_voxels()

# ---- FIELDS (data over space) ----------------------------------------------
f = ScalarField.from_voxels(part)     # one float per active voxel
f.set((0, 0, 0), 100.0); f.get((0, 0, 0))          # single point (get -> None if inactive)
pts  = np.array([(0, 0, 0), (5, 0, 0)], np.float32)
f.set_many(pts, np.array([1.0, 2.0], np.float32))  # BULK = fast; prefer over loops
vals, found = f.get_many(pts)                       # (N,) float32, (N,) bool
vf = VectorField.from_voxels(part); vf.set((0, 0, 0), (1, 0, 0))   # 3-vector per voxel

# ---- METADATA (typed annotations: str / float / 3-vector) ------------------
md = Metadata.from_voxels(part)
md["material"] = "Ti-6Al-4V"; md["wall_mm"] = 1.25; md["dir"] = (0, 0, 1)
md["material"]; "wall_mm" in md; md.to_dict()
# Names starting with "PicoGK." are reserved -> writing one raises ValueError.

# ---- FILE I/O (OpenVDB; voxels + fields, one file) -------------------------
save_vdb("model.vdb", body=part, heat=f)            # name=object
objs = load_vdb("model.vdb")                         # {"body": Voxels, "heat": ScalarField}

# ---- HEADLESS VISUALIZATION  (needs picopie[viz]) --------------------------
from picopie import save_slice_png, save_slice_sheet, mesh_preview
save_slice_png(part, "z.png", axis="z", mode="sdf")   # mask | sdf | gray
save_slice_sheet(part, "sheet.png", count=16)          # montage of slices
mesh_preview(part.to_mesh(), "preview.png")            # matplotlib 3D preview

# ---- INTERACTIVE VIEWER  (needs a display; main thread only) ---------------
picopie.show(part)                     # window: left-drag orbit, scroll zoom, Esc close
picopie.render_png(part, "render.png", size=(1920, 1080))   # offscreen one-shot
from picopie import Viewer
with Viewer() as viewer:              # 'with' guarantees cleanup (close() is required)
    viewer.add(part).set_group_material(0, (0.35, 0.6, 0.95))
    viewer.screenshot("shot.png")
    # viewer.run()                    # blocks until the window closes

# ---- PARAMETRIC SHAPES  (picopie.shapes: a Pythonic ShapeKernel port) -------
from picopie.shapes import (Sphere, Box, Cylinder, Cone, Ring, Lens, Pipe,
                           PipeSegment, Revolve, LocalFrame, Frames)
# Build engineering primitives from a local frame; .to_voxels() / .to_mesh().
f = LocalFrame(position=(0, 0, 0))            # position + orthonormal x/y/z axes
ball   = Sphere(f, radius=10).to_voxels()
box    = Box(f, length=20, width=10, depth=8).to_voxels()
cyl    = Cylinder(f, length=30, radius=8).to_voxels()
cone   = Cone(f, 30, start_radius=8, end_radius=2).to_voxels()
ring   = Ring(f, ring_radius=20, radius=5).to_voxels()          # torus
tube   = Pipe(f, length=30, inner_radius=4, outer_radius=8).to_voxels()  # hollow

# Dimensions accept a number, a numpy lambda, or a Modulation (the parametric part):
wavy   = Sphere(radius=lambda phi, theta: 20 + 3 * np.cos(6 * phi)).to_voxels()
fluted = Cylinder(f, 40, radius=lambda phi, lr: 10 - 3 * np.cos(8 * lr)).to_voxels()

# SPINED: follow a curve. Frames carry local frames along a spline.
from picopie.shapes import ControlPointSpline
spine  = Frames.aligned_to_x(ControlPointSpline([[0,0,0],[0,40,0],[0,50,30]]).points(500),
                             target_x=(0, 1, 0))
bent   = Pipe(frames=spine, inner_radius=3, outer_radius=6).to_voxels()

# LATTICES + IMPLICITS (SDFs you can render or intersect):
from picopie.shapes import LatticePipe, ImplicitGyroid
lat    = LatticePipe(f, length=60, radius=8).to_voxels()        # tube of beams
gyroid = ImplicitGyroid(unit_size=3, thickness_ratio=1).intersect(ball)  # TPMS-filled

# MEASURE + COLOUR:
from picopie.shapes import measure as me, Palette
print(me.volume(ball), me.surface_area(ball), me.centre_of_gravity(ball))
picopie.show(box) if False else None           # colours are RGB 0..1 (Palette.BLUE, ...)

# ---- RELIABILITY -----------------------------------------------------------
# Native errors are CATCHABLE, never fatal:
from picopie._errors import PicoPieError, InvalidHandleError
try:
    part.intersect_implicit_(lambda x, y, z: x)   # sharp edge: prefer composing in render_implicit_
except PicoPieError as e:
    print(e)
# NaN/inf geometry inputs raise ValueError up front (would otherwise segfault/hang).
# Wrong-typed args raise TypeError. Using a closed object raises InvalidHandleError.

# ---- LIFETIME --------------------------------------------------------------
part.close()                          # optional for most objects (GC frees them);
                                      # REQUIRED for Viewer (use 'with' or run()).
picopie.shutdown()                     # end the session (also runs atexit)

For deeper, narrative coverage see the tutorials: Novice · Intermediate · Advanced · Shapes, and the gallery.