Halfedge orientation marks for crease pattern stacking order.
Many origami pipelines need to know, for each interior edge, which of the
two adjacent faces is above the other in the folded model. We encode
this via a single halfedge attribute, the THIS_WAY mark:
The face whose halfedge carries ``THIS_WAY`` lies *above* the face on
the opposite side, in the folded model.
This module provides several strategies for setting THIS_WAY automatically.
All assigners share two important conventions:
- Border halfedges are skipped. THIS_WAY is meaningful only on
interior edges, where two faces meet.
- Already-assigned edges are not overwritten. Each helper checks for
an existing THIS_WAY on either side of an edge and leaves it alone if
present. This makes the helpers chainable — apply a primary strategy
first, then a tiebreaker:
.. code-block:: python
# Primary: faces with larger area on top.
assign_this_way_by_face_area(G)
# Tiebreaker on equal-area edges: distance from center.
assign_this_way_by_distance(G)
Use :func:clear_this_way to start over.
Strategies
- :func:
assign_this_way_by_face_z_order and
:func:assign_this_way_by_vertex_z_order — explicit numeric z_order
attribute (e.g. set by user code or by a BFS).
- :func:
assign_this_way_by_face_bfs — BFS distance from a chosen source
face; nearer faces lie on top.
- :func:
assign_this_way_from_center — convenience: BFS from the face
closest to the geometric centroid of G.
- :func:
assign_this_way_by_distance — distance of face midpoints from a
point (default: centroid); nearer faces lie on top.
- :func:
assign_this_way_by_face_degree — larger-degree (or smaller-, see
flag) faces lie on top.
- :func:
assign_this_way_by_face_area — larger-area faces lie on top by
default.
THIS_WAY
module-attribute
Halfedge attribute name marking the upper side of an interior edge (h.face will lie above h.rev.face).
clear_this_way
clear_this_way(G: HalfEdgeGraph) -> None
Remove all THIS_WAY marks from halfedges of G.
Call before re-running an assignment from scratch.
Source code in pleat/shrink_rotate/crease_orientation.py
| def clear_this_way(G: HalfEdgeGraph) -> None:
"""Remove all THIS_WAY marks from halfedges of *G*.
Call before re-running an assignment from scratch.
"""
for e in G.halfedges:
if THIS_WAY in e.attributes:
del e[THIS_WAY]
|
assign_this_way_by_face_z_order
assign_this_way_by_face_z_order(
G: HalfEdgeGraph, key: str = "z_order"
) -> None
Use a numeric face attribute key to orient each interior edge.
The halfedge whose face has the larger value of key is left
unmarked; the opposite halfedge gets THIS_WAY (i.e. its face lies
above). Ties are broken using the mean key over the faces incident
to each endpoint.
Skips edges that already have THIS_WAY assigned on either side.
Source code in pleat/shrink_rotate/crease_orientation.py
| def assign_this_way_by_face_z_order(G: HalfEdgeGraph, key: str = "z_order") -> None:
"""Use a numeric face attribute ``key`` to orient each interior edge.
The halfedge whose face has the *larger* value of ``key`` is left
unmarked; the opposite halfedge gets THIS_WAY (i.e. its face lies
above). Ties are broken using the mean ``key`` over the faces incident
to each endpoint.
Skips edges that already have THIS_WAY assigned on either side.
"""
for e in _interior_unassigned_halfedges(G):
f1, f2 = e.face, e.rev.face
if f1[key] > f2[key]:
e[THIS_WAY] = True
elif f1[key] < f2[key]:
e.rev[THIS_WAY] = True
else:
z_orig = np.mean([f[key] for f in e.orig.true_face_iter()])
z_dest = np.mean([f[key] for f in e.dest.true_face_iter()])
if z_orig > z_dest:
e[THIS_WAY] = True
elif z_dest > z_orig:
e.rev[THIS_WAY] = True
|
assign_this_way_by_vertex_z_order
assign_this_way_by_vertex_z_order(
G: HalfEdgeGraph, key: str = "z_order"
) -> None
Like :func:assign_this_way_by_face_z_order but keyed on endpoint vertices.
Skips edges that already have THIS_WAY assigned on either side.
Source code in pleat/shrink_rotate/crease_orientation.py
| def assign_this_way_by_vertex_z_order(G: HalfEdgeGraph, key: str = "z_order") -> None:
"""Like :func:`assign_this_way_by_face_z_order` but keyed on endpoint vertices.
Skips edges that already have THIS_WAY assigned on either side.
"""
for e in _interior_unassigned_halfedges(G):
v1, v2 = e.orig, e.dest
if v1[key] > v2[key]:
e[THIS_WAY] = True
elif v1[key] < v2[key]:
e.rev[THIS_WAY] = True
else:
z_orig = np.mean([v[key] for v in e.face.vertex_iter()])
z_dest = np.mean([v[key] for v in e.rev.face.vertex_iter()])
if z_orig > z_dest:
e[THIS_WAY] = True
elif z_dest > z_orig:
e.rev[THIS_WAY] = True
|
assign_this_way_by_bfs
assign_this_way_by_bfs(
G: HalfEdgeGraph,
source: Vertex | Face | set[Vertex] | set[Face],
) -> None
Use BFS depth from source face: shallower lies on top.
Skips edges that already have THIS_WAY assigned on either side.
Source code in pleat/shrink_rotate/crease_orientation.py
| def assign_this_way_by_bfs(G: HalfEdgeGraph, source: Vertex | Face | set[Vertex] | set[Face]) -> None:
"""Use BFS depth from *source* face: shallower lies on top.
Skips edges that already have THIS_WAY assigned on either side.
"""
if not isinstance(source, set):
source = {source}
source_faces = {f for s in source for f in (s.true_face_iter() if isinstance(s, Vertex) else [s])}
source_vertices = {v for s in source for v in (s.vertex_iter() if isinstance(s, Face) else [s])}
assign_this_way_by_face_bfs(G, source_faces) # primary: face BFS
assign_this_way_by_vertex_bfs(G, source_vertices) # tiebreaker: vertex BFS
|
assign_this_way_by_face_bfs
assign_this_way_by_face_bfs(
G: HalfEdgeGraph, source: Face | set[Face]
) -> None
Use BFS depth from source face: shallower faces lie on top.
Faces visited later (greater BFS depth) are marked as lying below; the
halfedge of the deeper-visited face on each interior edge gets
THIS_WAY.
Skips edges that already have THIS_WAY assigned on either side.
Source code in pleat/shrink_rotate/crease_orientation.py
| def assign_this_way_by_face_bfs(G: HalfEdgeGraph, source: Face | set[Face]) -> None:
"""Use BFS depth from *source* face: shallower faces lie on top.
Faces visited later (greater BFS depth) are marked as lying below; the
halfedge of the deeper-visited face on each interior edge gets
THIS_WAY.
Skips edges that already have THIS_WAY assigned on either side.
"""
# Reuse the canonical BFS spanner so the traversal matches other helpers.
depth = _bfs_face_depth(source)
for e in _interior_unassigned_halfedges(G):
d_here = depth.get(e.face)
d_other = depth.get(e.rev.face)
if d_here is None or d_other is None:
continue
if d_here > d_other:
e.rev[THIS_WAY] = True
elif d_other > d_here:
e[THIS_WAY] = True
|
assign_this_way_by_vertex_bfs
assign_this_way_by_vertex_bfs(
G: HalfEdgeGraph, source: Vertex | set[Vertex]
) -> None
Use BFS depth from source vertex: shallower faces lie on top.
Faces visited later (greater BFS depth) are marked as lying below; the
halfedge of the deeper-visited face on each interior edge gets
THIS_WAY.
Skips edges that already have THIS_WAY assigned on either side.
Source code in pleat/shrink_rotate/crease_orientation.py
| def assign_this_way_by_vertex_bfs(G: HalfEdgeGraph, source: Vertex | set[Vertex]) -> None:
"""Use BFS depth from *source* vertex: shallower faces lie on top.
Faces visited later (greater BFS depth) are marked as lying below; the
halfedge of the deeper-visited face on each interior edge gets
THIS_WAY.
Skips edges that already have THIS_WAY assigned on either side.
"""
depth = _bfs_vertex_depth(source)
for e in _interior_unassigned_halfedges(G):
d_here = depth.get(e.orig)
d_other = depth.get(e.dest)
if d_here is None or d_other is None:
continue
if d_here > d_other:
e.rev[THIS_WAY] = True
elif d_other > d_here:
e[THIS_WAY] = True
|
assign_this_way_from_center
assign_this_way_from_center(G: HalfEdgeGraph) -> None
Convenience: BFS from the face nearest the geometric centroid of G.
Equivalent to selecting the centermost face and calling
:func:assign_this_way_by_bfs. Skips already-assigned edges.
Source code in pleat/shrink_rotate/crease_orientation.py
| def assign_this_way_from_center(G: HalfEdgeGraph) -> None:
"""Convenience: BFS from the face nearest the geometric centroid of *G*.
Equivalent to selecting the centermost face and calling
:func:`assign_this_way_by_bfs`. Skips already-assigned edges.
"""
center = _graph_centroid(G)
src = _face_nearest_to(G, center)
assign_this_way_by_bfs(G, src)
|
assign_this_way_by_distance
assign_this_way_by_distance(
G: HalfEdgeGraph, point=None
) -> None
Orient interior edges by distance of face midpoints from point.
The face whose midpoint is farther from point lies below (gets
THIS_WAY). When point is None the centroid of G is used.
Skips already-assigned edges.
Source code in pleat/shrink_rotate/crease_orientation.py
| def assign_this_way_by_distance(G: HalfEdgeGraph, point=None) -> None:
"""Orient interior edges by distance of face midpoints from *point*.
The face whose midpoint is *farther* from *point* lies below (gets
THIS_WAY). When *point* is ``None`` the centroid of *G* is used.
Skips already-assigned edges.
"""
point = _graph_centroid(G) if point is None else np.asarray(point)
for e in _interior_unassigned_halfedges(G):
d_here = float(np.linalg.norm(np.asarray(e.face.midpoint()) - point))
d_other = float(np.linalg.norm(np.asarray(e.rev.face.midpoint()) - point))
if d_here > d_other:
e.rev[THIS_WAY] = True
elif d_other > d_here:
e[THIS_WAY] = True
else: # faces have the same distance; go by distance of endpoints of the edge
d_orig = float(np.linalg.norm(np.asarray(e.orig["pos"]) - point))
d_dest = float(np.linalg.norm(np.asarray(e.dest["pos"]) - point))
if d_orig > d_dest:
e.rev[THIS_WAY] = True
elif d_dest > d_orig:
e[THIS_WAY] = True
|
assign_this_way_by_face_degree
assign_this_way_by_face_degree(
G: HalfEdgeGraph, larger_on_top: bool = True
) -> None
Orient interior edges by face degree (number of incident edges).
With larger_on_top (the default), the smaller-degree face is marked
as lying below. Set larger_on_top=False to invert.
Skips already-assigned edges.
Source code in pleat/shrink_rotate/crease_orientation.py
| def assign_this_way_by_face_degree(G: HalfEdgeGraph, larger_on_top: bool = True) -> None:
"""Orient interior edges by face degree (number of incident edges).
With *larger_on_top* (the default), the smaller-degree face is marked
as lying below. Set ``larger_on_top=False`` to invert.
Skips already-assigned edges.
"""
for e in _interior_unassigned_halfedges(G):
d_here = e.face.order()
d_other = e.rev.face.order()
if d_here == d_other:
continue
higher = (d_here > d_other) == larger_on_top
if higher:
e[THIS_WAY] = True
else:
e.rev[THIS_WAY] = True
|
assign_this_way_by_face_area
assign_this_way_by_face_area(
G: HalfEdgeGraph, larger_on_top: bool = True
) -> None
Orient interior edges by face area.
With larger_on_top (the default), the smaller-area face lies below.
Set larger_on_top=False to invert.
Skips already-assigned edges.
Source code in pleat/shrink_rotate/crease_orientation.py
| def assign_this_way_by_face_area(G: HalfEdgeGraph, larger_on_top: bool = True) -> None:
"""Orient interior edges by face area.
With *larger_on_top* (the default), the smaller-area face lies below.
Set ``larger_on_top=False`` to invert.
Skips already-assigned edges.
"""
areas = {f: f.area() for f in G.faces if not f.on_border()}
for e in _interior_unassigned_halfedges(G):
a_here = areas.get(e.face)
a_other = areas.get(e.rev.face)
if a_here is None or a_other is None or a_here == a_other:
continue
higher = (a_here > a_other) == larger_on_top
if higher:
e[THIS_WAY] = True
else:
e.rev[THIS_WAY] = True
|