init_code
This commit is contained in:
998
pygarment/garmentcode/edge.py
Normal file
998
pygarment/garmentcode/edge.py
Normal file
@@ -0,0 +1,998 @@
|
||||
from copy import deepcopy, copy
|
||||
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
import svgpathtools as svgpath # https://github.com/mathandy/svgpathtools
|
||||
|
||||
from pygarment.garmentcode.utils import R2D
|
||||
from pygarment.garmentcode.utils import close_enough
|
||||
from pygarment.garmentcode.utils import c_to_list
|
||||
from pygarment.garmentcode.utils import list_to_c
|
||||
from pygarment.pattern.utils import rel_to_abs_2d, abs_to_rel_2d
|
||||
|
||||
ILENGTH_S_TOL = 1e-10 # NOTE: tolerance value for evaluating curve parameter (t) from acr length
|
||||
|
||||
class Edge:
|
||||
"""Edge an individual segment of a panel border connecting two panel
|
||||
vertices, the basic building block of panels
|
||||
|
||||
Edges are defined on 2D coordinate system with Start vertex as an origin
|
||||
and (End-Start) as Ox axis
|
||||
"""
|
||||
|
||||
def __init__(self, start=None, end=None, label='') -> None:
|
||||
""" Simple edge initialization.
|
||||
Parameters:
|
||||
* start, end: from/to vertices that the edge connects,
|
||||
describing the _interface_ of an edge
|
||||
* label: semantic label of the edge to be writted down as a property on assembly
|
||||
|
||||
# TODOLOW Add support for fold schemes to allow guided folds at
|
||||
the edge (e.g. pleats)
|
||||
"""
|
||||
if start is None:
|
||||
start = [0, 0]
|
||||
if end is None:
|
||||
end = [0, 0]
|
||||
assert not all(close_enough(s, e) for s, e in zip(start, end)), 'Start and end of an edge should differ'
|
||||
|
||||
self.start = start # NOTE: careful with references to vertex objects
|
||||
self.end = end
|
||||
|
||||
# Semantic label
|
||||
self.label = label
|
||||
|
||||
# ID w.r.t. other edges in a super-panel
|
||||
# Filled out at the panel assembly time
|
||||
self.geometric_id = 0
|
||||
|
||||
def length(self):
|
||||
"""Return current length of an edge.
|
||||
Since vertices may change their locations externally, the length
|
||||
is dynamically evaluated
|
||||
"""
|
||||
return self._straight_len()
|
||||
|
||||
def _straight_len(self):
|
||||
"""Length of the edge ignoring the curvature"""
|
||||
return norm(np.asarray(self.end) - np.asarray(self.start))
|
||||
|
||||
def __eq__(self, __o: object, tol=1e-2) -> bool:
|
||||
"""Special implementation of comparison: same edges == edges can be
|
||||
connected by flat stitch
|
||||
Edges are the same if their length is the same (if their flattened
|
||||
representation is the same) => vertices do not have to be on the
|
||||
same locations
|
||||
|
||||
NOTE: The edges may not have the same curvature and still be
|
||||
considered equal ("connectible")
|
||||
"""
|
||||
|
||||
if not isinstance(__o, Edge):
|
||||
return False
|
||||
|
||||
# Base length is the same
|
||||
if close_enough(self.length(), __o.length(), tol=tol):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'Straight:[{self.start[0]:.2f}, {self.start[1]:.2f}]->[{self.end[0]:.2f}, {self.end[1]:.2f}]'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
""" 'Official string representation' -- for nice printing of lists of edges
|
||||
|
||||
https://stackoverflow.com/questions/3558474/how-to-apply-str-function-when-printing-a-list-of-objects-in-python
|
||||
"""
|
||||
return self.__str__()
|
||||
|
||||
def midpoint(self):
|
||||
"""Center of the edge"""
|
||||
return (np.array(self.start) + np.array(self.end)) / 2
|
||||
|
||||
def shortcut(self):
|
||||
"""Return straight shortcut for an edge,
|
||||
as `np.array`
|
||||
|
||||
For straight edges it's the same as the edge itself
|
||||
"""
|
||||
|
||||
return np.array([self.start, self.end])
|
||||
|
||||
def flip_x_axis(self):
|
||||
"""Flips the Bezier curve along the x-axis by inverting the y-coordinates"""
|
||||
|
||||
self.start[1] = -self.start[1]
|
||||
self.end[1] = -self.end[1]
|
||||
return self
|
||||
|
||||
|
||||
# Representation
|
||||
def as_curve(self):
|
||||
"""As svgpath curve object"""
|
||||
# Get the nodes correcly
|
||||
nodes = np.vstack((self.start, self.end))
|
||||
|
||||
params = nodes[:, 0] + 1j*nodes[:, 1]
|
||||
|
||||
return svgpath.Line(*params)
|
||||
|
||||
def linearize(self, n_verts_inside = 0):
|
||||
"""Return a linear approximation of an edge using the same vertex objects
|
||||
|
||||
# NOTE: for the linear edge it is an egde if n_verts_inside = 0,
|
||||
# else n_verts_inside = number of vertices (excluding the start
|
||||
and end vertices) used to create a linearization of the edge
|
||||
"""
|
||||
|
||||
if not n_verts_inside:
|
||||
return EdgeSequence(self)
|
||||
else:
|
||||
n = n_verts_inside + 1
|
||||
tvals = np.linspace(0, 1, n, endpoint=False)[1:]
|
||||
|
||||
curve = self.as_curve()
|
||||
edge_verts = [c_to_list(curve.point(t)) for t in tvals]
|
||||
seq = self.to_edge_sequence(edge_verts)
|
||||
|
||||
return seq
|
||||
|
||||
def to_edge_sequence(self, edge_verts):
|
||||
"""
|
||||
Returns the edge as a sequence of STRAIGHT edges based on points
|
||||
sampled on the edge between `self.start` and `self.end` (edge_verts).
|
||||
"""
|
||||
seq = EdgeSequence(Edge(self.start, edge_verts[0]))
|
||||
for i in range(1, len(edge_verts)):
|
||||
seq.append(Edge(seq[-1].end, edge_verts[i]))
|
||||
seq.append(Edge(seq[-1].end, self.end))
|
||||
|
||||
return seq
|
||||
|
||||
# Actions
|
||||
def reverse(self):
|
||||
"""Flip the direction of the edge"""
|
||||
self.start, self.end = self.end, self.start
|
||||
|
||||
return self
|
||||
|
||||
def reflect_features(self):
|
||||
"""Reflect edge fetures from one side of the edge to the other"""
|
||||
# Nothing to do for straight edge
|
||||
return self
|
||||
|
||||
def snap_to(self, new_start=None):
|
||||
"""Translate the edge vertices s.t. the start is at new_start
|
||||
"""
|
||||
if new_start is None:
|
||||
new_start = [0, 0]
|
||||
|
||||
self.end[0] = self.end[0] - self.start[0] + new_start[0]
|
||||
self.end[1] = self.end[1] - self.start[1] + new_start[1]
|
||||
self.start[:] = new_start
|
||||
return self
|
||||
|
||||
def rotate(self, angle):
|
||||
"""Rotate edge by angle in place, using first point as a reference
|
||||
|
||||
Parameters:
|
||||
angle -- desired rotation angle in radians (!)
|
||||
"""
|
||||
curr_start = copy(self.start)
|
||||
|
||||
# set the start point to zero
|
||||
self.snap_to([0, 0])
|
||||
self.end[:] = np.matmul(R2D(angle), self.end)
|
||||
|
||||
# recover the original location
|
||||
self.snap_to(curr_start)
|
||||
|
||||
return self
|
||||
|
||||
def subdivide_len(self, fractions: list, connect_internal_verts=True):
|
||||
"""Add intermediate vertices to an edge,
|
||||
splitting its length according to fractions
|
||||
while preserving the overall shape
|
||||
|
||||
* merge_internal -- if False, the newly inserted vertices would be
|
||||
defined
|
||||
as independent objects for each edge. If True, vertex objects
|
||||
will be shared
|
||||
"""
|
||||
# Parametrized by length
|
||||
new_edges = self._subdivide(fractions, by_length=True)
|
||||
|
||||
if connect_internal_verts:
|
||||
self._merge_subdiv_vertices(new_edges)
|
||||
|
||||
return new_edges
|
||||
|
||||
def subdivide_param(self, fractions: list, connect_internal_verts=True):
|
||||
"""Add intermediate vertices to an edge,
|
||||
splitting its curve parametrization according to fractions
|
||||
while preserving the overall shape
|
||||
|
||||
NOTE: for line, it's the same as subdivision by length
|
||||
"""
|
||||
|
||||
new_edges = self._subdivide(fractions, by_length=False)
|
||||
|
||||
if connect_internal_verts:
|
||||
self._merge_subdiv_vertices(new_edges)
|
||||
|
||||
return new_edges
|
||||
|
||||
def _subdivide(self, fractions: list, by_length=True):
|
||||
"""Subdivide edge by length or curve parametrization
|
||||
|
||||
NOTE: equivalent for straight lines
|
||||
"""
|
||||
|
||||
frac = [abs(f) for f in fractions]
|
||||
if not close_enough(fsum := sum(frac), 1, 1e-4):
|
||||
raise RuntimeError(f'Edge Subdivision::ERROR::fraction is incorrect. The sum {fsum} is not 1')
|
||||
|
||||
vec = np.asarray(self.end) - np.asarray(self.start)
|
||||
verts = [self.start]
|
||||
seq = EdgeSequence()
|
||||
for i in range(len(frac) - 1):
|
||||
verts.append(
|
||||
[verts[-1][0] + frac[i]*vec[0],
|
||||
verts[-1][1] + frac[i]*vec[1]]
|
||||
)
|
||||
seq.append(Edge(verts[-2], verts[-1]))
|
||||
verts.append(self.end)
|
||||
seq.append(Edge(verts[-2], verts[-1]))
|
||||
|
||||
return seq
|
||||
|
||||
def _merge_subdiv_vertices(self, subdivision):
|
||||
"""Merge the vertices from cosecutive edges in the given edge subdivision"""
|
||||
|
||||
for i in range(1, len(subdivision)):
|
||||
subdivision[i].start = subdivision[i-1].end
|
||||
return subdivision
|
||||
|
||||
# Assembly into serializable object
|
||||
def assembly(self):
|
||||
"""Returns the dict-based representation of edges,
|
||||
compatible with core -> BasePattern JSON (dict)
|
||||
"""
|
||||
properties = {"endpoints": [0, 1]}
|
||||
if self.label:
|
||||
properties['label'] = self.label
|
||||
|
||||
return [self.start, self.end], properties
|
||||
|
||||
|
||||
class CircleEdge(Edge):
|
||||
"""Curvy edge as circular arc"""
|
||||
|
||||
def __init__(self, start=None, end=None, cy=None, label='') -> None:
|
||||
"""
|
||||
Define a circular arc edge
|
||||
* start, end: from/to vertices that the edge connects
|
||||
* cy: third point on a circle arc (= control point).
|
||||
Expressed relatively w.r.t. distance between start and end.
|
||||
X value for control point is fixed at x=0.5 (edge center) to
|
||||
avoid ambiguity
|
||||
* label: semantic label of the edge to be writted down as a property on assembly
|
||||
|
||||
NOTE: representing control point in relative coordinates
|
||||
allows preservation of curvature (arc angle, relative radius
|
||||
w.r.t. straight edge length)
|
||||
When distance between vertices shrinks / extends
|
||||
|
||||
NOTE: full circle not supported: start & end should differ
|
||||
"""
|
||||
if start is None:
|
||||
start = [0, 0]
|
||||
if end is None:
|
||||
end = [1, 0]
|
||||
super().__init__(start, end, label=label)
|
||||
self.control_y = cy
|
||||
|
||||
def length(self):
|
||||
"""Return current length of an edge.
|
||||
Since vertices may change their locations externally, the length
|
||||
is dynamically evaluated
|
||||
"""
|
||||
return self._rel_radius() * self._straight_len() * self._arc_angle()
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
points = [self.start, [0.5, self.control_y]]
|
||||
|
||||
str = [f'[{p[0]:.2f}, {p[1]:.2f}]->' for p in points]
|
||||
str += [f'[{self.end[0]:.2f}, {self.end[1]:.2f}]']
|
||||
|
||||
return 'Arc:' + ''.join(str)
|
||||
|
||||
def midpoint(self):
|
||||
"""Center of the edge"""
|
||||
return rel_to_abs_2d(self.start, self.end, [0.5, self.control_y])
|
||||
|
||||
# Actions
|
||||
def reverse(self):
|
||||
"""Flip the direction of the edge, accounting for curvatures"""
|
||||
|
||||
self.start, self.end = self.end, self.start
|
||||
self.control_y *= -1
|
||||
|
||||
return self
|
||||
|
||||
def reflect_features(self):
|
||||
"""Reflect edge features from one side of the edge to the other"""
|
||||
|
||||
self.control_y *= -1
|
||||
|
||||
return self
|
||||
|
||||
def _subdivide(self, fractions: list, by_length=False):
|
||||
"""Add intermediate vertices to an edge,
|
||||
splitting its parametrization according to fractions
|
||||
while preserving the overall shape
|
||||
|
||||
NOTE: param subdiv == length subdiv for circle arcs
|
||||
"""
|
||||
# NOTE: subdivide_param() is the same as subdivide_len()
|
||||
# So parent implementation is ok
|
||||
# TODOLOW Implementation is very similar to CurveEdge param-based subdivision
|
||||
|
||||
from pygarment.garmentcode.edge_factory import EdgeFactory # TODOLOW: ami - better solution?
|
||||
frac = [abs(f) for f in fractions]
|
||||
if not close_enough(fsum := sum(frac), 1, 1e-4):
|
||||
raise RuntimeError(f'Edge Subdivision::ERROR::fraction is incorrect. The sum {fsum} is not 1')
|
||||
|
||||
curve = self.as_curve()
|
||||
# Sub-curves
|
||||
covered_fr = 0
|
||||
subcurves = []
|
||||
for fr in fractions:
|
||||
subcurves.append(curve.cropped(covered_fr, covered_fr + fr))
|
||||
covered_fr += fr
|
||||
|
||||
# Convert to CircleEdge objects
|
||||
subedges = EdgeSequence()
|
||||
for curve in subcurves:
|
||||
subedges.append(EdgeFactory.from_svg_curve(curve))
|
||||
# Reference the first/last vertices correctly
|
||||
subedges[0].start = self.start
|
||||
subedges[-1].end = self.end
|
||||
|
||||
return subedges
|
||||
|
||||
# Special tools for circle representation
|
||||
def as_curve(self):
|
||||
"""Represent as svgpath Arc"""
|
||||
|
||||
radius, la, sweep = self.as_radius_flag()
|
||||
|
||||
return svgpath.Arc(
|
||||
list_to_c(self.start),
|
||||
list_to_c([radius, radius]), 0, la, sweep,
|
||||
list_to_c(self.end)
|
||||
)
|
||||
|
||||
def as_radius_flag(self):
|
||||
"""Return circle representation as radius and arc flags"""
|
||||
|
||||
return (self._rel_radius() * self._straight_len(),
|
||||
self._is_large_arc(),
|
||||
self.control_y < 0) # left/right orientation
|
||||
|
||||
def as_radius_angle(self):
|
||||
"""Return circle representation as radius and an angle"""
|
||||
|
||||
return (
|
||||
self._rel_radius() * self._straight_len(),
|
||||
self._arc_angle(),
|
||||
self.control_y < 0
|
||||
)
|
||||
|
||||
def linearize(self, n_verts_inside = 9):
|
||||
"""Return a linear approximation of an edge using the same vertex objects
|
||||
NOTE: n_verts_inside = number of vertices (excluding the start
|
||||
and end vertices) used to create a linearization of the edge
|
||||
"""
|
||||
n = n_verts_inside + 1
|
||||
tvals = np.linspace(0, 1, n, endpoint=False)[1:]
|
||||
|
||||
curve = self.as_curve()
|
||||
edge_verts = [c_to_list(curve.point(t)) for t in tvals]
|
||||
seq = self.to_edge_sequence(edge_verts)
|
||||
|
||||
return seq
|
||||
|
||||
# NOTE: The following values are calculated at runtime to allow
|
||||
# changes to control point after the edge definition
|
||||
def _rel_radius(self, abs_radius=None):
|
||||
"""Eval relative radius (w.r.t. straight distance) from 3-point
|
||||
representation"""
|
||||
|
||||
if abs_radius:
|
||||
return abs_radius / self._straight_len()
|
||||
|
||||
# Using the formula for radius of circumscribed circle
|
||||
# https://en.wikipedia.org/wiki/Circumscribed_circle#Other_properties
|
||||
|
||||
# triangle sides, assuming the begginning and end of an edge are at
|
||||
# (0, 0) and (1, 0)
|
||||
# accordingly
|
||||
a = 1
|
||||
b = norm([0.5, self.control_y])
|
||||
c = norm([0.5 - 1, self.control_y])
|
||||
p = (a + b + c) / 2 # semiperimeter
|
||||
|
||||
rad = a * b * c / np.sqrt(p * (p - a) * (p - b) * (p - c)) / 4
|
||||
|
||||
return rad
|
||||
|
||||
def _arc_angle(self):
|
||||
"""Eval arc angle from control point"""
|
||||
rel_rad = self._rel_radius()
|
||||
|
||||
# NOTE: Bound the sin to avoid out of bounds errors
|
||||
# due to floating point error accumulation
|
||||
arc = 2 * np.arcsin(min(max(1 / rel_rad / 2, -1.), 1.))
|
||||
|
||||
if self._is_large_arc():
|
||||
arc = 2 * np.pi - arc
|
||||
|
||||
return arc
|
||||
|
||||
def _is_large_arc(self):
|
||||
"""Indicate if the arc sweeps the large or small angle"""
|
||||
return abs(self.control_y) > self._rel_radius()
|
||||
|
||||
def assembly(self):
|
||||
"""Returns the dict-based representation of edges,
|
||||
compatible with core -> BasePattern JSON (dict)
|
||||
"""
|
||||
ends, props = super().assembly()
|
||||
|
||||
# NOTE: arc representation is the same as in SVG
|
||||
rad, large_arc, right = self.as_radius_flag()
|
||||
props['curvature'] = {
|
||||
"type": 'circle',
|
||||
"params": [rad, int(large_arc), int(right)]
|
||||
}
|
||||
return ends, props
|
||||
|
||||
|
||||
class CurveEdge(Edge):
|
||||
"""Curvy edge as Besier curve / B-spline"""
|
||||
|
||||
def __init__(self, start=None, end=None, control_points=None,
|
||||
relative=True,
|
||||
label='') -> None:
|
||||
"""Define a Bezier curve edge
|
||||
* start, end: from/to vertices that the edge connects
|
||||
* control_points: coordinated of Bezier control points.
|
||||
Specification of One control point creates the Quadratic Bezier,
|
||||
Specification of 2 control points creates Cubic Bezier.
|
||||
Other degrees are not supported.
|
||||
* label: semantic label of the edge to be writted down as a property on assembly
|
||||
|
||||
* relative: specify whether the control point coordinated are given
|
||||
relative to the edge length (True) or in 2D coordinate system of a
|
||||
panel (False)
|
||||
|
||||
"""
|
||||
if control_points is None:
|
||||
control_points = []
|
||||
if start is None:
|
||||
start = [0, 0]
|
||||
if end is None:
|
||||
end = [0, 0]
|
||||
super().__init__(start, end, label=label)
|
||||
|
||||
self.control_points = control_points
|
||||
|
||||
if len(self.control_points) > 2:
|
||||
raise NotImplementedError(f'{self.__class__.__name__}::ERROR::Up to 2 control points (cubic Bezier) are supported')
|
||||
|
||||
# Storing control points as relative since it preserves overall curve
|
||||
# shape during edge extension/contraction
|
||||
if not relative:
|
||||
self.control_points = [abs_to_rel_2d(self.start, self.end, c).tolist()
|
||||
for c in self.control_points]
|
||||
def flip_x_axis(self):
|
||||
"""Flips the Bezier curve along the x-axis by inverting the y-coordinates"""
|
||||
abs_control_points = [rel_to_abs_2d(self.start, self.end, c) for c in self.control_points]
|
||||
self.start[1] = -self.start[1]
|
||||
self.end[1] = -self.end[1]
|
||||
|
||||
abs_control_points= [[x, -y] for x, y in abs_control_points]
|
||||
self.control_points= [abs_to_rel_2d(self.start, self.end, c).tolist() for c in abs_control_points]
|
||||
return self
|
||||
def length(self):
|
||||
"""Length of Bezier curve edge"""
|
||||
curve = self.as_curve()
|
||||
|
||||
return curve.length()
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
points = [self.start] + self.control_points
|
||||
|
||||
str = [f'[{p[0]:.2f}, {p[1]:.2f}]->' for p in points]
|
||||
str += [f'[{self.end[0]:.2f}, {self.end[1]:.2f}]']
|
||||
|
||||
return 'Curve:' + ''.join(str)
|
||||
|
||||
def midpoint(self):
|
||||
"""Center of the edge"""
|
||||
curve = self.as_curve()
|
||||
|
||||
t_mid = curve.ilength(curve.length()/2, s_tol=ILENGTH_S_TOL)
|
||||
return c_to_list(curve.point(t_mid))
|
||||
|
||||
def _subdivide(self, fractions: list, by_length=False):
|
||||
"""Add intermediate vertices to an edge,
|
||||
splitting its curve parametrization or overall length according to
|
||||
fractions while preserving the overall shape
|
||||
"""
|
||||
from pygarment.garmentcode.edge_factory import EdgeFactory # TODOLOW: ami - better solution?
|
||||
curve = self.as_curve()
|
||||
|
||||
# Sub-curves
|
||||
covered_fr, prev_t = 0, 0
|
||||
clen = curve.length()
|
||||
subcurves = []
|
||||
for fr in fractions:
|
||||
covered_fr += fr
|
||||
if by_length:
|
||||
next_t = curve.ilength(clen * covered_fr, s_tol=ILENGTH_S_TOL)
|
||||
subcurves.append(curve.cropped(prev_t, next_t))
|
||||
prev_t = next_t
|
||||
else:
|
||||
subcurves.append(curve.cropped(covered_fr - fr, covered_fr))
|
||||
|
||||
# Convert to CurveEdge objects
|
||||
subedges = EdgeSequence()
|
||||
for curve in subcurves:
|
||||
subedges.append(EdgeFactory.from_svg_curve(curve))
|
||||
# Reference the first/last vertices correctly
|
||||
subedges[0].start = self.start
|
||||
subedges[-1].end = self.end
|
||||
|
||||
return subedges
|
||||
|
||||
# Actions
|
||||
def reverse(self):
|
||||
"""Flip the direction of the edge, accounting for curvatures"""
|
||||
|
||||
self.start, self.end = self.end, self.start
|
||||
|
||||
# change order of control points
|
||||
if len(self.control_points) == 2:
|
||||
self.control_points[0], self.control_points[1] = self.control_points[1], self.control_points[0]
|
||||
|
||||
# Update coordinates
|
||||
for p in self.control_points:
|
||||
p[0], p[1] = 1 - p[0], -p[1]
|
||||
|
||||
return self
|
||||
|
||||
def reflect_features(self):
|
||||
"""Reflect edge fetures from one side of the edge to the other"""
|
||||
|
||||
for p in self.control_points:
|
||||
p[1] = -p[1]
|
||||
|
||||
return self
|
||||
|
||||
def as_curve(self, absolute=True):
|
||||
"""As svgpath curve object
|
||||
|
||||
Converting on the fly as exact vertex location might have been updated since
|
||||
the creation of the edge
|
||||
"""
|
||||
# Get the nodes correcly
|
||||
if absolute:
|
||||
cp = [rel_to_abs_2d(self.start, self.end, c) for c in self.control_points]
|
||||
nodes = np.vstack((self.start, cp, self.end))
|
||||
else:
|
||||
cp = self.control_points
|
||||
nodes = np.vstack(([0, 0], cp, [1, 0]))
|
||||
|
||||
params = nodes[:, 0] + 1j*nodes[:, 1]
|
||||
|
||||
return svgpath.QuadraticBezier(*params) if len(cp) < 2 else svgpath.CubicBezier(*params)
|
||||
|
||||
def linearize(self, n_verts_inside=9):
|
||||
"""Return a linear approximation of an edge using the same vertex objects
|
||||
NOTE: n_verts_inside = number of vertices (excluding the start
|
||||
and end vertices) used to create a linearization of the edge
|
||||
|
||||
"""
|
||||
n = n_verts_inside + 1
|
||||
tvals_init = np.linspace(0, 1, n, endpoint=False)[1:]
|
||||
|
||||
curve = self.as_curve(absolute=False)
|
||||
curve_lengths = tvals_init * curve.length()
|
||||
tvals = [curve.ilength(c_len, s_tol=ILENGTH_S_TOL) for c_len in curve_lengths]
|
||||
|
||||
edge_verts = [rel_to_abs_2d(self.start, self.end, c_to_list(curve.point(t))) for t in tvals]
|
||||
seq = self.to_edge_sequence(edge_verts)
|
||||
|
||||
return seq
|
||||
|
||||
def _extreme_points(self):
|
||||
"""Return extreme points (on Y) of the current edge
|
||||
NOTE: this does NOT include the border vertices of an edge
|
||||
"""
|
||||
|
||||
# Variation of https://github.com/mathandy/svgpathtools/blob/5c73056420386753890712170da602493aad1860/svgpathtools/bezier.py#L197
|
||||
curve = self.as_curve(absolute=False) # relative coords to find real extremizers
|
||||
poly = svgpath.bezier2polynomial(curve, return_poly1d=True)
|
||||
y = svgpath.imag(poly)
|
||||
dy = y.deriv()
|
||||
y_extremizers = svgpath.polyroots(
|
||||
dy, realroots=True, condition=lambda r: 0 < r < 1)
|
||||
|
||||
extreme_points = np.array(
|
||||
[rel_to_abs_2d(self.start, self.end, c_to_list(curve.point(t)))
|
||||
for t in y_extremizers]
|
||||
)
|
||||
|
||||
return extreme_points
|
||||
|
||||
# Assembly into serializable object
|
||||
def assembly(self):
|
||||
"""Returns the dict-based representation of edges,
|
||||
compatible with core -> BasePattern JSON (dict)
|
||||
"""
|
||||
|
||||
ends, props = super().assembly()
|
||||
|
||||
props['curvature'] = {
|
||||
"type": 'quadratic' if len(self.control_points) == 1 else 'cubic',
|
||||
"params": self.control_points
|
||||
}
|
||||
return ends, props
|
||||
|
||||
|
||||
class EdgeSequence:
|
||||
"""Represents a sequence of (possibly chained) edges (e.g. every next edge
|
||||
starts from the same vertex that the previous edge ends with and
|
||||
allows building some typical edge sequences
|
||||
"""
|
||||
def __init__(self, *args, verbose: bool = False) -> None:
|
||||
self.edges = []
|
||||
self.verbose = verbose
|
||||
for arg in args:
|
||||
self.append(arg)
|
||||
|
||||
# ANCHOR Properties
|
||||
def __getitem__(self, i):
|
||||
if isinstance(i, slice):
|
||||
# return an EdgeSequence object for slices
|
||||
e_slice = self.edges[i]
|
||||
return EdgeSequence(e_slice)
|
||||
else:
|
||||
return self.edges[i]
|
||||
|
||||
def index(self, elem):
|
||||
# Find the same object (by reference)
|
||||
# list.index() is doing something different..
|
||||
# https://stackoverflow.com/a/47057419
|
||||
return next(i for i, e in enumerate(self.edges) if elem is e)
|
||||
|
||||
def __len__(self):
|
||||
"""Number of edges in the sequence"""
|
||||
return len(self.edges)
|
||||
|
||||
def __contains__(self, item):
|
||||
# check presence by comparing references
|
||||
return any([item is e for e in self.edges])
|
||||
|
||||
def __str__(self) -> str:
|
||||
return 'EdgeSeq: ' + str(self.edges)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def length(self):
|
||||
"""Total length of edges"""
|
||||
return sum([e.length() for e in self.edges])
|
||||
|
||||
def isLoop(self):
|
||||
return self.edges[0].start == self.edges[-1].end and len(self) > 1 #modify is to ==
|
||||
|
||||
def isChained(self):
|
||||
"""Does the sequence of edges represent correct chain?"""
|
||||
if len(self) < 2:
|
||||
return False
|
||||
|
||||
for i in range(1, len(self.edges)):
|
||||
if self.edges[i].start is not self.edges[i-1].end:
|
||||
if self.verbose:
|
||||
# This should be helpful to catch bugs
|
||||
print(f'{self.__class__.__name__}::WARNING!::Edge sequence is not properly chained')
|
||||
return False
|
||||
return True
|
||||
|
||||
def fractions(self) -> list:
|
||||
"""Fractions of the lengths of each edge in sequence w.r.t.
|
||||
the whole sequence
|
||||
"""
|
||||
total_len = sum([e.length() for e in self.edges])
|
||||
|
||||
return [e.length() / total_len for e in self.edges]
|
||||
|
||||
def lengths(self) -> list:
|
||||
"""Lengths of individual edges in the sequence"""
|
||||
return [e.length() for e in self.edges]
|
||||
|
||||
def verts(self):
|
||||
"""Return all vertex objects"""
|
||||
verts = [self.edges[0].start]
|
||||
for e in self.edges:
|
||||
if e.start is not verts[-1]: # avoid adding the vertices of chained edges twice
|
||||
verts.append(e.start)
|
||||
verts.append(e.end)
|
||||
if verts[0] is verts[-1]: # don't double count the loop origin
|
||||
verts.pop(-1)
|
||||
return verts
|
||||
|
||||
def shortcut(self):
|
||||
"""Opening of an edge sequence as a vector
|
||||
|
||||
# NOTE May not reflect true shortcut if the egdes were flipped but
|
||||
the order remained
|
||||
"""
|
||||
return np.array([self[0].start, self[-1].end])
|
||||
|
||||
def bbox(self):
|
||||
"""
|
||||
This function evaluates the 2D bounding box of the current panel and
|
||||
returns the panel vertices which are located on the bounding box (
|
||||
b_points).
|
||||
Output:
|
||||
* bbox (list): [min_x, max_x, min_y, max_y] of verts_2d
|
||||
* b_points (list): list of 2D vertices representing the b_points,
|
||||
i.e., the vertices of verts_2d located on the bounding box
|
||||
"""
|
||||
# Take linear version of the edges
|
||||
# To correctly process edges with extreme curvatures
|
||||
|
||||
lin_edges = EdgeSequence([e.linearize() for e in self.edges])
|
||||
verts_2d = np.asarray(lin_edges.verts())
|
||||
mi = verts_2d.min(axis=0)
|
||||
ma = verts_2d.max(axis=0)
|
||||
xs = [mi[0], ma[0]]
|
||||
ys = [mi[1], ma[1]]
|
||||
#return points on bounding box
|
||||
b_points = []
|
||||
for v in verts_2d:
|
||||
if v[0] in xs or v[1] in ys:
|
||||
b_points.append(v)
|
||||
if len(b_points) == 2:
|
||||
if not any(np.array_equal(arr, mi) for arr in b_points):
|
||||
b_points = [b_points[0], mi, b_points[1]]
|
||||
else:
|
||||
p = [mi[0],ma[1]]
|
||||
b_points = [b_points[0],p,b_points[1]]
|
||||
|
||||
# FIXME Use one common order for the bbox output
|
||||
bbox = [mi[0], ma[0], mi[1], ma[1]]
|
||||
|
||||
return bbox, b_points
|
||||
|
||||
# ANCHOR Modifiers
|
||||
# All modifiers return self object to allow chaining
|
||||
# Wrappers around python's list
|
||||
def append(self, item):
|
||||
if isinstance(item, Edge):
|
||||
self.edges.append(item)
|
||||
elif isinstance(item, list): # List of edge / EdgeSeq objects
|
||||
for e in item:
|
||||
self.append(e)
|
||||
elif isinstance(item, EdgeSequence):
|
||||
self.edges += item.edges
|
||||
else:
|
||||
raise ValueError(f'{self.__class__.__name__}::ERROR::Trying to add object of incompatible type {type(item)}')
|
||||
return self
|
||||
|
||||
def insert(self, i, item):
|
||||
if isinstance(item, Edge):
|
||||
self.edges.insert(i, item)
|
||||
elif isinstance(item, list) or isinstance(item, EdgeSequence):
|
||||
for j in range(len(item)):
|
||||
self.edges.insert(i + j, item[j])
|
||||
else:
|
||||
raise NotImplementedError(f'{self.__class__.__name__}::ERROR::incerting object of {type(item)} not suported (yet)')
|
||||
return self
|
||||
|
||||
def pop(self, i):
|
||||
if isinstance(i, Edge):
|
||||
i = self.index(i)
|
||||
self.edges.pop(i)
|
||||
return self
|
||||
|
||||
def substitute(self, orig, new):
|
||||
"""Remove orign item from the list and place seq into it's place
|
||||
orig can be either an id of an item to remove
|
||||
or an instance of Edge that exists in the current sequence
|
||||
"""
|
||||
if isinstance(orig, Edge):
|
||||
orig = self.index(orig)
|
||||
if orig < 0:
|
||||
orig = len(self) + orig
|
||||
self.pop(orig)
|
||||
self.insert(orig, new)
|
||||
return self
|
||||
|
||||
def reverse(self):
|
||||
"""Reverse edge sequence in-place"""
|
||||
self.edges.reverse()
|
||||
for edge in self.edges:
|
||||
edge.reverse()
|
||||
return self
|
||||
|
||||
# EdgeSequence-specific
|
||||
def translate_by(self, shift):
|
||||
"""Translate the edge seq vertices s.t. the first vertex is at new_origin
|
||||
"""
|
||||
for v in self.verts():
|
||||
v[0] += shift[0]
|
||||
v[1] += shift[1]
|
||||
return self
|
||||
|
||||
def snap_to(self, new_origin=None):
|
||||
"""Translate the edge seq vertices s.t. the first vertex is at new_origin
|
||||
"""
|
||||
if new_origin is None:
|
||||
new_origin = [0, 0]
|
||||
start = copy(self[0].start)
|
||||
shift = [new_origin[0] - start[0], new_origin[1] - start[1]]
|
||||
self.translate_by(shift)
|
||||
|
||||
return self
|
||||
|
||||
def close_loop(self):
|
||||
"""if edge loop is not closed, add and edge to close it"""
|
||||
self.isChained() # print warning if smth is wrong
|
||||
if not self.isLoop():
|
||||
self.append(Edge(self[-1].end, self[0].start))
|
||||
return self
|
||||
|
||||
def rotate(self, angle):
|
||||
"""Rotate edge sequence by angle in place, using first point as a reference
|
||||
|
||||
Parameters:
|
||||
angle -- desired rotation angle in radians (!)
|
||||
"""
|
||||
curr_start = copy(self[0].start)
|
||||
|
||||
# set the start point to zero
|
||||
self.snap_to([0, 0])
|
||||
rot = R2D(angle)
|
||||
|
||||
for v in self.verts():
|
||||
v[:] = np.matmul(rot, v)
|
||||
|
||||
# recover the original location
|
||||
self.snap_to(curr_start)
|
||||
|
||||
return self
|
||||
|
||||
def extend(self, factor):
|
||||
"""Extend or shrink the edges along the line from start of the first
|
||||
edge to the end of the last edge in sequence. The start of the first
|
||||
edge remains fixed
|
||||
"""
|
||||
# TODOLOW Version With preservation of total length?
|
||||
# TODOLOW Base extention factor on change in total length of edges rather
|
||||
# than on the shortcut length
|
||||
|
||||
# FIXME extending by negative factor should be predictable (e.g. opposite direction of extention)
|
||||
|
||||
# Need to take the target line from the chained order
|
||||
if not self.isChained():
|
||||
chained_edges = self.chained_order()
|
||||
chained_edges.isChained()
|
||||
if chained_edges.isLoop():
|
||||
print(f'{self.__class__.__name__}::WARNING::Extending looped edge sequences is not available')
|
||||
return self
|
||||
else:
|
||||
chained_edges = self
|
||||
|
||||
target_line = np.array(chained_edges[-1].end) - np.array(chained_edges[0].start)
|
||||
target_line = target_line / norm(target_line)
|
||||
|
||||
# gather vertices
|
||||
verts_coords = self.verts()
|
||||
nverts_coords = np.array(verts_coords)
|
||||
|
||||
# adjust their position based on projection to the target line
|
||||
verts_projection = np.empty(nverts_coords.shape)
|
||||
fixed = nverts_coords[0]
|
||||
for i in range(nverts_coords.shape[0]):
|
||||
verts_projection[i] = (nverts_coords[i] - fixed).dot(target_line) * target_line
|
||||
|
||||
new_verts = verts_coords - (1 - factor) * verts_projection
|
||||
|
||||
# Update vertex objects
|
||||
for i in range(len(verts_coords)):
|
||||
verts_coords[i][:] = new_verts[i]
|
||||
|
||||
return self
|
||||
|
||||
def reflect(self, v0, v1):
|
||||
"""Reflect 2D points w.r.t. 1D line defined by two points"""
|
||||
v0, v1 = np.asarray(v0), np.asarray(v1)
|
||||
vec = np.asarray(v1) - np.asarray(v0)
|
||||
vec = vec / norm(vec) # normalize
|
||||
|
||||
# https://demonstrations.wolfram.com/ReflectionMatrixIn2D/#more
|
||||
Ref = np.array([
|
||||
[1 - 2 * vec[1]**2, 2*vec[0]*vec[1]],
|
||||
[2*vec[0]*vec[1], - 1 + 2 * vec[1]**2]
|
||||
])
|
||||
|
||||
# translate -> reflect -> translate back
|
||||
for v in self.verts():
|
||||
v[:] = np.matmul(Ref, np.asarray(v) - v0) + v0
|
||||
|
||||
# Reflect edge features (curvatures, etc.)
|
||||
for e in self.edges:
|
||||
e.reflect_features()
|
||||
|
||||
return self
|
||||
|
||||
def propagate_label(self, label):
|
||||
"""Propagate label to sub-edges
|
||||
NOTE: Recommended to perform after all edge modification
|
||||
operations (stitching, cutting, inserting) were completed
|
||||
Support for edge label propagation through those operations is not (yet) implemented
|
||||
# TODO Edge labels on cuts/reassemble in the
|
||||
"""
|
||||
for e in self.edges:
|
||||
e.label = label
|
||||
|
||||
# ANCHOR New sequences & versions
|
||||
def copy(self):
|
||||
"""Create a copy of a current edge sequence preserving the chaining
|
||||
property of edge sequences"""
|
||||
new_seq = deepcopy(self)
|
||||
|
||||
# deepcopy recreates the vertex objects on both sides of the edges
|
||||
# in changed edges those vertex objects are supposed to be shared
|
||||
# by neighbor edges
|
||||
|
||||
for i in range(1, len(new_seq)):
|
||||
if self[i].start is self[i-1].end:
|
||||
new_seq[i].start = new_seq[i-1].end
|
||||
|
||||
if self.isLoop():
|
||||
new_seq[-1].end = new_seq[0].start
|
||||
|
||||
return new_seq
|
||||
|
||||
def chained_order(self):
|
||||
""" Attempt to restore a chain in the EdgeSequence
|
||||
The chained edge sequence may lose its property if the edges
|
||||
were reversed externally.
|
||||
This routine created a copy of the correct sequence with aligned
|
||||
the order of edges,
|
||||
|
||||
It might be useful for various calculations
|
||||
|
||||
"""
|
||||
chained = self.copy()
|
||||
|
||||
for i in range(len(chained)):
|
||||
# Assuming the previous one is already sorted
|
||||
if i > 0 and chained[i].end is chained[i-1].end:
|
||||
chained[i].reverse()
|
||||
# Not connected to the previous one
|
||||
elif (i + 1 < len(chained)
|
||||
and (chained[i].start is chained[i+1].start or chained[i].start is chained[i+1].end)):
|
||||
chained[i].reverse()
|
||||
# not connected to anything or connected properly -- leave as is
|
||||
|
||||
return chained
|
||||
Reference in New Issue
Block a user