Skip to content

editing¤

Hand-editing a coarse layout or a dense mesh, with no CAD in it. Rhino supplies only picks and prompts.

Classes¤

MeshEditor ¤

MeshEditor(mesh: 'QuadMesh', walls: list[Any] | None = None, work_on_copy: bool = True)

A quad mesh being hand-edited. No CAD, no prompts, no prints.

Parameters:

  • mesh (QuadMesh) –

    The mesh to edit. Kept as target; the editing happens on a copy.

  • walls (list, default: None ) –

    The domain walls, as point lists, closed polylines or BoundaryLoop instances. A boundary vertex that moves is projected back onto the nearest one, so nudging the edge of the mesh does not eat the outline. Without walls a moved boundary vertex is left where it was put.

  • work_on_copy (bool, default: True ) –

    Edit a copy and leave mesh untouched until a commit transplants into it. Default True; False edits in place, for a caller that has already made its own copy.

Attributes:

  • target (mesh) –

    The object handed in. Only a commit writes to it.

  • mesh (mesh) –

    The working copy. Replaced wholesale by every topological operation that succeeds, so hold the editor, not the mesh.

  • walls (list) –

    The domain walls as BoundaryLoop instances.

  • last_reason (str) –

    Why the last operation was refused. Empty when nothing was.

Methods:¤

boundary_vertices ¤
boundary_vertices(mesh: 'QuadMesh | None' = None) -> set[int]

Every vertex on every boundary, holes included, as a set read off the halfedges.

check_strip_count ¤
check_strip_count(mesh: 'QuadMesh | None' = None) -> tuple[bool, int, int]

Check thesis Eq 5.6: the number of open strips is E - 2F. (ok, counted, E - 2F).

Returns:

  • (bool, int, int) –

    Whether it holds, the counted number of open strips, and E - 2F.

Notes

Collected on a COPY, so calling it never leaves strip data on a mesh the caller did not ask to have it.

discard_last_undo ¤
discard_last_undo() -> None

Drop the most recent push_undo snapshot -- nothing changed.

is_vertex_on_boundary ¤
is_vertex_on_boundary(vkey: int, mesh: 'QuadMesh | None' = None) -> bool

Whether a vertex is on ANY boundary of the mesh -- holes included.

mean_edge ¤
mean_edge(mesh: 'QuadMesh | None' = None) -> float

Mean edge length -- the scale every tolerance here is expressed in.

move_vertex ¤
move_vertex(
    vkey: int, xyz: list[float], project: bool = True
) -> tuple[bool, dict[str, Any]]

Move one vertex, projecting a boundary vertex back onto its wall unless project is off. (ok, notes).

plan_strip_deletion ¤
plan_strip_deletion(
    edge: tuple[int, int] | None = None,
    preserve_boundaries: bool = False,
    skey: int | None = None,
) -> dict[str, Any]

What deleting the strip would cost, predicted on a copy: moved vertices, collateral strips, lost boundaries.

Returns:

  • dict –

    ok, reason, skey, faces, collateral (a LIST), boundaries_lost (a list of vertex rings), to_split, and the before/after face and vertex counts. A subclass that publishes different names or shapes adapts this at its own boundary.

project_to_wall ¤
project_to_wall(xyz: list[float]) -> list[float]

The nearest point of the nearest wall, or xyz if there is no wall.

Only for vertices topologically on the mesh boundary.

push_undo ¤
push_undo() -> None

Snapshot the mesh before an operation that may mutate it.

remove_strip ¤
remove_strip(
    edge: tuple[int, int] | None = None,
    preserve_boundaries: bool = False,
    skey: int | None = None,
) -> tuple[bool, dict[str, Any]]

Delete the strip through edge (or skey), on a copy adopted only if it passes _gate. (ok, notes).

Collapses the band and welds its sides; it does not dissolve the picked line.

reset ¤
reset() -> tuple[bool, dict[str, Any]]

Restore the whole mesh as it was at construction or at the last snapshot.

snapshot ¤
snapshot() -> None

Make the current mesh the state reset returns to.

strip_through ¤
strip_through(edge: tuple[int, int]) -> int | None

The strip key through edge, resolved on a copy. None if none.

undo ¤
undo() -> tuple[bool, dict[str, Any]]

Put back the mesh as it was before the last push_undo. (ok, notes).

Refuses with nothing to restore rather than silently doing nothing, so a front end can tell "undid something" from "there was nothing left".

CoarseEditor ¤

CoarseEditor(
    coarse: "CoarsePseudoQuadMesh",
    field: Any = None,
    loops: list[list[list[float]]] | None = None,
    polylines: Sequence[list[list[float]]] = (),
    poles: list[list[float]] | None = None,
    snap_tol: float | None = None,
)

A coarse quad layout being hand-edited. No Rhino, no prompts, no prints.

Parameters:

  • coarse (CoarsePseudoQuadMesh) –

    The layout to edit. Edited on a COPY; commit writes back into this object, keeping its identity.

  • field (CrossField, default: None ) –

    Carried so a caller can densify with it afterwards, and used for its background when loops or snap_tol are not given. Never solved, never consulted by an edit.

  • loops (list, default: None ) –

    The domain walls, outer first, as point lists. A boundary corner that moves is projected onto the nearest one. Resolved from field when omitted, and from the layout's own boundary when there is no field either -- in which case the projection is a near no-op and no curvature is available.

  • polylines (list, default: () ) –

    The traced separatrices as plain point lists. Used to give the two halves of a curved edge their shape when a cut crosses it, and published with the drawn curves through all_polylines.

  • poles (list, default: None ) –

    Preferred pole positions for a rebuild. Defaults to the layout's own.

  • snap_tol (float, default: None ) –

    How far a corner ON THE LAYOUT'S BOUNDARY may be projected onto a domain wall at commit. Interior corners are never moved, whatever their distance.

Attributes:

  • curves (dict) –

    (rounded pa, rounded pb) -> [[x, y, z], ...] -- the SHAPE of a coarse edge that has one: a curve the user drew, or a half of a curved edge that a cut crossed. Keyed geometrically and never by vertex key, because a rebuilding commit renumbers them.

  • last_reason (str) –
  • last_cut (dict) –
  • last_deletion (dict) –

Examples:

>>> editor = CoarseEditor(coarse, loops=[outer] + holes)
>>> ok, notes = editor.divide([[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]], extend=True)
>>> ok, notes = editor.move_pole(pkey, editor.pole_targets(pkey)[0])
>>> layout, notes = editor.commit()

Attributes¤

all_polylines property ¤
all_polylines: list[list[list[float]]]

Traced separatrices plus the user's drawn curves, as published to densification.

Methods:¤

add_strip ¤
add_strip(polyedge: list[int]) -> tuple[bool, dict[str, Any]]

Unzip a polyedge of existing corners into a new strip (thesis 5.3.1). (ok, notes).

Strip labels and densities survive; the new strip is opened by the grammar's exact rule.

Parameters:

  • polyedge (list[int]) –

    Corner keys, in order, each joined to the next by a coarse edge. Either closed on itself, or with both ends on the layout boundary or at a pole -- a strip has to run the full width of the layout, for the same reason a cut has to reach a wall.

commit ¤
commit() -> tuple['CoarsePseudoQuadMesh | None', dict[str, Any]]

Write the edited layout back into the mesh this editor was given. (layout, notes).

Moves only keep keys and strip data; a cut rebuilds via coarse_from_skeleton. layout is None on refusal, with notes['error']; notes['path'] says which path ran.

corner_tol ¤
corner_tol() -> float

How close to a corner a point is taken as that corner.

Keeps edge splits clear of slivers and of the 3-decimal weld at commit.

divide ¤
divide(
    points: list[list[float]] | None = None,
    skey: int | None = None,
    t: float | None = None,
    extend: bool | Any | None = None,
) -> tuple[bool, dict[str, Any]]

Subdivide a strip, by a drawn curve (points) or a picked strip (skey, t). (ok, notes).

A legal cut crosses exactly the patches of one strip; anything else is refused.

edge_curves ¤
edge_curves(
    loops: list[list[list[float]]] | None = None, warp: bool | None = None
) -> tuple[dict[tuple[int, int], list[list[float]]], dict[str, Any]]

(mapping, tally): one polyline per coarse edge, for densification.

Wall arc, then exact separatrix match, then (if warp) a warped separatrix, else the chord.

edge_shape ¤
edge_shape(u: int, v: int) -> list[list[float]] | None

The drawn SHAPE this edge carries, or None if it is a chord.

For display: an edge that carries an inserted arc should be drawn as that arc, or a cut that worked looks like one that snapped straight.

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

Where a point sits on the layout: ('vertex', vkey), ('edge', (u, v), xyz) or None.

Corners win over edges within corner_tol.

move_pole ¤
move_pole(pkey: int, vkey: int) -> tuple[bool, dict[str, Any]]

Move the pole at pkey to corner vkey by relabelling its pseudo-quads. (ok, notes).

Strip data is not carried through a commit afterwards; see pole_targets.

on_tol ¤
on_tol() -> float

How far off an edge a point may be and still count as on it.

Tight, because a point that reaches here was picked with the cursor constrained onto a layout edge -- this absorbs float noise, not aim.

plan_strip_deletion ¤
plan_strip_deletion(
    edge: tuple[int, int] | None = None,
    preserve_boundaries: bool = False,
    skey: int | None = None,
) -> dict[str, Any]

What deleting the strip would cost, without mutating anything.

Returns skey, faces, collateral, boundaries_lost (counts), ok and reason.

pole_targets ¤
pole_targets(pkey: int) -> list[int]

The corners the pole at pkey may move to: those shared by all its triangles.

project ¤
project(xyz: list[float], boundary: bool) -> list[float]

Project a boundary corner onto the nearest wall; an interior one stays put.

Eligibility is topological, as in rebuild.snap_to_loops.

remove_strip ¤
remove_strip(
    edge: tuple[int, int] | None = None,
    preserve_boundaries: bool = False,
    skey: int | None = None,
) -> tuple[bool, dict[str, Any]]

Delete the strip through edge (or skey). (ok, notes).

reset ¤
reset() -> tuple[bool, dict[str, Any]]

Back to the layout this round of edits started from, curves and poles included.

snapshot ¤
snapshot() -> None

Make the current layout, curves and poles the state reset returns to.

strip_of_cut ¤
strip_of_cut(points: list[list[float]]) -> int | None

The strip a drawn curve would divide, or None.

Resolved from the first coarse edge the curve crosses. A front end can highlight the band before the rest of the curve is drawn.

DenseMeshEditor ¤

DenseMeshEditor(
    mesh: "QuadMesh",
    walls: list[Any] | None = None,
    relax_iterations: int | None = None,
)

A dense mesh being hand-edited. No Rhino, no prompts, no prints.

Parameters:

  • mesh (QuadMesh) –

    The mesh to edit, polygons allowed. A QuadMesh rather than a plain compas.datastructures.Mesh, because the strip operations need its polyedges and strips.

  • walls (list, default: None ) –

    The domain walls, as point lists, closed polylines or BoundaryLoop instances. A boundary vertex that moves is projected onto the nearest one. Without walls it is left where it was put.

  • relax_iterations (int, default: None ) –

    Default number of smoothing passes for relax. Defaults to RELAX_ITERATIONS.

Attributes:

  • mesh (QuadMesh) –

    The mesh as it currently stands.

  • last_reason (str) –

    Why the last operation was refused. Empty when nothing was.

  • last_addition (dict) –

    What the last successful add_line did.

  • last_deletion (dict) –

    What the last successful remove_line did.

Examples:

>>> editor = DenseMeshEditor(mesh, walls=loops)
>>> ok, notes = editor.remove_edge((12, 13))
>>> ok, notes = editor.draw_edges([[0.0, 2.5, 0.0], [10.0, 2.5, 0.0]])
>>> editor.undo()

Methods:¤

add_line ¤
add_line(edge: tuple[int, int]) -> tuple[bool, dict[str, Any]]

Grow a strip along the polyedge through edge, on a copy adopted only if it passes _gate. (ok, notes).

Only the new vertex pairs move; smoothing is a separate step (relax).

Parameters:

  • edge (tuple[int, int]) –

    Any edge of the polyedge to add a strip along.

draw_edges ¤
draw_edges(
    points: list[list[float]], snap: float | None = None
) -> tuple[bool, dict[str, Any]]

Cut a drawn polyline into the mesh, splitting every edge and face it crosses (in XY). (ok, notes).

An edge cannot end inside a face, so dangling or outside parts are left out and counted in notes.

Parameters:

  • points (list) –

    The polyline, at least two points.

  • snap (float, default: None ) –

    Distance within which the line goes through an existing vertex or lies on an existing edge. Defaults to DRAW_SNAP times the mean edge length.

face_at ¤
face_at(point: list[float], mesh: 'QuadMesh | None' = None) -> int | None

The face whose outline contains point in plan, or None.

Tested in XY, so it also finds a face of a mesh that has been given height, as long as the mesh does not fold back over itself in plan.

non_manifold_vertices ¤
non_manifold_vertices(mesh: 'QuadMesh | None' = None) -> list[int]

Vertices where two separate fans of faces meet at a single point.

non_quad_faces ¤
non_quad_faces(mesh: 'QuadMesh | None' = None) -> list[int]

Every face that is not four-sided. Empty on a clean quad mesh.

plan_strip_deletion ¤
plan_strip_deletion(
    edge: tuple[int, int] | None = None,
    preserve_boundaries: bool = False,
    skey: int | None = None,
) -> dict[str, Any]

What deleting the strip through edge (or skey) would cost. Mutates nothing.

The deletion is PERFORMED on a copy and what came out is reported, because no cheap test predicts every failure.

Returns:

  • dict –

    ok -- whether remove_line would succeed with these arguments -- and reason when it would not. Plus strip, faces, collateral, boundaries_lost (a COUNT), boundaries_lost_vertices (which ones), to_split, faces_before / faces_after and vertices_before / vertices_after. The counts after are None when the deletion would not go through.

polyedge_through ¤
polyedge_through(
    edge: tuple[int, int], mesh: "QuadMesh | None" = None
) -> tuple[int | None, list[int] | None]

(pkey, polyedge) for the polyedge containing edge, as a new list.

Re-collected every time: an added or deleted strip renumbers the keys. The list is a COPY because add_strip pops from the list it is given.

relax ¤
relax(iterations: int | None = None, mesh: 'QuadMesh | None' = None) -> 'QuadMesh'

Smooth the interior, holding every boundary ring. Returns the mesh.

remove_edge ¤
remove_edge(edge: tuple[int, int]) -> tuple[bool, dict[str, Any]]

Remove one edge by merging the two faces either side of it. (ok, notes).

A boundary edge deletes its one face instead, with notes['merged'] set to None.

remove_face ¤
remove_face(fkey: int) -> tuple[bool, dict[str, Any]]

Delete one face. (ok, notes).

Mesh.delete_face. Refused only for a face that is not there, and for the last face of the mesh.

remove_line ¤
remove_line(
    edge: tuple[int, int], preserve_boundaries: bool = False
) -> tuple[bool, dict[str, Any]]

Delete the strip through edge. (ok, notes).

Performs the same trial as plan_strip_deletion and adopts the result, so a refusal costs nothing.

Parameters:

  • edge (tuple[int, int]) –

    Any edge of the strip to delete.

  • preserve_boundaries (bool, default: False ) –

    Pre-split the strips that would otherwise leave a boundary with too few splits.

remove_strip ¤
remove_strip(
    edge: tuple[int, int] | None = None,
    preserve_boundaries: bool = False,
    skey: int | None = None,
) -> tuple[bool, dict[str, Any]]

Same as remove_line, with the strip given by edge or skey. (ok, notes).

remove_vertex ¤
remove_vertex(vkey: int) -> tuple[bool, dict[str, Any]]

Delete a vertex and every face around it. (ok, notes).

Not refused when it leaves a non-manifold vertex; notes['non_manifold'] lists them.

reset ¤
reset() -> tuple[bool, dict[str, Any]]

Put the mesh back as it was at construction, or at the last snapshot.

GuideCurve ¤

GuideCurve(points: 'Iterable[Sequence[float]]', curve: Any = None)

A guide curve as a polyline with an arc-length table and, optionally, the curve itself.

Chains are chosen on the samples; closest_point lands on curve when given.

Parameters:

  • points (sequence[point] | Polyline) –

    The guide, already discretised. Consecutive duplicates are dropped.

  • curve (Curve, default: None ) –

    The curve points were sampled from -- from compas_rhino's curve_to_compas, from compas_occ, a compas.geometry.Circle, ... It must lie where the samples do. Default is None: vertices land on the samples.

Attributes:

  • points (list[list[float]]) –

    The guide points.

  • length (float) –

    The total arc length of the samples.

  • closed (bool) –

    Whether the first and last point coincide.

  • curve (Curve | None) –

    The curve a vertex lands on, if one was given.

Raises:

  • ValueError –

    If fewer than two distinct points are given.

Methods:¤

closest_point ¤
closest_point(point: list[float]) -> list[float] | None

The closest point on the guide (on curve if set), usable as a smoothing constraint.

delta ¤
delta(t: float, t0: float) -> float

Signed progress from arc length t0 to t, wrapped the short way on a closed guide.

project ¤
project(
    xyz: list[float],
) -> tuple[float | None, float, tuple[float, float, float] | None]

Project a point onto the guide.

Parameters:

Returns:

  • tuple[float | None, float, tuple[float, float, float] | None] –

    The distance to the guide, the arc length of the closest point, and the unit tangent there. (None, 0.0, None) if the guide has no non-degenerate segment, which cannot happen for a guide built by this class.

Guide chains¤

guide_chain ¤

guide_chain(
    mesh: "QuadMesh",
    guide: "GuideCurve | Iterable[Sequence[float]]",
    tolerance: float | None = None,
    max_angle: float = DEFAULT_MAX_ANGLE,
    boundary: str = "anchor",
    polyedges: list[tuple[list[int], bool]] | None = None,
) -> tuple[list[int], dict[str, Any]]

Choose the run of a polyedge that follows a guide curve.

Parameters:

  • mesh (QuadMesh) –

    The dense quad mesh. A QuadMesh, not a plain compas.datastructures.Mesh: the polyedges are what this selects from.

  • guide (GuideCurve | sequence[point]) –

    The guide curve, already discretised.

  • tolerance (float, default: None ) –

    How far off the guide a chain vertex may sit. Default is DEFAULT_TOLERANCE_FACTOR times the mean edge length -- the target quad edge length the mesh was densified with.

  • max_angle (float, default: DEFAULT_MAX_ANGLE ) –

    How far the polyedge may run off the guide's tangent at a vertex, in degrees, and still have that vertex selected. Default is DEFAULT_MAX_ANGLE; 90 turns it off.

  • boundary (('anchor', 'exclude', 'free'), default: 'anchor' ) –

    What the chain may do with boundary vertices.

    • 'anchor' -- a polyedge that is entirely on the boundary is refused, so the only boundary vertices that can survive are the two ENDS of an interior polyedge. A cable drawn wall to wall reaches the wall.
    • 'exclude' -- boundary vertices are dropped as well, so the chain stops one vertex short at each end.
    • 'free' -- no restriction: the outline itself may be selected. This exists so a measurement can show what that costs; it is not a setting to use.
  • polyedges (list, default: None ) –

    The result of collect_polyedges for this mesh, if it has already been collected. Default is None, in which case it is collected here.

Returns:

  • tuple[list[int], dict] –

    The chain, ordered along the guide, and a dictionary with:

    reason Why the chain is empty, or ''. tolerance, boundary What it ran with. alignment How closely the chosen chain runs along the guide, 0 to 1. candidates How many polyedge runs were within tolerance of the guide. coverage The share of the guide's length the chain spans, 0 to 1. gap_start, gap_end The length of guide left uncovered at each end, in mean edge lengths. Zero on a closed guide.

attach_chain ¤

attach_chain(
    mesh: "QuadMesh",
    chain: list[int],
    guide: "GuideCurve | Iterable[Sequence[float]]",
    hold: str = "fixed",
    boundary_curves: dict[int, Polyline] | None = None,
) -> tuple[dict[int, list[float]], dict[int, Any]]

Where each chain vertex goes and what holds it there. The mesh is not modified.

A boundary vertex is never moved onto the guide; at most it slides along its outline.

Parameters:

  • mesh (Mesh) –

    The mesh. NOT modified -- the moves are returned, not applied.

  • chain (list[int]) –

    The vertices to attach.

  • guide (GuideCurve | sequence[point]) –

    The guide to attach them to.

  • hold (('fixed', 'sliding'), default: 'fixed' ) –

    How an INTERIOR vertex is held once it is on the guide. 'fixed' pins it where it lands; 'sliding' re-projects it every iteration, so it may travel along the guide but never leave it. A boundary vertex slides on its outline either way.

  • boundary_curves (dict, default: None ) –

    The result of _boundary_curves for this mesh, if already built.

Returns:

  • tuple[dict, dict] –

    {vertex: [x, y, z]} -- the vertices to move, and where to. Boundary vertices are absent: they do not move. {vertex: constraint} -- what holds each vertex of the chain, ready to be merged into the constraints of compas_singular.datastructures.constrained_smoothing.

chain_quality ¤

chain_quality(
    mesh: "QuadMesh", chain: list[int], guide: "GuideCurve | Iterable[Sequence[float]]"
) -> dict[str, Any]

Measure a chain against a clean, long polyedge line that follows the guide.

Parameters:

  • mesh (QuadMesh) –

    The mesh the chain was selected on, unmodified.

  • chain (list[int]) –

    The chain, as returned by guide_chain.

  • guide (GuideCurve | sequence[point]) –

    The guide it was selected for.

Returns:

  • dict –

    n The number of vertices. boundary_interior Boundary vertices that are NOT at an end of the chain. Must be 0: a chain running along the outline takes the outline with it when it is projected. boundary_ends Boundary vertices at an end, 0 to 2. These are anchors, and are allowed. valid_path Whether every consecutive pair is a real edge and no vertex repeats. purity The share of interior triples that continue straight across the middle vertex. 1.0 means the chain IS a piece of a polyedge. runs, longest_run The number of maximal straight runs and the longest, in vertices. turn_max, turn_mean The geometric turn off straight at each interior vertex, in degrees. A polyedge is topologically straight but not geometrically straight, so these are read against the mesh, not against zero. alignment How closely the chain's edges run along the guide, 0 to 1. off_max, off_mean Distance from the guide, in mean edge lengths. coverage, gap_start, gap_end The share of the guide the chain spans, and what is left at each end. min_face_angle, folded_faces The worst face corner angle among the faces around the chain, and the number of faces that turn inside out, after the chain is projected onto the guide on a COPY of the mesh. A band selection folds faces; a course must not. end_drift How far an anchored boundary end moves off the outline when it is projected, in mean edge lengths. This is the price of allowing anchors.

collect_polyedges ¤

collect_polyedges(mesh: 'QuadMesh') -> list[tuple[list[int], bool]]

The polyedges of a quad mesh as vertex lists, closed ones without the repeated end.

Collect once and reuse across guides; it is O(edges squared).

mean_edge_length ¤

mean_edge_length(mesh: 'QuadMesh') -> float

The mean length of the non-degenerate edges of a mesh.

Rebuilding¤

coarse_from_skeleton ¤

coarse_from_skeleton(
    geometry: Any,
    loops: list[list[list[float]]] | None = None,
    poles: Sequence[list[float]] = (),
    snap_tol: float = 0.0,
    cls: type = CoarsePseudoQuadMesh,
) -> tuple["QuadMesh | None", dict[str, Any]]

A coarse quad layout from an edited patch skeleton (the wireframe handed back from Rhino).

Pass one point per patch corner, not curve samples.

Parameters:

  • geometry (mesh or list) –

    A compas mesh, or closed polylines with ONE POINT PER PATCH CORNER. See design_notes/editing.md (rebuild.py) on why.

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

    Boundary loops -- outer first, then holes. Used for snapping and handed to repair.solve_non_quad_faces so a split vertex lands on the wall rather than on a chord.

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

    Preferred pole positions. Pass the previous layout's poles so an untouched pseudo-quad keeps its collapsed corner where it was.

  • snap_tol (float, default: 0.0 ) –

    How far a BOUNDARY corner may be projected onto a wall -- not which corners are eligible, which is topological. 0 disables snapping. A boundary corner further than this from every loop is left alone and counted in off_wall, because at that distance the layout is wrong rather than imprecise and moving it would hide the fact.

  • cls (type, default: CoarsePseudoQuadMesh ) –

Returns:

  • (mesh or None, dict) –

    The layout, and what happened to it: faces_in, faces_out, vertices, snapped, off_wall, degenerate, sides (the side-count histogram BEFORE repair -- anything other than {4: n} means the skeleton was not four-sided patches) and repair (the note repair.solve_non_quad_faces returned).

face_polylines ¤

face_polylines(coarse: 'QuadMesh') -> list[Polyline]

One closed polyline per patch with corners only, to bake and edit.

Returns:

faces_from_geometry ¤

faces_from_geometry(geometry: Any) -> list[list[list[float]]]

[[corner, corner, ...], ...] from a mesh, closed polylines, or faces already given as point lists.

mesh_from_faces ¤

mesh_from_faces(
    faces: list[list[list[float]]], cls: type = CoarsePseudoQuadMesh
) -> tuple["QuadMesh | None", int]

A coarse mesh from faces as corner point lists, welded at PRECISION. None if nothing is left.

Faces that repeat a corner are dropped as degenerate.

snap_to_loops ¤

snap_to_loops(
    mesh: "QuadMesh", loops: list[list[list[float]]], tol: float
) -> tuple[int, int]

Project the mesh's own boundary corners onto the domain walls; interior corners never move.

Returns (moved, left_off_wall); the second should be 0.

Returns:

  • (int, int) –

    How many boundary corners were moved, and how many were further from every loop than tol and so left where they were. The second number should be 0; anything else means a patch is hanging off the domain.

warp_polyline ¤

warp_polyline(
    points: list[list[float]], pa: list[float], pb: list[float], limit: float = 1.0
) -> list[list[float]] | None

End-anchored warp of a traced polyline onto moved endpoints, blending by arc length.

None when an end moves more than limit times the curve length.

Parameters:

  • points (list[[x, y, z]]) –
  • pa ([x, y, z]) –
  • pb ([x, y, z]) –
  • limit (float, default: 1.0 ) –

    Reject the warp when either end has to move further than limit times the curve's own length. A displacement of that size is not this curve moved, it is a different curve, and warping it produces a self-crossing polyline that densifies into folded quads.

Returns:

  • list[[x, y, z]] or None –

    None when the warp was rejected, so the caller can fall back to the straight chord.

Repair¤

densifiable ¤

densifiable(mesh: 'QuadMesh') -> tuple[bool, str]

Whether densification will succeed: quads or registered poles, none inverted, opposite edges share a strip.

Returns:

  • (bool, str) –

    Verdict and, when false, the first reason.

solve_non_quad_faces ¤

solve_non_quad_faces(
    mesh: "QuadMesh",
    cls: type | None = None,
    loops: list[list[list[float]]] | None = None,
    poles: Sequence[list[float]] = (),
) -> tuple["QuadMesh", str]

Make mesh a quad mesh: merge wall triangles, fan n-gons, keep triangles as poles, else quad-split.

Returns (mesh, note).

Parameters:

  • mesh (CoarsePseudoQuadMesh) –
  • cls (type, default: None ) –
  • loops (list[list[[x, y, z]]], default: None ) –

    Boundary loops, so a split vertex lands on the wall and not on a chord.

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

    Preferred pole positions. A triangle with a corner at one of these collapses there rather than wherever stage 3 would have chosen.

Returns:

  • (mesh, note) –

    The mesh, and a one-line description of what was needed -- empty when the input was already all quads.

topological_quad_split ¤

topological_quad_split(
    vertices: list[list[float]],
    faces: list[list[int]],
    loops: list[list[list[float]]] | None = None,
) -> tuple[list[list[float]], list[list[int]]]

Catmull-Clark topological split: every n-gon becomes n quads, no vertex moves.

Always all-quad and conforming, at the cost of a valence-n vertex per n-gon.

Returns: