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