(guide-metamaterials-conformal)= # Conformal mapping CAD surfaces ```{include} ../_guide-sidebar-compiler-membership.md ``` A conformal lattice lets a repeated structure follow a curved surface instead of a rectangular block. You still choose the lattice pattern — for example, a gyroid sheet or an octet beam network — and the cell map decides where each cell sits in the finished shape. Beam radii and wall thicknesses remain millimetre-based controls. This guide uses **exact CAD faces**: the surface information already stored in a CAD model supplies the map. If your starting surface is a triangle mesh, such as an STL, a scan, or a generated mesh patch, use [Conformal mapping triangle meshes](conformal-meshes.md) instead. Both workflows can be combined because they use the same surface interface; the general `mm.cell_map_from_surface(...)` and `mm.cell_map_between_surfaces(...)` functions accept either surface type. The CAD-specific functions below are convenient names for the same operations. ## Install the optional CAD authoring stack You only need CadQuery when authoring surfaces in Python. It is optional because its CAD engine and display dependencies are sizeable: ```bash pip install "OpenVCAD[cad]" ``` `pyvcad` imports CadQuery only when you call `pv.CADModel.from_cadquery(...)`. ## How a face becomes a cell map For one face, the map uses U and V to move across the surface, and W to move away from it. W follows the face's direction (its normal) for the requested signed `height`: a positive height extends in the normal direction, while a negative height extends inward.
Logical grid of cells, the grid bent over one CAD face, and the grid spanning two CAD faces
The left grid is the reusable cell layout. The middle strip bends it over one face, and the right strip fills the gap between two faces.
Logical coordinates run from `[0, nu] × [0, nv] × [0, nw]`; each whole-number interval is one cell. A periodic direction, such as the wraparound direction of a cylinder, stays connected at its seam. For trimmed faces, `trim` controls how the map meets the real boundary: the default `trim="boundary"` clips smoothly to the edge curve, `trim="cells"` keeps or removes whole cells, and `trim="none"` leaves the map untrimmed. See the {ref}`three trimming modes ` for a visual comparison. Each comparison below uses one fixed orthographic camera. Slide the handle: the left image shows the CAD face that was selected, and the right image shows the lattice created from it. ## Author and select a CadQuery face Start with a cylinder, because its curved side makes the effect easy to see. CadQuery creates the cylinder; the `%CYLINDER` selector keeps its side face rather than either flat end. Converting that selected CadQuery face gives OpenVCAD the surface used to place the lattice. ```python import cadquery as cq import pyvcad as pv import pyvcad_metamaterials as mm import pyvcad_rendering as viz # Make a cylinder, then keep its curved side face as the mapping surface. cad = cq.Workplane("XY").circle(20.0).extrude(30.0) surface = pv.CADModel.from_cadquery(cad.faces("%CYLINDER")).faces[0] # Wrap a 24-by-10 gyroid sheet around the face, 4 mm along its normal. cell_map = mm.cell_map_from_cad_face( surface, cells=(24, 10, 1), height=4.0, linear=False, ) # The gyroid is the repeated open-sheet structure placed by the cell map. root = mm.gyroid(cell_map, wall_thickness=0.65) viz.Render(root) ```
Conformal gyroid mapped onto a cylindrical face
Gyroid mapped onto a cylindrical face Selected cylindrical CAD face Selected cylindrical face Conformal gyroid
The selected-face view is the plain cylindrical wall; the mapped view is a 24-by-10 gyroid sheet wrapped around it. `linear=False` keeps extra samples within curved cells, which preserves the cylinder more faithfully than a straight-sided approximation. Run the complete script: [`12_cadquery_conformal_gyroid.py`](../../../../examples/metamaterials/conformal_cad/12_cadquery_conformal_gyroid.py). ## Map between two faces Use two faces when the lattice must fill a curved shell and both sides matter. Here the inner and outer cylinder walls become the two W boundaries, rather than extending a fixed distance from one face. ```python import cadquery as cq import pyvcad as pv import pyvcad_metamaterials as mm import pyvcad_rendering as viz # These two cylindrical side faces bound the shell that will contain the lattice. inner_cad = cq.Workplane("XY").circle(15.0).extrude(30.0) outer_cad = cq.Workplane("XY").circle(22.0).extrude(30.0) inner = pv.CADModel.from_cadquery(inner_cad.faces("%CYLINDER")).faces[0] outer = pv.CADModel.from_cadquery(outer_cad.faces("%CYLINDER")).faces[0] # Fill the gap with 24-by-10-by-3 octet cells instead of offsetting one face. cell_map = mm.cell_map_between_cad_faces( inner, outer, cells=(24, 10, 3), ) # Grow the octet beams from the inner face (0.15 mm) to the outer face (0.25 mm). beam_radius = cell_map.logical_position.z.map_range(0.0, 3.0, 0.15, 0.25) root = mm.octet(cell_map, beam_radius=beam_radius) viz.Render(root) ```
Octet lattice mapped between inner and outer cylindrical faces
Octet lattice mapped between cylindrical faces Selected paired cylindrical faces Selected face pair Octet lattice between faces
The selected-face view shows the orange inner wall and blue outer wall. The mapped view fills their gap with an octet beam network; its beams grow from 0.15 mm at the inner wall to 0.25 mm at the outer wall. OpenVCAD normally finds a compatible surface direction and cylinder seam automatically. Use explicit origins and flips only when you need an exact, repeatable seam location. See [`13_cadquery_paired_face_octet.py`](../../../../examples/metamaterials/conformal_cad/13_cadquery_paired_face_octet.py). ## Import STEP and select a face Use a STEP model when the surface was made elsewhere. `pv.CADModel` loads it once and keeps stable face references. You can select a face by index, or filter by properties such as surface type, approximate direction, area, or bounding-box overlap. ```python from pathlib import Path import pyvcad as pv import pyvcad_metamaterials as mm import pyvcad_rendering as viz model_path = ( Path(__file__).resolve().parents[2] / "data" / "3d_models" / "conformal_loft.step" ) # Find the largest smooth B-spline face in the imported part to use as the surface. model = pv.CADModel.from_step(str(model_path)) curved_faces = model.select_faces(surface_type="bspline") surface = max(curved_faces, key=lambda face: face.area) # Place a three-cell-thick cubic lattice along the selected freeform face. cell_map = mm.cell_map_from_cad_face( surface, cells=(9, 7, 3), height=9.0, ) root = mm.cubic(cell_map, beam_radius=0.42, node_radius=0.48) viz.Render(root) ```
Cubic lattice mapped from a selected STEP face
Cubic lattice mapped from the selected STEP face Selected B-spline face from the STEP model Selected B-spline face Mapped cubic lattice
The selected-face view is the largest B-spline face — a smooth freeform patch — from the imported part. The mapped view uses it as a three-cell-thick cubic lattice volume. The repository example resolves the STEP path relative to its own file: [`14_imported_step_face_selection.py`](../../../../examples/metamaterials/conformal_cad/14_imported_step_face_selection.py). Passing several `CADFace` objects to `cell_map_from_cad_face(...)` returns a `CellMapCollection`. Each face supplies one separately mapped patch, and the lattice builder combines those patches into one node. ## Create other surface shapes The workflow is not limited to cylinders or imported files. Any CadQuery operation that produces a selectable face can supply a map. These examples show how the same lattice-placement steps behave on several familiar surface shapes. ### Lofted conical surface ```python import cadquery as cq import pyvcad as pv import pyvcad_metamaterials as mm import pyvcad_rendering as viz # Loft two circles to make a tapered wall, then select that conical face. cad = ( cq.Workplane("XY") .circle(18.0) .workplane(offset=32.0) .circle(10.0) .loft(combine=True, ruled=True) ) surface = pv.CADModel.from_cadquery(cad.faces("%CONE")).faces[0] # Bend one BCC layer over the tapered wall, extending 3 mm from the face. cell_map = mm.cell_map_from_cad_face( surface, cells=(24, 10, 1), height=3.0, ) root = mm.bcc(cell_map, beam_radius=0.22, node_radius=0.28) viz.Render(root) ```
BCC lattice mapped onto a lofted conical face
BCC lattice mapped onto a conical loft Selected conical loft face Lofted conical face Conformal BCC lattice
The selected face is the tapered wall made by lofting two circles. The mapped view shows a single BCC (body-centred cubic) beam layer following that wall. Run [`15_cadquery_conical_bcc.py`](../../../../examples/metamaterials/conformal_cad/15_cadquery_conical_bcc.py). ### Spherical band ```python import cadquery as cq import pyvcad as pv import pyvcad_metamaterials as mm import pyvcad_rendering as viz # Limit the sphere to an open band so its spherical face can carry the lattice. cad = cq.Workplane( obj=cq.Solid.makeSphere( 22.0, angleDegrees1=-60.0, angleDegrees2=60.0, ) ) surface = pv.CADModel.from_cadquery(cad.faces("%SPHERE")).faces[0] # Map one Schwarz-P sheet layer across the band, 4.5 mm along the face normal. cell_map = mm.cell_map_from_cad_face( surface, cells=(14, 7, 1), height=4.5, ) root = mm.schwarz_p(cell_map, wall_thickness=0.45) viz.Render(root) ```
Schwarz-P mapped over a spherical band CAD face
Schwarz-P mapped over a spherical band Selected spherical band face Spherical CAD face Conformal Schwarz-P
The selected face is an open spherical band: the top and bottom caps are omitted. The mapped view is one Schwarz-P layer, a smooth porous sheet, stretched over that band. [`16_cadquery_spherical_band_schwarz_p.py`](../../../../examples/metamaterials/conformal_cad/16_cadquery_spherical_band_schwarz_p.py) starts from `cq.Solid.makeSphere(...)`, selects its spherical face, and maps the layer over it. ### Freeform surface from a Python function ```python import math import cadquery as cq import pyvcad as pv import pyvcad_metamaterials as mm import pyvcad_rendering as viz def saddle(u, v): # Describe the freeform surface in ordinary Python coordinates. x = 42.0 * (u - 0.5) y = 34.0 * (v - 0.5) z = 7.0 * math.sin(math.pi * (u - 0.5)) * math.sin(math.pi * (v - 0.5)) return x, y, z # Turn the Python function into a CAD face, then use it as the mapping surface. cad = cq.Workplane("XY").parametricSurface( saddle, N=20, tol=0.02, smoothing=None, ) surface = pv.CADModel.from_cadquery(cad).faces[0] # Follow the saddle with one diamond-beam layer, 2.5 mm thick. cell_map = mm.cell_map_from_cad_face( surface, cells=(12, 10, 1), height=2.5, ) root = mm.diamond(cell_map, beam_radius=0.24, node_radius=0.3) viz.Render(root) ```
Diamond lattice mapped over a freeform parametric saddle face
Diamond lattice mapped over a freeform saddle Selected CadQuery parametric saddle face Parametric saddle face Conformal diamond lattice
The selected-face view is the wavy saddle produced by the Python function. The mapped view lays a diamond beam network across the same rises and dips. [`17_cadquery_freeform_diamond.py`](../../../../examples/metamaterials/conformal_cad/17_cadquery_freeform_diamond.py) uses `parametricSurface(...)`, showing that a surface authored from a Python function follows the same workflow. ## Validation is always final Creating a map checks that its cells do not collapse or turn inside out. This final validation is the reliable check for the resulting curved volume: - A `height` larger than the tightest inside bend of a concave face can fold the offset — *"Surface CellMap is folded or singular."* - Pairing faces that cannot be matched consistently raises *"No valid paired-surface correspondence was found,"* with details for each attempted match. If validation fails, reduce the height or choose faces that describe the same region. Once the map is valid, its lattice is an ordinary OpenVCAD node: it can use the graded fields from the previous guide, combine with maps from [Conformal mapping triangle meshes](conformal-meshes.md), and go to any compatible [compiler](../compilers/index.md).