from copy import copy from numpy.linalg import norm import numpy as np from pygarment.garmentcode.edge import EdgeSequence, Edge from pygarment.garmentcode.utils import close_enough class Interface: """Description of an interface of a panel or component that can be used in stitches as a single unit """ def __init__(self, panel, edges, ruffle=1., right_wrong=False): """ Parameters: * panel - Panel object * edges - Edge or EdgeSequence -- edges in the panel that are allowed to connect to # TODO rename to something more generic/projection related? * ruffle - ruffle coefficient for a particular edge. Interface object will supply projecting_edges() shape s.t. the ruffles with the given rate are created. Default = 1. (no ruffles, smooth connection) * right_wrong -- control of stitch orientation -- indication if this interface's right side of the fabric should be connected to the wrong side of another interface. Default -- False -- connect right side of the fabric to the right side of the faric, sufficient in most cases. """ self.edges = edges if isinstance(edges, EdgeSequence) else EdgeSequence(edges) self.panel = [panel for _ in range(len(self.edges))] # matches every edge self.right_wrong = [right_wrong for _ in range(len(self.edges))] # Allow to enfoce change the direction of edge # (used in many-to-many stitches correspondance determination) self.edges_flipping = [False for _ in range(len(self.edges))] # Ruffles are applied to sections # Since extending a chain of edges != extending each edge individually if isinstance(ruffle, list): assert len(ruffle) == len(edges), "Ruffles and Edges don't match" self.ruffle = [] last_coef = None last_start = 0 for i, coef in enumerate(ruffle): if coef == last_coef or last_coef is None: last_coef = coef # Making sure to overwrite None continue self.ruffle.append(dict(coeff=last_coef, sec=[last_start, i])) last_start, last_coef = i, coef self.ruffle.append(dict(coeff=last_coef, sec=[last_start, len(ruffle)])) else: self.ruffle = [dict(coeff=ruffle, sec=[0, len(self.edges)])] def projecting_edges(self, on_oriented=False) -> EdgeSequence: """Return edges shape that should be used when projecting interface onto another panel NOTE: reflects current state of the edge object. Call this function again if egdes change (e.g. their direction) # FIXME projection only works w.r.t. the line connecting the first and # the last vertex of the edge sequence -> use with cation """ # Per edge set ruffle application projected = self.edges.copy() if not on_oriented else self.oriented_edges() for r in self.ruffle: if not close_enough(r['coeff'], 1, 1e-3): if r['sec'][1] >= len(projected) or not projected[r['sec'][1] - 1:r['sec'][1] + 1].isChained(): projected[r['sec'][0]:r['sec'][1]].extend(1 / r['coeff']) else: # Don't let extention to affect the rest of the sequence # Find the vert that separates the ruffle seqences prev_edge, next_edge = projected[r['sec'][1] - 1], projected[r['sec'][1]] # Common vertex common_v = prev_edge.end if prev_edge.end is next_edge.end or prev_edge.end is next_edge.start else prev_edge.start # Create copy and assign to next edge common_v_copy = common_v.copy() copy_to_end = False if common_v is next_edge.end: next_edge.end = common_v_copy copy_to_end = True else: next_edge.start = common_v_copy # Extend the sequence projected[r['sec'][0]:r['sec'][1]].extend(1 / r['coeff']) # move the next edges s.t. created vertex alignes with the original common vertex projected[r['sec'][1]:].translate_by([common_v[0] - common_v_copy[0], common_v[1] - common_v_copy[1]]) # re-chain the edges if copy_to_end: next_edge.end = common_v else: next_edge.start = common_v return projected # ANCHOR --- Projections for stitching -- to connect edges correctly and create correct ruffles def projecting_lengths(self): """Desired projected length of the interface edges, as specified by ruffle coefficients""" projecting_lengths = [] for r in self.ruffle: if not close_enough(r['coeff'], 1, 1e-3): projecting_lengths += [e.length() / r['coeff'] for e in self.edges[r['sec'][0]:r['sec'][1]]] else: projecting_lengths += [e.length() for e in self.edges[r['sec'][0]:r['sec'][1]]] return np.array(projecting_lengths) def projecting_fractions(self): """Desired projected fractions of the interface edges, as specified by ruffle coefficients Fractions calculated w.r.t. to total projection lengths """ projecting_lengths = self.projecting_lengths() return projecting_lengths / projecting_lengths.sum() def needsFlipping(self, i): """ Check if particular edge (i) should be re-oriented to follow the general direction of the interface * tol -- tolerance in distance differences that triggers flipping (in cm) """ return self.edges_flipping[i] # ANCHOR --- Info ---- def oriented_edges(self): """ Orient the edges withing the interface sequence along the general direction of the interface Creates a copy of the interface s.t. not to disturb the original edge objects """ # NOTE we cannot we do the same for the edge sub-sequences: # - midpoint of a sequence is less representative # - more likely to have weird relative 3D orientations # => heuristic won't work as well oriented = self.edges.copy() for i in range(len(self.edges)): if self.needsFlipping(i): oriented[i].reverse() oriented[i].flipped = True else: oriented[i].flipped = False return oriented def verts_3d(self): """Return 3D locations of all vertices that participate in the interface""" verts_2d = [] matching_panels = [] for e, panel in zip(self.edges, self.panel): if all(e.start is not v for v in verts_2d): # Ensuring uniqueness verts_2d.append(e.start) matching_panels.append(panel) if all(e.end is not v for v in verts_2d): # Ensuring uniqueness verts_2d.append(e.end) matching_panels.append(panel) # To 3D verts_3d = [] for v, panel in zip(verts_2d, matching_panels): verts_3d.append(panel.point_to_3D(v)) return np.asarray(verts_3d) def bbox_3d(self): """Return Interface bounding box""" # NOTE: Vertex repetitions don't matter for bbox evaluation verts_3d = [] for e, panel in zip(self.edges, self.panel): # Using curve linearization for more accurate approximation of bbox lin_edges = e.linearize() verts_2d = lin_edges.verts() verts_3d += [panel.point_to_3D(v) for v in verts_2d] verts_3d = np.asarray(verts_3d) return verts_3d.min(axis=0), verts_3d.max(axis=0) def __len__(self): return len(self.edges) def __str__(self) -> str: return f'Interface: {[p.name for p in self.panel]}: {str(self.oriented_edges())}' def __repr__(self) -> str: return self.__str__() def panel_names(self): return [p.name for p in self.panel] # ANCHOR --- Interface Updates ----- def reverse(self, with_edge_dir_reverse=False): """Reverse the order of edges in the interface (without updating the edge objects) Reversal is useful for reordering interface edges for correct matching in the multi-stitches """ self.edges.edges.reverse() self.panel.reverse() self.edges_flipping.reverse() if with_edge_dir_reverse: self.edges_flipping = [not e for e in self.edges_flipping] enum = len(self.edges) for r in self.ruffle: # Update ids r['sec'][0] = enum - r['sec'][0] r['sec'][1] = enum - r['sec'][1] # Swap r['sec'][0], r['sec'][1] = r['sec'][1], r['sec'][0] return self def flip_edges(self): """Reverse the direction of edges in the interface (without updating the edge objects) Reversal is useful for updating interface edges for correct matching in the multi-stitches """ self.edges_flipping = [not e for e in self.edges_flipping] return self # TODO Edge Sequence Function? def reorder(self, curr_edge_ids, projected_edge_ids): """Change the order of edges from curr_edge_ids to projected_edge_ids in the interface Note that the input should prescrive new ordering for all affected edges e.g. if moving 0 -> 1, specify the new location for 1 as well """ for i, j in zip(curr_edge_ids, projected_edge_ids): for r in self.ruffle: if (i >= r['sec'][0] and i < r['sec'][1] and (j < r['sec'][0] or j >= r['sec'][1])): raise NotImplementedError( f'{self.__class__.__name__}::ERROR::reordering between panel-related sub-segments is not supported') new_edges = EdgeSequence() new_panel_list = [] new_flipping_info = [] new_right_wrong = [] for i in range(len(self.panel)): id = i if i not in curr_edge_ids else projected_edge_ids[curr_edge_ids.index(i)] # edges new_edges.append(self.edges[id]) new_flipping_info.append(self.edges_flipping[id]) # panels new_panel_list.append(self.panel[id]) # connectivity indication new_right_wrong.append(self.right_wrong[id]) self.edges = new_edges self.panel = new_panel_list self.edges_flipping = new_flipping_info self.right_wrong = new_right_wrong def substitute(self, orig, new_edges, new_panels): """Update the interface edges with correct correction of panels * orig -- could be an edge object or the id of edges that need substitution * new_edges -- new edges to insert in place of orig * new_panels -- per-edge panel objects indicating where each of new_edges belong to NOTE: the ruffle indicator for the new_edges is expected to be the same as for orig edge Specifying new indicators is not yet supported """ if isinstance(orig, Edge): orig = self.edges.index(orig) if orig < 0: orig = len(self.edges) + orig self.edges.substitute(orig, new_edges) # Update panels & flip info & right_wrong info self.panel.pop(orig) curr_edges_flip = self.edges_flipping.pop(orig) curr_right_wrong = self.right_wrong.pop(orig) if isinstance(new_panels, list) or isinstance(new_panels, tuple): for j in range(len(new_panels)): self.panel.insert(orig + j, new_panels[j]) # TODOLOW Note propagation of default values. Allow to specify them as func input! self.edges_flipping.insert(orig + j, curr_edges_flip) self.right_wrong.insert(orig + j, curr_right_wrong) else: self.panel.insert(orig, new_panels) self.edges_flipping.insert(orig, curr_edges_flip) self.right_wrong.insert(orig + j, curr_right_wrong) # Propagate ruffle indicators ins_len = 1 if isinstance(new_edges, Edge) else len(new_edges) if ins_len > 1: for it in self.ruffle: # UPD ruffle indicators if it['sec'][0] > orig: it['sec'][0] += ins_len - 1 if it['sec'][1] > orig: it['sec'][1] += ins_len - 1 return self def set_right_wrong(self, right_wrong): """Set all right_wrong values for the edges in current interface to the input value""" self.right_wrong = [right_wrong for _ in range(len(self.edges))] return self # ANCHOR ----- Statics ---- @staticmethod def from_multiple(*ints): """Create interface from other interfaces: * Allows to use different panels in one interface * different ruffle values in one interface # NOTE the relative order of edges is preserved from the original interfaces and the incoming interface sequence This order will then be used in the SrtitchingRule when determing connectivity between interfaces """ new_int = copy(ints[0]) # shallow copy -- don't create unnecessary objects new_int.edges = EdgeSequence() new_int.edges_flipping = [] new_int.panel = [] new_int.ruffle = [] new_int.right_wrong = [] for elem in ints: shift = len(new_int.edges) new_int.ruffle += [copy(r) for r in elem.ruffle] for r in new_int.ruffle[-len(elem.ruffle):]: r.update(sec=[r['sec'][0] + shift, r['sec'][1] + shift]) new_int.edges.append(elem.edges) new_int.panel += elem.panel new_int.right_wrong += elem.right_wrong new_int.edges_flipping += elem.edges_flipping return new_int @staticmethod def _is_order_matching(panel_s, vert_s, panel_1, vert1, panel_2, vert2) -> bool: """Check which of the two vertices vert1 (panel_1) or vert2 (panel_2) is closer to the vert_s from panel_s in 3D""" s_3d = panel_s.point_to_3D(vert_s) v1_3d = panel_1.point_to_3D(vert1) v2_3d = panel_2.point_to_3D(vert2) return norm(v1_3d - s_3d) < norm(v2_3d - s_3d)