Skip to content

framefield¤

The frame-field route: solve a cross field, trace its separatrices into a coarse layout, and densify along the field.

Classes¤

FieldDecomposition ¤

FieldDecomposition(
    background: BackgroundMesh | None,
    field: CrossField | None,
    separatrices: list[Separatrix],
    tracer: Tracer | None,
)

Coarse quad layout from a cross field's separatrices.

Attributes:

  • background (BackgroundMesh) –
  • field (CrossField) –
  • separatrices (list[Separatrix]) –
  • tracer (Tracer) –
  • inputs (dict) –

    The arguments from_boundary was called with, so the same route can be rebuilt on another domain (a symmetric unit, say).

  • mesh (CoarsePseudoQuadMesh or None) –

    The coarse layout, once decomposition_mesh has run -- or the triangulated backstop after a quad_mesh that fell back.

  • dense (QuadMesh or None) –

    The last mesh quad_mesh returned.

  • polylines (list or None) –

    The network the layout was built from, plus user_curves.

  • guides (list) –

    The guide curves the field was solved with.

  • symmetry (Symmetry or None) –

    The in-solver symmetry group (framefield.symmetry).

  • symmetry_report (SymmetryReport or None) –

    What find_symmetry found.

  • trace_report, snap_report, densify_stats, edit_notes (dict) –

    What tracing, singularity snapping, densification and edit_coarse / edges_to_curves did.

  • repair_notes (list[str]) –

    Everything that degraded the result, in words. See warnings.

  • last_error (str or None) –

    The traceback of an exception quad_mesh caught and fell back from.

  • user_curves (list) –

    Curves the user drew on the layout (a line added in Rhino); kept in polylines so the edge they made densifies along them. Set with set_user_curves.

  • field_aware (bool) –

    False densifies patch interiors as plain Coons patches. For A/B measurements only.

Methods:¤

coarse_mesh ¤
coarse_mesh(poles: Any = (), force: bool = False) -> CoarsePseudoQuadMesh

The coarse quad layout -- the name both front ends answer to. See decomposition_mesh.

decomposition_mesh ¤
decomposition_mesh(poles: Any = (), force: bool = False) -> CoarsePseudoQuadMesh

The all-quad coarse mesh, cached so densities and strips set on it survive.

Falls back to a one-polygon or triangulated layout; route says which.

Parameters:

  • poles (list[[x, y, z]], default: () ) –

    Preferred pole positions: where a triangular patch the repair turns into a pseudo-quad puts its collapsed corner.

  • force (bool, default: False ) –

    Rebuild even when the cached layout would do.

Returns:

  • CoarsePseudoQuadMesh –
decomposition_polylines ¤
decomposition_polylines() -> list[list[list[float]]]

All the polylines forming the decomposition -- separatrices and walls.

densify ¤
densify(coarse: Any = None, **kwargs: Any) -> QuadMesh

The layout densified, patch edges along their separatrices and patch interiors along the field.

Parameters:

  • coarse (CoarsePseudoQuadMesh, default: None ) –

    Strips collected and densities set. Defaults to mesh.

  • **kwargs (Any, default: {} ) –

    Passed to densify.field_densification.

Returns:

  • QuadMesh –
edges_to_curves ¤
edges_to_curves() -> dict[tuple[int, int], list[list[float]]]

{(u, v): polyline} for densification: exact, then boundary arc, then (edited only) warped, then chord.

The tally is kept in edit_notes['edges'].

edit_coarse ¤
edit_coarse(
    geometry: Any, poles: Any = None, snap_tol: float | None = None, strict: bool = True
) -> CoarsePseudoQuadMesh

Take a hand-edited coarse layout in place of the generated one: weld, snap, repair and check it.

Parameters:

  • geometry (mesh or list) –

    The edited layout: a mesh, or one closed polyline per patch with one point per corner. See editing.rebuild.coarse_from_skeleton.

  • poles (list[[x, y, z]], default: None ) –

    Preferred pole positions. Defaults to the current layout's.

  • snap_tol (float, default: None ) –

    How far a corner on the layout's boundary may be moved onto a wall. Defaults to half the background spacing; 0 disables snapping.

  • strict (bool, default: True ) –

    Raise if the edited layout will not densify (default), rather than keep it with a note.

Returns:

  • CoarsePseudoQuadMesh –

    A NEW mesh: vertices are renumbered and no strips are collected.

find_symmetry ¤
find_symmetry(
    tol: float | None = None,
    include: tuple[str, ...] = ("walls", "holes", "guides", "poles"),
    max_order: int = 12,
) -> SymmetryReport

Detect the symmetry of the domain -- walls, holes, guides.

Works without a solve (solve=False). Kept on symmetry_report.

Returns:

from_boundary classmethod ¤
from_boundary(
    outer_boundary: Any,
    inner_boundaries: Any = None,
    guides: Any = None,
    mode: str = "perpendicular",
    target_length: float | None = None,
    orthogonal: bool | None = None,
    guide_weight: float | None = 1.0,
    guide_band: float | None = None,
    relax: bool = False,
    tau: float | None = None,
    symmetry: Symmetry | str | None = "auto",
    solve: bool = True,
    field_tau: float | None = None,
) -> FieldDecomposition

Solve the field and trace its separatrices.

Parameters:

  • outer_boundary (list[[x, y, z]]) –
  • inner_boundaries (list[list[[x, y, z]]], default: None ) –
  • guides (list[list[[x, y, z]]], default: None ) –

    Guide curves -- cables, force lines. A LIST of curves; each may be a list of points or a compas Polyline.

  • mode (optional, default: 'perpendicular' ) –

    As CrossField.from_boundary. target_length is the background spacing, not the quad size -- that is set later by set_strips_density_target or quad_mesh. With the default symmetry='auto' a symmetric input gives a symmetric layout; an asymmetric one is unaffected.

  • target_length (optional, default: 'perpendicular' ) –

    As CrossField.from_boundary. target_length is the background spacing, not the quad size -- that is set later by set_strips_density_target or quad_mesh. With the default symmetry='auto' a symmetric input gives a symmetric layout; an asymmetric one is unaffected.

  • guide_weight (optional, default: 'perpendicular' ) –

    As CrossField.from_boundary. target_length is the background spacing, not the quad size -- that is set later by set_strips_density_target or quad_mesh. With the default symmetry='auto' a symmetric input gives a symmetric layout; an asymmetric one is unaffected.

  • guide_band (optional, default: 'perpendicular' ) –

    As CrossField.from_boundary. target_length is the background spacing, not the quad size -- that is set later by set_strips_density_target or quad_mesh. With the default symmetry='auto' a symmetric input gives a symmetric layout; an asymmetric one is unaffected.

  • relax (optional, default: 'perpendicular' ) –

    As CrossField.from_boundary. target_length is the background spacing, not the quad size -- that is set later by set_strips_density_target or quad_mesh. With the default symmetry='auto' a symmetric input gives a symmetric layout; an asymmetric one is unaffected.

  • tau (optional, default: 'perpendicular' ) –

    As CrossField.from_boundary. target_length is the background spacing, not the quad size -- that is set later by set_strips_density_target or quad_mesh. With the default symmetry='auto' a symmetric input gives a symmetric layout; an asymmetric one is unaffected.

  • symmetry (optional, default: 'perpendicular' ) –

    As CrossField.from_boundary. target_length is the background spacing, not the quad size -- that is set later by set_strips_density_target or quad_mesh. With the default symmetry='auto' a symmetric input gives a symmetric layout; an asymmetric one is unaffected.

  • orthogonal (bool, default: None ) –

    Only orthogonal (cross) fields are implemented; False raises.

  • solve (bool, default: True ) –

    False stores the inputs and returns without solving, for find_symmetry and symmetry_unit. Anything that needs the field raises until solve is called.

  • field_tau (float, default: None ) –

    Old name of tau.

from_mesh classmethod ¤
from_mesh(trimesh: Mesh, **kwargs: Any) -> FieldDecomposition

Construct from a triangulation's BOUNDARY, as SkeletonDecomposition.from_mesh does. The interior is rebuilt at a spacing a field can live on.

get_field ¤
get_field() -> CrossField

The cross field, to hand to densification(field=...) of any coarse layout. The same object as field.

quad_mesh ¤
quad_mesh(
    target_length: float | None = None,
    density: int | None = None,
    coarse: Any = None,
    densities: dict[Any, int] | None = None,
    strict: bool = False,
) -> QuadMesh

An all-quad mesh of the domain, always: via the field layout, else one polygon, else a triangulation.

route says which route ran and warnings why.

Parameters:

  • target_length (float, default: None ) –

    Target quad edge length.

  • density (int, default: None ) –

    Fixed subdivision per coarse edge instead; takes precedence.

  • coarse (mesh or list, default: None ) –

    A hand-edited layout, put through edit_coarse. Once edited, the edit stays in use.

  • densities (dict, default: None ) –

    {strip key: density} over the base pass. A key naming no strip is noted, not raised. Strip keys renumber whenever the layout is rebuilt.

  • strict (bool, default: False ) –

    Re-raise an exception from densification instead of falling back. For tests and debugging.

Returns:

  • QuadMesh –
quality ¤
quality(mesh: Any = None, low_angle: float | None = None) -> dict[str, Any]

The numbers to gate on: element quality of a mesh, as a dict.

Parameters:

  • mesh (Mesh, default: None ) –

    Defaults to the last mesh quad_mesh returned.

  • low_angle (float, default: None ) –

    Threshold for share_below, in degrees.

Returns:

  • dict –

    quality.mesh_quality's metrics, plus route, coverage (mesh area over domain area) and coarse_faces.

report ¤
report() -> dict[str, Any]

The network's and the field's reports, plus face counts of the layout.

route ¤
route() -> str

Which route produced the last layout or mesh: 'field', 'polygon' or 'triangulation'.

set_user_curves ¤
set_user_curves(curves: list[list[Any]]) -> None

Adopt the curves a user drew on the layout, replacing any set before, and add them to polylines.

solve ¤
solve() -> FieldDecomposition

The solved decomposition for the stored inputs -- a NEW object, or this one if it is already solved.

symmetry_unit ¤
symmetry_unit(
    keys: Any = None,
    centre: str = "route",
    seam: Any = None,
    report: SymmetryReport | None = None,
) -> SymmetricUnit

Solve and trace one symmetric unit of the domain with this decomposition's settings.

Parameters are those of SkeletonDecomposition.symmetry_unit.

Returns:

  • SymmetricUnit –
warnings ¤
warnings() -> list[str]

Everything that silently degrades the layout, in words; check arm_mismatch first.

CrossField ¤

CrossField(
    background: BackgroundMesh,
    u: dict[int, complex],
    relaxed: bool = False,
    iterations: int | None = None,
    residual: float | None = None,
)

A boundary-aligned cross field on a planar triangulation.

Attributes:

  • background (BackgroundMesh) –
  • u (dict[int, complex]) –

    exp(i*4*theta) per vertex; |u| = 1 only when relaxed.

  • theta (dict[int, float]) –

    A representative cross angle per vertex, in (-pi/4, pi/4].

  • relaxed (bool) –

    Which solver produced this field.

  • iterations, residual ((int, float or None)) –

    Diffusion steps taken and the final max|u_k+1 - u_k|, when relaxed.

  • guides (list) –

    The guide curves the field was solved with. Densification spends element quality on alignment only when there are some.

  • symmetry (Symmetry or None) –

    The symmetry group of the input.

  • singularity_points (dict[int, [x, y, z]]) –

    Where symmetry.snap_singularities put each singularity; empty when no symmetry was applied.

  • snap_report (dict) –

    What the snapping did.

  • inputs (dict or None) –

    FieldInputs.provenance of what this field was solved from; None for a field built through solve directly.

Attributes¤

__data__ property ¤
__data__: dict[str, Any]

compas Data: the save_to_json payload, so a field can travel inside a larger document (a session) and come back exactly.

Methods:¤

__from_jsondata__ classmethod ¤
__from_jsondata__(data: dict[str, Any]) -> CrossField

Rebuild from __jsondata__'s payload.

__getstate__ ¤
__getstate__() -> dict[str, Any]

Everything but the point locator, for pickle and deepcopy.

__jsondata__ ¤
__jsondata__() -> dict[str, Any]

The payload save_to_json writes: plain types plus one Mesh.

angle_in_face ¤
angle_in_face(
    fkey: int, bary: tuple[float, float, float], reference: float | None = None
) -> float

The cross angle inside a triangle, interpolating u rather than angles.

Parameters:

  • fkey (int) –
  • bary ((float, float, float)) –

    Barycentric coordinates, in the order of face_vertices(fkey).

  • reference (float, default: None ) –

    Return the arm nearest this angle.

Returns:

densify ¤
densify(
    coarse: Any,
    edges_to_curves: dict[tuple[int, int], list[list[float]]] | None = None,
    spend: bool | None = None,
    **kwargs: Any,
) -> tuple[Any, dict[str, Any]]

Densify a coarse layout with this field steering patch interiors.

What coarse.densification(field=field) calls; call it directly to get the statistics as well.

Parameters:

  • coarse (CoarseQuadMesh or CoarsePseudoQuadMesh) –

    Strips collected and densities set.

  • edges_to_curves (dict, default: None ) –

    {(u, v): polyline}, as densification takes it.

  • spend (bool, default: None ) –

    Whether element quality may be spent on alignment. Defaults to whether this field has guides.

  • **kwargs (Any, default: {} ) –

    Passed to densify.field_densification.

Returns:

  • (QuadMesh, dict) –

    The dense mesh (also set on coarse) and the per-patch counts.

directions ¤
directions(vkey: int) -> list[list[float]]

The four arms of the cross at a vertex, as unit vectors.

from_boundary classmethod ¤
from_boundary(
    outer_boundary: Any,
    inner_boundaries: Any = None,
    guides: Any = None,
    mode: str = "perpendicular",
    target_length: float | None = None,
    guide_weight: float | None = 1.0,
    guide_band: float | None = None,
    relax: bool = False,
    tau: float | None = None,
    symmetry: Symmetry | str | None = "auto",
) -> CrossField

A field over a domain: background, constraints, solve and symmetry, without tracing.

Parameters:

  • outer_boundary (list[[x, y, z]]) –
  • inner_boundaries (list[list[[x, y, z]]], default: None ) –
  • guides (list[list[[x, y, z]]], default: None ) –

    Guide curves -- cables, force lines. A LIST of curves; a lone curve is accepted too.

  • mode (('perpendicular', 'tangent'), default: 'perpendicular' ) –

    See constraints.from_curves.

  • target_length (float, default: None ) –

    Background spacing -- not the quad size.

  • guide_weight (float, default: 1.0 ) –

    Weight of the guide constraints, None for hard.

  • guide_band (float, default: None ) –

    How far from a guide it constrains the field. Defaults to the background spacing.

  • relax (bool, default: False ) –

    Use the diffusion + normalisation solver; see the module docstring.

  • tau (float, default: None ) –

    Diffusion time per step, relax only.

  • symmetry (Symmetry or 'auto' or None, default: 'auto' ) –

    'auto' detects the input's symmetry group and keeps it through the background, the field and its singularities. None ignores symmetry. An asymmetric input is unaffected either way.

Returns:

  • CrossField –

    With guides, symmetry, singularity_points, snap_report and inputs set.

load_from_json classmethod ¤
load_from_json(filepath: str, default: Any = None) -> CrossField | Any

Read a field back. The inverse of save_to_json.

Does NOT check that the field still belongs to your domain -- ask mismatch straight afterwards, every time.

Parameters:

  • filepath (str) –
  • default (optional, default: None ) –

    Returned when the file does not exist.

Raises:

  • ValueError –

    If the file is not a field, or is newer than this version reads.

locator ¤
locator() -> PointLocator

Point location on this field's background. Built once, cached, never saved -- it is derived from the background alone.

magnitude ¤
magnitude(vkey: int) -> float

|u|. Near 0 means the vertex is close to a singularity.

matches ¤
matches(*args: Any, **kwargs: Any) -> bool

Whether this field was solved from these inputs. False when it carries no record of its inputs: unknown is not equal.

mismatch ¤
mismatch(*args: Any, **kwargs: Any) -> str | None

Why this field does not belong to these inputs, or None.

Ask it of every field read back from disk before densifying with it.

Returns:

  • str or None –

    The first difference, as a sentence.

poincare_hopf ¤
poincare_hopf() -> dict[str, Any]

Check that the interior indices sum to the field's measured boundary winding.

Returns:

  • dict –

    interior_index_sum, boundary_winding, residual_radians, singular_faces and ok.

report ¤
report() -> dict[str, Any]

poincare_hopf plus the solve's diagnostics.

min_magnitude and mean_magnitude are only informative when the field is not relaxed -- relaxation pins every magnitude to 1.

save_to_json ¤
save_to_json(filepath: str, pretty: bool = False) -> str

Write this field to a JSON file, making the folder if needed. Returns filepath.

Returns:

  • str –

    filepath.

singularities ¤
singularities() -> list[tuple[int, int]]

Singular faces and their indices in quarter-turns (+1 valence 3, -1 valence 5).

Returns:

  • list[int, int] –

    (fkey, index) for every face with a non-zero index.

solve classmethod ¤
solve(
    background: BackgroundMesh,
    constraints: list[Constraint] | None = None,
    relax: bool = False,
    tau: float | None = None,
    tol: float = 1e-09,
    max_iterations: int = 20000,
) -> CrossField

Smoothest cross field satisfying constraints, with uniform edge weights.

Parameters:

  • background (BackgroundMesh) –
  • constraints (list[Constraint], default: None ) –

    Defaults to from_boundary(background): tangent to every wall.

  • relax (bool, default: False ) –

    Keep |u| = 1 by diffusion + normalisation. See the module docstring.

  • tau (float, default: None ) –

    Diffusion time per step, relax only. Defaults to (domain diagonal / 25) ** 2 -- a property of the domain, so the steady state does not move when the background is refined.

  • tol (optional, default: 1e-09 ) –

    Steady-state test on max|u_k+1 - u_k|, relax only.

  • max_iterations (optional, default: 1e-09 ) –

    Steady-state test on max|u_k+1 - u_k|, relax only.

Returns:

FieldInputs dataclass ¤

FieldInputs(
    outer_boundary: Any,
    inner_boundaries: Any = None,
    guides: Any = None,
    mode: str = "perpendicular",
    target_length: float | None = None,
    guide_weight: float | None = 1.0,
    guide_band: float | None = None,
    relax: bool = False,
    tau: float | None = None,
    symmetry: Any = "auto",
)

Everything a field is solved from, with the defaults; see CrossField.from_boundary.

Methods:¤

provenance ¤
provenance() -> dict[str, Any]

The inputs canonicalised for comparison and storage, as plain JSON.

Stored in every saved field; its shape must not change.

resolved ¤
resolved() -> FieldInputs

A copy with guides as a list and 'auto' symmetry detected.

BackgroundMesh ¤

BackgroundMesh(
    mesh: Mesh,
    outer: list[list[float]],
    inners: list[list[list[float]]],
    target_length: float,
)

A well-shaped triangulation of a planar domain, with boundary tangents.

Attributes:

  • mesh (Mesh) –

    The triangulation. All faces have positive area in XY.

  • outer (list[[x, y, z]]) –

    The discretised outer boundary, open (no repeated last point).

  • inners (list[list[[x, y, z]]]) –

    The discretised holes, same convention.

  • target_length (float) –

    The spacing the triangulation was built at.

Attributes¤

diagonal property ¤
diagonal: float

Bounding-box diagonal of the outer boundary.

loops property ¤
loops: list[list[list[float]]]

Every boundary loop, outer first.

Methods:¤

boundary_tangents ¤
boundary_tangents() -> dict[int, list[list[float]]]

The two adjacent boundary edge directions at every boundary vertex (not their average).

Returns:

boundary_vertices ¤
boundary_vertices() -> set[int]

Set of vertex keys on any boundary.

face_basis ¤
face_basis(fkey: int) -> tuple[list[float], list[float]]

Orthonormal tangent basis of a face: world XY, on a planar domain.

from_boundary classmethod ¤
from_boundary(
    outer_boundary: list[list[float]],
    inner_boundaries: list[list[list[float]]] | None = None,
    target_length: float | None = None,
    margin: float = 0.45,
    symmetry: Symmetry | None = None,
    alpha: float = 0.02,
    d_min: int = 5,
) -> BackgroundMesh

Triangulate the region inside outer_boundary and outside the holes.

Parameters:

  • outer_boundary (list[[x, y, z]]) –

    The domain outline, closed or open.

  • inner_boundaries (list[list[[x, y, z]]], default: None ) –

    Holes.

  • target_length (float, default: None ) –

    Edge length, for the walls and the interior grid alike. Defaults to alpha times the bounding-box diagonal (thesis eq. 4.1, see discretise_boundary).

  • margin (float, default: 0.45 ) –

    Interior points' clearance from the walls, as a fraction of target_length.

  • symmetry (Symmetry, default: None ) –

    Passed to interior_grid.

  • alpha (float, default: 0.02 ) –

    target_length as a fraction of the diagonal, when not given.

  • d_min (int, default: 5 ) –

    Fewest points per boundary loop.

Returns:

validate ¤
validate() -> dict[str, Any]

Check the invariants the field solve relies on.

Returns:

  • dict –

    ok plus the counts behind it: no negative-area face, triangles only, one boundary loop per wall, and Euler characteristic 1 - holes (which catches a face bridging a hole).

Constraint module-attribute ¤

Constraint = namedtuple('Constraint', 'vkey direction weight')

PointLocator ¤

PointLocator(background: BackgroundMesh)

Point location on a BackgroundMesh, using a face grid cached as plain arrays.

Parameters:

Methods:¤

cell_of ¤
cell_of(point: list[float]) -> tuple[int, int]

Grid cell of a point.

locate ¤
locate(
    point: list[float], hint: int | None = None
) -> tuple[int, tuple[float, float, float]] | None

Containing face and barycentric coordinates, or None if outside.

hint and its neighbours are tried first: consecutive queries almost always land in the same or an adjacent triangle.

locate_arrays ¤
locate_arrays(points: Any) -> tuple[ndarray, ndarray, ndarray]

locate for a batch, without a hint, as arrays.

Returns:

  • (ndarray, ndarray, ndarray) –

    status per point -- 0 when no face accepts it, 1 when exactly one does, 2 when several do (AMBIGUOUS: which one locate picks then depends on its hint) -- and, where status is 1, the face's position in fkeys and the barycentric coordinates. Status 0 and 1 are exactly what locate returns for ANY hint.

locate_many ¤
locate_many(points: Any) -> list[Any]

locate_arrays as a list: per point (fkey, barycentric), None, or AMBIGUOUS.

nearest ¤
nearest(point: list[float]) -> tuple[int, tuple[float, float, float]] | None

The face whose centroid is closest, within the surrounding 3 x 3 cells, with barycentric (1/3, 1/3, 1/3). For a point just outside the domain.

neighbours ¤
neighbours(fkey: int) -> list[int]

Edge-adjacent faces, in the order mesh.face_neighbors gives them.

test ¤
test(fkey: int, point: list[float]) -> tuple[int, tuple[float, float, float]] | None

(fkey, barycentric) if the face contains the point, else None.

Symmetry ¤

Symmetry(
    centre: list[float],
    elements: list[tuple[int, int, int, int, bool, str]],
    steps: tuple[str, ...] = STEPS,
)

A subgroup of the square's 8 symmetries, about a centre.

Attributes:

  • centre ([x, y, z]) –
  • elements (list[tuple]) –

    A subset of ELEMENTS, always containing the identity.

  • steps (tuple[str]) –

    Which of STEPS to apply.

Methods:¤

apply ¤
apply(g: tuple[int, int, int, int, bool, str], point: list[float]) -> list[float]

g applied to a point, about centre.

detect classmethod ¤
detect(
    loops: list[list[list[float]]],
    centre: list[float] | None = None,
    tol: float = 1e-06,
) -> Symmetry

The subgroup mapping the union of loops (walls, holes and guides) onto itself, as a point set.

Parameters:

  • loops (list[list[[x, y, z]]]) –

    EVERY curve the field will see -- outer boundary, holes and guides; a symmetric plate with an off-centre cable is not symmetric.

  • centre ([x, y, z], default: None ) –

    Defaults to the centre of the outer boundary's bounding box, which any subgroup of the square's symmetries fixes.

  • tol (float, default: 1e-06 ) –

    Point-matching tolerance. Loose, because CAD sampling of a mirrored curve is not bit-exact.

Returns:

enabled ¤
enabled(step: str) -> bool

Whether step should be applied. Never for a trivial group.

linear staticmethod ¤
linear(g: tuple[int, int, int, int, bool, str], vector: list[float]) -> list[float]

g's linear part applied to a free vector.

Constraints¤

from_boundary ¤

from_boundary(
    background: BackgroundMesh,
    weight: float | None = None,
    corner_tolerance: float = 0.1,
) -> list[Constraint]

Pin the field tangent to the wall at every boundary vertex; 45/135-degree corners are left free.

Parameters:

  • background (BackgroundMesh) –
  • weight (float, default: None ) –

    None (default) for hard constraints.

  • corner_tolerance (float, default: 0.1 ) –

    Averaged representations shorter than this leave the vertex free.

Returns:

from_curves ¤

from_curves(
    background: BackgroundMesh,
    curves: Any,
    mode: str = "perpendicular",
    band: float | None = None,
    weight: float | None = 1.0,
    skip_boundary: bool = True,
) -> list[Constraint]

Pin the field along guide curves at every vertex within band, independent of curve order.

Parameters:

  • background (BackgroundMesh) –
  • curves (list[list[[x, y, z]]]) –

    The guide curves; a single curve may also be passed. Points may be lists, tuples or compas Points.

  • mode (('perpendicular', 'tangent'), default: 'perpendicular' ) –

    Whether mesh edges should run across the curve or along it. For a cross field the two are the same constraint; the parameter matters once the frame is not orthogonal.

  • band (float, default: None ) –

    Only vertices within this distance of a curve are constrained. Defaults to background.target_length.

  • weight (float, default: 1.0 ) –

    Least-squares weight, or None for hard. Soft by default: a guide that disagrees with a wall should bend, not break the solve.

  • skip_boundary (bool, default: True ) –

    Leave boundary vertices to from_boundary; the wall must win.

Returns:

Densification and relaxation¤

field_densification ¤

field_densification(
    coarse: CoarsePseudoQuadMesh,
    field: CrossField,
    locator: Any = None,
    edges_to_curves: dict[tuple[int, int], list[list[float]]] | None = None,
    iterations: int = ITERATIONS,
    tikhonov: float = TIKHONOV,
    stiffness: tuple[float, ...] = STIFFNESS,
    guard: bool = True,
    spend: bool = False,
    field_aware: bool = True,
    early_exit: bool = True,
) -> tuple[Any, dict[str, Any]]

Densify a coarse layout with the field steering ordinary patch interiors.

A drop-in for CoarsePseudoQuadMesh.densification.

Parameters:

  • coarse (CoarsePseudoQuadMesh) –

    Strips collected and densities set.

  • field (CrossField) –
  • locator (PointLocator, default: None ) –

    Defaults to field.locator(). A Tracer is accepted too.

  • edges_to_curves (dict, default: None ) –

    {(u, v): polyline}. An edge missing from it densifies as a chord.

  • iterations (optional, default: ITERATIONS ) –

    Passed to relax_patch.

  • tikhonov (optional, default: ITERATIONS ) –

    Passed to relax_patch.

  • stiffness (optional, default: ITERATIONS ) –

    Passed to relax_patch.

  • guard (optional, default: ITERATIONS ) –

    Passed to relax_patch.

  • spend (optional, default: ITERATIONS ) –

    Passed to relax_patch.

  • early_exit (optional, default: ITERATIONS ) –

    Passed to relax_patch.

  • field_aware (bool, default: True ) –

    False reproduces discrete_coons_patch exactly, for A/B tests.

Returns:

  • (mesh, dict) –

    The dense mesh, and counts: patches; relaxed, guarded and flat (from relax_patch); poles (pseudo-quads, kept Coons); coons (patches left Coons because field_aware is off); stiffness (per relaxed patch) and folds. seam_edge maps the geometric key of every patch-boundary node to (u, v, index) -- the coarse edge it lies on and its position along it -- which relax.relax_mesh needs to slide seam nodes without reordering them.

relax_mesh ¤

relax_mesh(
    mesh: Mesh,
    seam_edge: dict[str, tuple[int, int, int]] | None = None,
    edges_to_curves: dict[tuple[int, int], list[list[float]]] | None = None,
    corner_angle: float = 30.0,
    schedule: Sequence[tuple[int, float]] = SCHEDULE,
    seams: str = "free",
    pin_singularities: bool = False,
) -> dict[str, Any]

Relax a dense mesh globally, its outline sliding along itself.

Modifies mesh in place, and only if no quality measure gets worse.

Parameters:

  • mesh (Mesh) –

    The dense mesh.

  • seam_edge (dict, default: None ) –

    {geometric_key: (u, v, index)}, as in FieldDecomposition.densify_stats['seam_edge']. Needed for seams other than 'free'.

  • edges_to_curves (dict, default: None ) –

    {(u, v): [point, ...]}, from FieldDecomposition.edges_to_curves.

  • corner_angle (float, default: 30.0 ) –

    Kink angle, in degrees, past which a boundary vertex is a pinned corner.

  • schedule (sequence[int, float], default: SCHEDULE ) –

    (iterations, damping) settings, gentlest first.

  • seams (('free', 'slide', 'fixed'), default: 'free' ) –

    What the patch-seam vertices may do. 'free' (the default, and the only one measured to help) lets them leave their separatrix; 'slide' keeps them on it; 'fixed' pins them.

  • pin_singularities (bool, default: False ) –

    Hold irregular vertices and poles in place. Off by default.

Returns:

  • dict –

    accepted -- the setting used, or None if the mesh was left untouched; before / after -- (min angle, max angle, max aspect); sliding / pinned -- how many vertices of each kind.

Quality¤

mesh_quality ¤

mesh_quality(mesh: Mesh, low_angle: float = LOW_ANGLE) -> dict[str, Any]

Element quality of a quad (or pseudo-quad) mesh, as plain numbers.

Parameters:

  • mesh (Mesh) –
  • low_angle (float, default: LOW_ANGLE ) –

    Threshold for share_below, in degrees.

Returns:

  • dict –

    faces, poles, min_angle, max_angle, aspect_max, share_below, low_angle, irregular_interior, and worst_face -- the key of the face carrying min_angle, so a failure can be looked at rather than merely counted.

Notes

Aspect ratio is longest edge over shortest edge, per face, worst over the mesh. A perfect grid reads 1.00. It is inf when a face has a zero-length edge, which is what hard_floor rejects on.

curve_alignment ¤

curve_alignment(
    mesh: Mesh,
    curve: list[list[float]],
    radius: float = ALIGNMENT_RADIUS,
    spacing: float = 0.5,
) -> float

Mean of curve_alignment_profile, in degrees; check the profile too.

hard_floor ¤

hard_floor(metrics: dict[str, Any]) -> tuple[bool, str]

TIER 1. Is anything in this mesh degenerate rather than merely poor?

Absolute: an angle at or near 0 or 180 degrees, or a non-finite aspect ratio. Everything softer is the regression check's business.

Returns:

  • (bool, str) –

    Verdict, and when false the offending metric named with its value.

guide_metrics ¤

guide_metrics(
    mesh: Mesh, guides: list[list[Any]], steep: float = STEEP
) -> dict[str, Any]

How well a mesh carries its guides, as numbers rather than a verdict.

Parameters:

  • mesh (Mesh) –

    A quad or pseudo-quad mesh.

  • guides (sequence[sequence[point]]) –

    The guide curves, as polylines -- the same objects fed to the field.

  • steep (float, default: STEEP ) –

    Degrees off horizontal past which a guide station counts as tilted.

Returns:

  • dict –

    faces -- faces the guides pass through, the population everything else is measured over. Zero means the metric could not see the guide at all, and every other value is None.

    along -- median angle between the guide and the face family nearer to it, in degrees. 0 is a course running along the guide.

    across / across_within / across_tilted -- median angle of the other family, the share of it within ACROSS_TOLERANCE of 90 degrees, and the median over tilted stations only. 90 is an interface meeting the guide square.

    off_centre / off_centre_worst -- the guide's offset from the face centre in units of half the face width across it. 0 is mid-block, 1 is on the joint.

    poles -- pseudo-quad faces skipped, having no two families to compare.