198 lines
8.3 KiB
Python
198 lines
8.3 KiB
Python
import numpy as np
|
|
|
|
from pygarment.garmentcode.interface import Interface
|
|
from pygarment.garmentcode.utils import close_enough
|
|
|
|
|
|
class StitchingRule:
|
|
"""High-level stitching instructions connecting two component interfaces
|
|
"""
|
|
def __init__(self, int1: Interface, int2: Interface,
|
|
verbose: bool = False) -> None:
|
|
"""
|
|
Inputs:
|
|
* int1, int2 -- two interfaces to connect in the stitch
|
|
NOTE: When connecting interfaces with multiple edge count on both
|
|
sides,
|
|
1) Note that the edge sequences may change their structure.
|
|
Involved interfaces and corresponding patterns will be updated
|
|
automatically
|
|
Use of the same interfaces in other stitches (creating 3+way
|
|
stitch edge) may fail.
|
|
2) The interfaces' edges are matched based on the provided order
|
|
in the interface.
|
|
The order can be controlled at the moment of interface creation
|
|
"""
|
|
# TODO Explicitely support 3+way stitches
|
|
self.int1 = int1
|
|
self.int2 = int2
|
|
self.verbose = verbose
|
|
if not self.isMatching():
|
|
self.match_interfaces()
|
|
|
|
if verbose and not close_enough(
|
|
len1 := int1.projecting_lengths().sum(),
|
|
len2 := int2.projecting_lengths().sum(),
|
|
tol=0.3): # NOTE = 3 mm
|
|
print(
|
|
f'{self.__class__.__name__}::WARNING::Projected edges do not match in the stitch: \n'
|
|
f'{len1}: {int1}\n{len2}: {int2}')
|
|
|
|
def isMatching(self, tol=0.05):
|
|
# if both the breakdown and relative partitioning is similar
|
|
|
|
frac1 = self.int1.projecting_fractions()
|
|
frac2 = self.int2.projecting_fractions()
|
|
|
|
return len(self.int1) == len(self.int2) and np.allclose(frac1, frac2, atol=tol)
|
|
|
|
def match_interfaces(self):
|
|
""" Subdivide the interface edges on both sides s.t. they are matching
|
|
and can be safely connected
|
|
(same number of edges on each side and same relative fractions)
|
|
|
|
Serializable format does not natively support t-stitches,
|
|
so the longer edges needs to be broken down into matching segments
|
|
"""
|
|
|
|
# Eval the fractions corresponding to every segment in the interfaces
|
|
# Using projecting edges to match desired gather patterns
|
|
frac1 = self.int1.projecting_fractions()
|
|
frac2 = self.int2.projecting_fractions()
|
|
min_frac = min(min(frac1), min(frac2)) # projection tolerance should not be larger than the smallest fraction
|
|
|
|
self._match_to_fractions(self.int1, frac2, tol=min(1e-2, min_frac / 2))
|
|
|
|
self._match_to_fractions(self.int2, frac1, tol=min(1e-2, min_frac / 2))
|
|
|
|
|
|
def _match_to_fractions(self, inter:Interface, to_add, tol=1e-2):
|
|
"""Add the vertices at given location to the edge sequence in a given
|
|
interface
|
|
|
|
Parameters:
|
|
* inter -- interface to modify
|
|
* to_add -- the faractions of segements to be projected onto the
|
|
edge sequence in the inter
|
|
* tol -- the proximity of vertices when they can be regarded as
|
|
the same vertex.
|
|
NOTE: tol should be shorter than the smallest expected edge
|
|
"""
|
|
|
|
# NOTE Edge sequences to subdivide might be disconnected
|
|
# (even belong to different panels), so we need to subdivide per edge
|
|
|
|
# Go over the edges keeping track of their fractions
|
|
add_id, in_id = 0, 0
|
|
covered_init, covered_added = 0, 0
|
|
curr_fractions = inter.projecting_fractions()
|
|
|
|
while in_id < len(inter.edges) and add_id < len(to_add):
|
|
# projected edges since they represent the stitch sizes
|
|
# NOTE: sometimes overshoots slightly due to error accumulation -> bounding by 1.
|
|
|
|
next_init = min(covered_init + curr_fractions[in_id], 1.)
|
|
next_added = min(covered_added + to_add[add_id], 1.)
|
|
if close_enough(next_init, next_added, tol):
|
|
# the vertex exists, skip
|
|
in_id += 1
|
|
add_id += 1
|
|
covered_init, covered_added = next_init, next_added
|
|
elif next_init < next_added:
|
|
# add on the next step
|
|
in_id += 1
|
|
covered_init = next_init
|
|
else:
|
|
# add a vertex to the edge at the new location
|
|
# Eval on projected edge
|
|
in_frac = curr_fractions[in_id]
|
|
new_v_loc = in_frac - (next_init - next_added)
|
|
split_frac = new_v_loc / in_frac
|
|
base_edge, base_panel = inter.edges[in_id], inter.panel[in_id]
|
|
|
|
# Check edge orientation
|
|
flip = inter.needsFlipping(in_id)
|
|
if flip:
|
|
split_frac = 1 - split_frac
|
|
if self.verbose:
|
|
print(f'{self.__class__.__name__}::INFO::{base_edge} from {base_panel.name} reoriented in interface')
|
|
|
|
# Split the base edge accordingly
|
|
subdiv = base_edge.subdivide_len([split_frac, 1 - split_frac])
|
|
|
|
inter.panel[in_id].edges.substitute(base_edge, subdiv) # Update the panel
|
|
# Always follows the edge order in the panel
|
|
# Swap subdiv order for interface to s.w. the interface sequence remains oriented
|
|
if flip:
|
|
subdiv.edges.reverse()
|
|
|
|
# Update interface accordingly
|
|
inter.substitute(
|
|
base_edge, subdiv, [inter.panel[in_id]
|
|
for _ in range(len(subdiv))])
|
|
|
|
# TODO what if these edges are used in other interfaces? Do they need to be updated as well?
|
|
# next step
|
|
curr_fractions = inter.projecting_fractions()
|
|
covered_init += curr_fractions[in_id]
|
|
covered_added = next_added
|
|
in_id += 1
|
|
add_id += 1
|
|
|
|
if add_id != len(to_add):
|
|
raise RuntimeError(f'{self.__class__.__name__}::ERROR::Projection on {inter.panel_names()} failed')
|
|
|
|
def assembly(self):
|
|
"""Produce a stitch that connects two interfaces
|
|
"""
|
|
if self.verbose and not self.isMatching():
|
|
print(f'{self.__class__.__name__}::WARNING::Stitch sides do not match on assembly!!')
|
|
|
|
stitches = []
|
|
|
|
for i, j in zip(range(len(self.int1.edges)), range(len(self.int2.edges))):
|
|
stitches.append([
|
|
{
|
|
'panel': self.int1.panel[i].name, # corresponds to a name.
|
|
# Only one element of the first level is expected
|
|
'edge': self.int1.edges[i].geometric_id
|
|
},
|
|
{
|
|
'panel': self.int2.panel[j].name,
|
|
'edge': self.int2.edges[j].geometric_id
|
|
}
|
|
])
|
|
|
|
# Swap indication
|
|
# NOTE: Swap is indicated on the interfaces in order to support component
|
|
# incapsulation. Same stitching rule for different participating components may have different
|
|
# fabric side preferences.
|
|
# NOTE: "right_wrong" stitch is used when either of the interfaces request it
|
|
# NOTE: Backward-compatible formulation
|
|
if self.int1.right_wrong[i] or self.int2.right_wrong[j]:
|
|
stitches[-1].append('right_wrong')
|
|
|
|
return stitches
|
|
|
|
|
|
class Stitches:
|
|
"""Describes a collection of StitchingRule objects
|
|
Needed for more compact specification and evaluation of those rules
|
|
"""
|
|
def __init__(self, *rules) -> None:
|
|
"""Rules -- any number of tuples of two interfaces (Interface, Interface) """
|
|
|
|
self.rules = [StitchingRule(int1, int2) for int1, int2 in rules]
|
|
|
|
def append(self, pair): # TODOLOW two parameters explicitely rather then "pair" object?
|
|
self.rules.append(StitchingRule(*pair))
|
|
|
|
def __getitem__(self, id):
|
|
return self.rules[id]
|
|
|
|
def assembly(self):
|
|
stitches = []
|
|
for rule in self.rules:
|
|
stitches += rule.assembly()
|
|
return stitches
|