Files
design2garmentcode-impl/pygarment/garmentcode/connector.py

198 lines
8.3 KiB
Python
Raw Normal View History

2025-07-03 17:03:00 +08:00
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