"""Map-first standard and custom TPMS builders."""
# OpenVCAD
import pyvcad as pv
# Local
from .fields import _coerce_attribute
from .maps import _as_maps
TPMS_NAMES = (
"fischer_koch_s",
"frd",
"gyroid",
"iwp",
"lidinoid",
"neovius",
"schwarz_d",
"schwarz_p",
"split_p",
)
def _combine(cell_map, build):
maps = _as_maps(cell_map)
nodes = [build(item) for item in maps]
return nodes[0] if len(nodes) == 1 else pv.MappedStructureCollection(maps, nodes)
_BUILDER_DOC = """Build a mapped {title} TPMS lattice.
{description}
The surface repeats once per map cell. Equivalent to
``tpms(cell_map, "{name}", mode=mode, wall_thickness=wall_thickness,
level=level)``; see :func:`tpms` for details.
Args:
cell_map (pv.CellMap | pv.CellMapCollection): Map that places the
cells, e.g. from :func:`rectangular_cell_map`.
wall_thickness (float | pv.FloatAttribute | callable): Total sheet
wall thickness in mm, default ``1.0``. Must stay positive. Used
only when ``mode="sheet"``.
mode (str): ``"sheet"`` (default) or ``"solid"``.
level (float | pv.FloatAttribute | callable): Solid level offset in mm,
default ``0.0``; a positive value grows the solid. Used only when
``mode="solid"``.
Returns:
pv.Node: a ``pv.MappedImplicitLattice``, or a
``pv.MappedStructureCollection`` for a multi-map
``pv.CellMapCollection``.
Example:
>>> node = mm.{name}(cell_map, wall_thickness=1.2)
"""
def _builder(title, description):
def decorate(function):
function.__doc__ = _BUILDER_DOC.format(
name=function.__name__, title=title, description=description
)
return function
return decorate
[docs]
def tpms(cell_map, kind, mode="sheet", wall_thickness=1.0, level=0.0):
"""Evaluate a standard or custom periodic implicit unit cell in a CellMap.
The unit cell repeats once per map cell, so the cell size comes from
``cell_map``. The cell's scalar field is converted to an approximate
signed distance in world millimetres through the map Jacobian, so wall
thickness and level stay physical lengths on stretched or curved maps.
Args:
cell_map (pv.CellMap | pv.CellMapCollection): Map that places the
cells, e.g. from :func:`rectangular_cell_map`.
kind (str | pv.ImplicitUnitCell): A name from ``TPMS_NAMES``
(``"gyroid"``, ``"schwarz_p"``, ...) or a custom
``pv.ImplicitUnitCell``.
mode (str): ``"sheet"`` (default) builds a wall of ``wall_thickness``
centred on the mid-surface. ``"solid"`` fills one side of the
mid-surface, offset by ``level``.
wall_thickness (float | pv.FloatAttribute | callable): Total sheet
wall thickness in mm, default ``1.0``. Must stay positive. Used
only in ``"sheet"`` mode.
level (float | pv.FloatAttribute | callable): Solid level offset in mm,
default ``0.0``. The solid is where the signed distance to the
mid-surface is below ``level``, so a positive value grows it. Used
only in ``"solid"`` mode.
Controls accept a number, a ``pv.FloatAttribute`` field sampled at each
point (plain ``x``, ``y``, ``z`` are in mm; ``cell_map.logical_position``
and friends give cell coordinates), or a Python function
``f(x, y, z, d)`` that is compiled with ``numba.cfunc``.
Returns:
pv.Node: a ``pv.MappedImplicitLattice`` for one map, or a
``pv.MappedStructureCollection`` when ``cell_map`` is a
``pv.CellMapCollection`` holding more than one map.
Raises:
TypeError: If ``kind`` is neither a name nor a ``pv.ImplicitUnitCell``.
Example:
>>> node = mm.tpms(cell_map, "schwarz_p", wall_thickness=1.0)
"""
if isinstance(kind, str):
unit_cell = pv.ImplicitUnitCell.standard(kind)
elif isinstance(kind, pv.ImplicitUnitCell):
unit_cell = kind
else:
raise TypeError("kind must be a TPMS name or pv.ImplicitUnitCell")
thickness_field = _coerce_attribute(wall_thickness)
level_field = _coerce_attribute(level)
return _combine(
cell_map,
lambda item: pv.MappedImplicitLattice(
item,
unit_cell,
mode=mode,
wall_thickness=thickness_field,
level=level_field,
),
)
[docs]
@_builder("gyroid", "Schoen gyroid surface (nodal approximation).")
def gyroid(cell_map, wall_thickness=1.0, mode="sheet", level=0.0):
return tpms(cell_map, "gyroid", mode=mode, wall_thickness=wall_thickness, level=level)
[docs]
@_builder("Schwarz P", "Schwarz Primitive surface (nodal approximation).")
def schwarz_p(cell_map, wall_thickness=1.0, mode="sheet", level=0.0):
return tpms(cell_map, "schwarz_p", mode=mode, wall_thickness=wall_thickness, level=level)
[docs]
@_builder("Schwarz D", "Schwarz Diamond surface (nodal approximation).")
def schwarz_d(cell_map, wall_thickness=1.0, mode="sheet", level=0.0):
return tpms(cell_map, "schwarz_d", mode=mode, wall_thickness=wall_thickness, level=level)
[docs]
@_builder("Neovius", "Neovius surface (nodal approximation).")
def neovius(cell_map, wall_thickness=1.0, mode="sheet", level=0.0):
return tpms(cell_map, "neovius", mode=mode, wall_thickness=wall_thickness, level=level)
[docs]
@_builder("I-WP", "Schoen I-WP surface (nodal approximation).")
def iwp(cell_map, wall_thickness=1.0, mode="sheet", level=0.0):
return tpms(cell_map, "iwp", mode=mode, wall_thickness=wall_thickness, level=level)
[docs]
@_builder("lidinoid", "Lidinoid surface (nodal approximation).")
def lidinoid(cell_map, wall_thickness=1.0, mode="sheet", level=0.0):
return tpms(cell_map, "lidinoid", mode=mode, wall_thickness=wall_thickness, level=level)
[docs]
@_builder("split P", "Split P surface (nodal approximation).")
def split_p(cell_map, wall_thickness=1.0, mode="sheet", level=0.0):
return tpms(cell_map, "split_p", mode=mode, wall_thickness=wall_thickness, level=level)
[docs]
@_builder("Fischer-Koch S", "Fischer-Koch S surface (nodal approximation).")
def fischer_koch_s(cell_map, wall_thickness=1.0, mode="sheet", level=0.0):
return tpms(
cell_map,
"fischer_koch_s",
mode=mode,
wall_thickness=wall_thickness,
level=level,
)
[docs]
@_builder("F-RD", "Schoen F-RD surface (nodal approximation).")
def frd(cell_map, wall_thickness=1.0, mode="sheet", level=0.0):
return tpms(cell_map, "frd", mode=mode, wall_thickness=wall_thickness, level=level)