editing¤
Hand-editing a coarse layout or a dense mesh, with no CAD in it. Rhino supplies only picks and prompts.
Classes¤
MeshEditor
¤
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
BoundaryLoopinstances. 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
meshuntouched until a commit transplants into it. DefaultTrue;Falseedits 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
BoundaryLoopinstances. -
last_reason(str) –Why the last operation was refused. Empty when nothing was.
Methods:¤
boundary_vertices
¤
Every vertex on every boundary, holes included, as a set read off the halfedges.
check_strip_count
¤
discard_last_undo
¤
Drop the most recent push_undo snapshot -- nothing changed.
is_vertex_on_boundary
¤
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 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
¤
The nearest point of the nearest wall, or xyz if there is no wall.
Only for vertices topologically on the mesh boundary.
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
¤
Restore the whole mesh as it was at construction or at the last snapshot.
strip_through
¤
The strip key through edge, resolved on a copy. None if none.
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;
commitwrites back into this object, keeping its identity. -
field(CrossField, default:None) –Carried so a caller can densify with it afterwards, and used for its
backgroundwhenloopsorsnap_tolare 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
fieldwhen 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
¤
Traced separatrices plus the user's drawn curves, as published to densification.
Methods:¤
add_strip
¤
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:
commit
¤
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
¤
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
¤
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 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
¤
The corners the pole at pkey may move to: those shared by all its triangles.
project
¤
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
¤
Back to the layout this round of edits started from, curves and poles included.
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
QuadMeshrather than a plaincompas.datastructures.Mesh, because the strip operations need its polyedges and strips. -
walls(list, default:None) –The domain walls, as point lists, closed polylines or
BoundaryLoopinstances. 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 toRELAX_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_linedid. -
last_deletion(dict) –What the last successful
remove_linedid.
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
¤
draw_edges
¤
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:
face_at
¤
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
¤
Vertices where two separate fans of faces meet at a single point.
non_quad_faces
¤
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-- whetherremove_linewould succeed with these arguments -- andreasonwhen it would not. Plusstrip,faces,collateral,boundaries_lost(a COUNT),boundaries_lost_vertices(which ones),to_split,faces_before/faces_afterandvertices_before/vertices_after. The counts after areNonewhen 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 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
¤
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:
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
¤
Delete a vertex and every face around it. (ok, notes).
Not refused when it leaves a non-manifold vertex; notes['non_manifold'] lists them.
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
pointswere sampled from -- fromcompas_rhino'scurve_to_compas, fromcompas_occ, acompas.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.
- Tutorial Smoothing and quality Smoothing functions
- Reference editing Guide chains
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 plaincompas.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_FACTORtimes 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;90turns 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_polyedgesfor 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:
reasonWhy the chain is empty, or''.tolerance,boundaryWhat it ran with.alignmentHow closely the chosen chain runs along the guide, 0 to 1.candidatesHow many polyedge runs were within tolerance of the guide.coverageThe share of the guide's length the chain spans, 0 to 1.gap_start,gap_endThe 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_curvesfor 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 ofcompas_singular.datastructures.constrained_smoothing.
- Tutorial Smoothing and quality Smoothing functions
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–nThe number of vertices.boundary_interiorBoundary 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_endsBoundary vertices at an end, 0 to 2. These are anchors, and are allowed.valid_pathWhether every consecutive pair is a real edge and no vertex repeats.purityThe share of interior triples that continue straight across the middle vertex. 1.0 means the chain IS a piece of a polyedge.runs,longest_runThe number of maximal straight runs and the longest, in vertices.turn_max,turn_meanThe 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.alignmentHow closely the chain's edges run along the guide, 0 to 1.off_max,off_meanDistance from the guide, in mean edge lengths.coverage,gap_start,gap_endThe share of the guide the chain spans, and what is left at each end.min_face_angle,folded_facesThe 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_driftHow 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
¤
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_facesso 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.
0disables snapping. A boundary corner further than this from every loop is left alone and counted inoff_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) andrepair(the noterepair.solve_non_quad_facesreturned).
face_polylines
¤
faces_from_geometry
¤
[[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
¤
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:
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
limittimes 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–Nonewhen the warp was rejected, so the caller can fall back to the straight chord.
Repair¤
densifiable
¤
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.