init_code
This commit is contained in:
0
pygarment/garmentcode/__init__.py
Normal file
0
pygarment/garmentcode/__init__.py
Normal file
141
pygarment/garmentcode/base.py
Normal file
141
pygarment/garmentcode/base.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import numpy as np
|
||||
|
||||
from pygarment.garmentcode.connector import Stitches
|
||||
|
||||
|
||||
class BaseComponent(ABC):
|
||||
"""Basic interface for garment-related components
|
||||
|
||||
NOTE: modifier methods return self object to allow chaining of the
|
||||
operations
|
||||
"""
|
||||
|
||||
def __init__(self, name, verbose=False) -> None:
|
||||
self.name = name
|
||||
self.verbose = verbose
|
||||
|
||||
# List or dictionary of the interfaces of this components
|
||||
# available for connectivity with other components
|
||||
self.interfaces = {}
|
||||
|
||||
# Rules for connecting subcomponents
|
||||
self.stitching_rules = Stitches()
|
||||
|
||||
# Info
|
||||
def pivot_3D(self):
|
||||
"""Pivot location of a component in 3D"""
|
||||
return [0, 0, 0]
|
||||
|
||||
def bbox(self):
|
||||
"""Bounding box -- in 2D"""
|
||||
return np.array([0, 0]), np.array([0, 0])
|
||||
|
||||
def bbox3D(self):
|
||||
"""Bounding box in 3D space"""
|
||||
return np.array([0, 0, 0]), np.array([0, 0, 0])
|
||||
|
||||
def is_self_intersecting(self):
|
||||
"""Check whether the component have self-intersections"""
|
||||
return False
|
||||
|
||||
# Operations
|
||||
@abstractmethod
|
||||
def translate_by(self, delta_translation):
|
||||
return self
|
||||
|
||||
@abstractmethod
|
||||
def translate_to(self, new_translation):
|
||||
"""Set panel translation to be exactly that vector"""
|
||||
return self
|
||||
|
||||
@abstractmethod
|
||||
def rotate_by(self, delta_rotation):
|
||||
return self
|
||||
|
||||
@abstractmethod
|
||||
def rotate_to(self, new_rot):
|
||||
return self
|
||||
|
||||
@abstractmethod
|
||||
def assembly(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
# ----- Placement routines: these are the same for panels and components
|
||||
def place_below(self, comp, gap=2):
|
||||
"""Place below the provided component"""
|
||||
other_bbox = comp.bbox3D()
|
||||
curr_bbox = self.bbox3D()
|
||||
|
||||
self.translate_by([0, other_bbox[0][1] - curr_bbox[1][1] - gap, 0])
|
||||
return self
|
||||
|
||||
def place_by_interface(
|
||||
self,
|
||||
self_interface,
|
||||
out_interface,
|
||||
gap=2,
|
||||
alignment='center',
|
||||
gap_dir=None
|
||||
):
|
||||
"""Adjust the placement of component according to the connectivity
|
||||
instruction
|
||||
|
||||
Alignment options:
|
||||
'center' center of the interface to center of the interface
|
||||
'top' - top on Y axis
|
||||
'bottom' - bottom on Y axis
|
||||
'left' - left on X axis
|
||||
'right' - right on X axis
|
||||
"""
|
||||
|
||||
# Align translation
|
||||
self_bbox = self_interface.bbox_3d()
|
||||
out_bbox = out_interface.bbox_3d()
|
||||
|
||||
# Determine alignment point depending on requested alignment type
|
||||
point_out = (out_bbox[1] + out_bbox[0]) / 2
|
||||
point_self = (self_bbox[1] + self_bbox[0]) / 2
|
||||
if alignment == 'center':
|
||||
pass # No modification needed
|
||||
elif alignment == 'top':
|
||||
point_out[1] = out_bbox[1][1] # Use max in Y
|
||||
point_self[1] = self_bbox[1][1]
|
||||
elif alignment == 'bottom':
|
||||
point_out[1] = out_bbox[0][1] # Use min in Y
|
||||
point_self[1] = self_bbox[0][1]
|
||||
elif alignment == 'right':
|
||||
point_out[0] = out_bbox[0][0] # Use min in X
|
||||
point_self[0] = self_bbox[0][0]
|
||||
elif alignment == 'left':
|
||||
point_out[0] = out_bbox[1][0] # Use max in X
|
||||
point_self[0] = self_bbox[1][0]
|
||||
else:
|
||||
raise ValueError(
|
||||
f'{self.__class__.__name__}::{self.name}::ERROR::'
|
||||
f'Uknown alignment type ({alignment}) requested in place_by_interface().'
|
||||
f' Available types: center, top, bottom, left, right')
|
||||
|
||||
# Add a gap outside the current
|
||||
if gap_dir is None:
|
||||
full_bbox = self.bbox3D()
|
||||
center = (full_bbox[0] + full_bbox[1]) / 2
|
||||
mid_self = (self_bbox[1] + self_bbox[0]) / 2
|
||||
gap_dir = mid_self - center
|
||||
|
||||
gap_dir = gap * gap_dir / np.linalg.norm(gap_dir)
|
||||
diff = point_out - (point_self + gap_dir)
|
||||
|
||||
self.translate_by(diff)
|
||||
|
||||
# NOTE: Norm evaluation of vertex set will fail
|
||||
# for the alignment of 2D panels, where they are likely
|
||||
# to be in one line or in a panel plane instead of
|
||||
# the interface place -- so I'm not using norms for gap estimation
|
||||
|
||||
# TODO Estimate rotation
|
||||
# TODO not just placement by the midpoint of the interfaces?
|
||||
# It created a little overlap when both interfaces are angled a little differently
|
||||
return self
|
||||
|
||||
|
||||
147
pygarment/garmentcode/component.py
Normal file
147
pygarment/garmentcode/component.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import numpy as np
|
||||
from scipy.spatial.transform import Rotation as R
|
||||
|
||||
from pygarment.garmentcode.base import BaseComponent
|
||||
from pygarment.pattern.wrappers import VisPattern
|
||||
|
||||
|
||||
class Component(BaseComponent):
|
||||
"""Garment element (or whole piece) composed of simpler connected garment
|
||||
elements"""
|
||||
|
||||
# TODOLOW Overload copy -- respecting edge sequences -- never had any problems though
|
||||
|
||||
def __init__(self, name) -> None:
|
||||
super().__init__(name)
|
||||
|
||||
self.subs = [] # list of generative subcomponents
|
||||
|
||||
def set_panel_label(self, label: str, overwrite=True):
|
||||
"""Propagate given label to all sub-panels (in subcomponents)"""
|
||||
subs = self._get_subcomponents()
|
||||
for sub in subs:
|
||||
sub.set_panel_label(label, overwrite)
|
||||
|
||||
def pivot_3D(self):
|
||||
"""Pivot of a component as a block
|
||||
|
||||
NOTE: The relation of pivots of sub-blocks needs to be
|
||||
preserved in any placement operations on components
|
||||
"""
|
||||
mins, maxes = self.bbox3D()
|
||||
return np.array(((mins[0] + maxes[0]) / 2, maxes[1],
|
||||
(mins[-1] + maxes[-1]) / 2))
|
||||
|
||||
def length(self):
|
||||
"""Length of a component in cm
|
||||
|
||||
Defaults the to the vertical length of a 3D bounding box
|
||||
* longest_dim -- if set, returns the longest dimention out of the bounding box dimentions
|
||||
"""
|
||||
subs = self._get_subcomponents()
|
||||
return sum([s.length() for s in subs]) if subs else 0
|
||||
|
||||
def translate_by(self, delta_vector):
|
||||
"""Translate component by a vector"""
|
||||
for subs in self._get_subcomponents():
|
||||
subs.translate_by(delta_vector)
|
||||
return self
|
||||
|
||||
def translate_to(self, new_translation):
|
||||
"""Set panel translation to be exactly that vector"""
|
||||
pivot = self.pivot_3D()
|
||||
for subs in self._get_subcomponents():
|
||||
sub_pivot = subs.pivot_3D()
|
||||
subs.translate_to(np.asarray(new_translation) + (sub_pivot - pivot))
|
||||
return self
|
||||
|
||||
def rotate_by(self, delta_rotation: R):
|
||||
"""Rotate component by a given rotation"""
|
||||
pivot = self.pivot_3D()
|
||||
for subs in self._get_subcomponents():
|
||||
# With preserving relationships between components
|
||||
rel = subs.pivot_3D() - pivot
|
||||
rel_rotated = delta_rotation.apply(rel)
|
||||
subs.rotate_by(delta_rotation)
|
||||
subs.translate_by(rel_rotated - rel)
|
||||
return self
|
||||
|
||||
def rotate_to(self, new_rot):
|
||||
# TODOLOW Implement with correct preservation of relative placement
|
||||
# of subcomponents
|
||||
raise NotImplementedError(
|
||||
f'Component::ERROR::rotate_to is not supported on component level.'
|
||||
'Use relative <rotate_by()> method instead')
|
||||
|
||||
def mirror(self, axis=[0, 1]):
|
||||
"""Swap this component with its mirror image by recursively mirroring
|
||||
subcomponents
|
||||
|
||||
Axis specifies 2D axis to swap around: Y axis by default
|
||||
"""
|
||||
for subs in self._get_subcomponents():
|
||||
subs.mirror(axis)
|
||||
return self
|
||||
|
||||
def assembly(self):
|
||||
"""Construction process of the garment component
|
||||
|
||||
get serializable representation
|
||||
Returns: simulator friendly description of component sewing pattern
|
||||
"""
|
||||
spattern = VisPattern()
|
||||
spattern.name = self.name
|
||||
|
||||
subs = self._get_subcomponents()
|
||||
if not subs:
|
||||
return spattern
|
||||
|
||||
# Simple merge of subcomponent representations
|
||||
for sub in subs:
|
||||
sub_raw = sub.assembly().pattern
|
||||
|
||||
# simple merge of panels
|
||||
spattern.pattern['panels'] = {**spattern.pattern['panels'],
|
||||
**sub_raw['panels']}
|
||||
|
||||
# of stitches
|
||||
spattern.pattern['stitches'] += sub_raw['stitches']
|
||||
|
||||
spattern.pattern['stitches'] += self.stitching_rules.assembly()
|
||||
return spattern
|
||||
|
||||
def bbox3D(self):
|
||||
"""Evaluate 3D bounding box of the current component"""
|
||||
|
||||
subs = self._get_subcomponents()
|
||||
bboxes = [s.bbox3D() for s in subs]
|
||||
|
||||
if not len(subs):
|
||||
# Special components without panel geometry -- no bbox defined
|
||||
return np.array([[np.inf, np.inf, np.inf], [-np.inf, -np.inf, -np.inf]])
|
||||
|
||||
mins = np.vstack([b[0] for b in bboxes])
|
||||
maxes = np.vstack([b[1] for b in bboxes])
|
||||
|
||||
return mins.min(axis=0), maxes.max(axis=0)
|
||||
|
||||
def is_self_intersecting(self):
|
||||
"""Check whether the component have self-intersections on panel level"""
|
||||
|
||||
for s in self._get_subcomponents():
|
||||
if s.is_self_intersecting():
|
||||
return True
|
||||
return False
|
||||
|
||||
# Subcomponents
|
||||
def _get_subcomponents(self):
|
||||
"""Unique set of subcomponents defined in the `self.subs` list or as
|
||||
attributes of the object"""
|
||||
|
||||
all_attrs = [getattr(self, name)
|
||||
for name in dir(self)
|
||||
if name[:2] != '__' and name[-2:] != '__']
|
||||
return list(set([att
|
||||
for att in all_attrs
|
||||
if isinstance(att, BaseComponent)] + self.subs))
|
||||
|
||||
197
pygarment/garmentcode/connector.py
Normal file
197
pygarment/garmentcode/connector.py
Normal file
@@ -0,0 +1,197 @@
|
||||
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
|
||||
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
|
||||
499
pygarment/garmentcode/edge_factory.py
Normal file
499
pygarment/garmentcode/edge_factory.py
Normal file
@@ -0,0 +1,499 @@
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
import svgpathtools as svgpath
|
||||
from scipy.optimize import minimize
|
||||
|
||||
from pygarment.garmentcode.edge import EdgeSequence, Edge, CurveEdge
|
||||
from pygarment.garmentcode.edge import CircleEdge
|
||||
from pygarment.garmentcode.utils import vector_angle
|
||||
from pygarment.garmentcode.utils import bbox_paths
|
||||
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
|
||||
|
||||
|
||||
class EdgeFactory:
|
||||
@staticmethod
|
||||
def from_svg_curve(seg):
|
||||
"""Create Edge/CurveEdge/CircleEdge object from svgpath object
|
||||
Type is determined by svgpath type
|
||||
"""
|
||||
|
||||
start, end = c_to_list(seg.start), c_to_list(seg.end)
|
||||
if isinstance(seg, svgpath.Line):
|
||||
return Edge(start, end)
|
||||
if isinstance(seg, svgpath.Arc):
|
||||
# NOTE: assuming circular arc (same radius in both directoins)
|
||||
radius = seg.radius.real
|
||||
return CircleEdgeFactory.from_points_radius(
|
||||
start, end, radius, seg.large_arc, seg.sweep
|
||||
)
|
||||
|
||||
# Only Bezier left
|
||||
if isinstance(seg, svgpath.QuadraticBezier):
|
||||
cp = [c_to_list(seg.control)]
|
||||
elif isinstance(seg, svgpath.CubicBezier):
|
||||
cp = [c_to_list(seg.control1), c_to_list(seg.control2)]
|
||||
else:
|
||||
raise NotImplementedError(f'CurveEdge::ERROR::Incorrect curve type supplied {seg.type}')
|
||||
|
||||
return CurveEdge(start, end, cp, relative=False)
|
||||
|
||||
class CircleEdgeFactory:
|
||||
@staticmethod
|
||||
def from_points_angle(start, end, arc_angle, right=True):
|
||||
"""Construct circle arc from two fixed points and an angle
|
||||
|
||||
arc_angle:
|
||||
|
||||
NOTE: Might fail on angles close to 2pi
|
||||
"""
|
||||
# Big or small arc
|
||||
if arc_angle > np.pi:
|
||||
arc_angle = 2 * np.pi - arc_angle
|
||||
to_sum = True
|
||||
else:
|
||||
to_sum = False
|
||||
|
||||
radius = 1 / np.sin(arc_angle / 2) / 2
|
||||
h = 1 / np.tan(arc_angle / 2) / 2
|
||||
|
||||
control_y = radius + h if to_sum else radius - h # relative control point
|
||||
control_y *= -1 if right else 1
|
||||
|
||||
return CircleEdge(start, end, cy=control_y)
|
||||
|
||||
@staticmethod
|
||||
def from_points_radius(start, end, radius, large_arc=False, right=True):
|
||||
"""Construct circle arc relative representation
|
||||
from two fixed points and an (absolute) radius
|
||||
"""
|
||||
# Find circle center
|
||||
str_dist = norm(np.asarray(end) - np.asarray(start))
|
||||
|
||||
# NOTE: close enough values may give negative
|
||||
# value under sqrt due to numerical errors
|
||||
if close_enough(radius ** 2, str_dist ** 2 / 4, 1e-3):
|
||||
center_r = 0.
|
||||
else:
|
||||
center_r = np.sqrt(radius ** 2 - str_dist ** 2 / 4)
|
||||
|
||||
# Find the absolute value of Y
|
||||
control_y = radius + center_r if large_arc else radius - center_r
|
||||
|
||||
# Convert to relative
|
||||
control_y = control_y / str_dist
|
||||
|
||||
# Flip sight according to "right" parameter
|
||||
control_y *= -1 if right else 1
|
||||
|
||||
return CircleEdge(start, end, cy=control_y)
|
||||
|
||||
@staticmethod
|
||||
def from_rad_length(rad, length, right=True, start=None):
|
||||
"""NOTE: if start vertex is not provided, both vertices will be created
|
||||
to match desired radius and length
|
||||
"""
|
||||
max_len = 2 * np.pi * rad
|
||||
|
||||
if length > max_len:
|
||||
raise ValueError(
|
||||
f'CircleEdge::ERROR::Incorrect length for specified radius')
|
||||
|
||||
large_arc = length > max_len / 2
|
||||
if large_arc:
|
||||
length = max_len - length
|
||||
|
||||
w_half = rad * np.sin(length / rad / 2)
|
||||
|
||||
edge = CircleEdgeFactory.from_points_radius(
|
||||
[-w_half, 0], [w_half, 0],
|
||||
radius=rad,
|
||||
large_arc=large_arc,
|
||||
right=right
|
||||
)
|
||||
|
||||
if start:
|
||||
edge.snap_to(start)
|
||||
edge.start = start
|
||||
|
||||
return edge
|
||||
|
||||
@staticmethod
|
||||
def from_three_points(start, end, point_on_arc, relative=False):
|
||||
"""Create a circle arc from 3 points (start, end and any point on an arc)
|
||||
|
||||
NOTE: Control point specified in the same coord system as start and end
|
||||
NOTE: points should not be on the same line
|
||||
"""
|
||||
if relative:
|
||||
point_on_arc = rel_to_abs_2d(start, end, point_on_arc)
|
||||
|
||||
nstart, nend, npoint_on_arc = np.asarray(start), np.asarray(
|
||||
end), np.asarray(point_on_arc)
|
||||
|
||||
# https://stackoverflow.com/a/28910804
|
||||
# Using complex numbers to calculate the center & radius
|
||||
x, y, z = list_to_c([start, point_on_arc, end])
|
||||
w = z - x
|
||||
w /= y - x
|
||||
c = (x - y) * (w - abs(w) ** 2) / 2j / w.imag - x
|
||||
# NOTE center = [c.real, c.imag]
|
||||
rad = abs(c + x)
|
||||
|
||||
# Large/small arc
|
||||
mid_dist = norm(npoint_on_arc - ((nstart + nend) / 2))
|
||||
|
||||
# Orientation
|
||||
angle = vector_angle(npoint_on_arc - nstart, nend - nstart) # +/-
|
||||
|
||||
return CircleEdgeFactory.from_points_radius(
|
||||
start, end, radius=rad,
|
||||
large_arc=mid_dist > rad, right=angle > 0)
|
||||
|
||||
class CurveEdgeFactory:
|
||||
@staticmethod
|
||||
def curve_3_points(start, end, target, verbose=False):
|
||||
"""Create (Quadratic) curve edge between start and end that
|
||||
passes through the target point
|
||||
"""
|
||||
rel_target = abs_to_rel_2d(start, end, target)
|
||||
|
||||
if rel_target[0] > 1 or rel_target[0] < 0:
|
||||
raise NotImplementedError(
|
||||
"CurveEdgeFactory::Curve_by_3_points::ERROR::requested target point's projection "
|
||||
"is outside of the base edge, which is not yet supported"
|
||||
)
|
||||
|
||||
# Initialization with a target point as control point
|
||||
# Ensures very smooth, minimal solution
|
||||
out = minimize(
|
||||
_fit_pass_point,
|
||||
rel_target,
|
||||
args=(rel_target)
|
||||
)
|
||||
|
||||
if not out.success:
|
||||
if verbose:
|
||||
print('Curve From Extreme::WARNING::Optimization not successful')
|
||||
print(out)
|
||||
|
||||
cp = out.x.tolist()
|
||||
|
||||
return CurveEdge(start, end, control_points=[cp], relative=True)
|
||||
|
||||
@staticmethod
|
||||
def curve_from_tangents(start, end, target_tan0=None, target_tan1=None,
|
||||
initial_guess=None, verbose=False):
|
||||
"""Create Quadratic Bezier curve connecting given points with the target tangents
|
||||
(both or any of the two can be specified)
|
||||
|
||||
NOTE: Target tangent vectors are automatically normalized
|
||||
"""
|
||||
|
||||
if target_tan0 is not None:
|
||||
target_tan0 = abs_to_rel_2d(start, end, target_tan0, as_vector=True)
|
||||
target_tan0 /= norm(target_tan0)
|
||||
|
||||
if target_tan1 is not None:
|
||||
target_tan1 = abs_to_rel_2d(start, end, target_tan1, as_vector=True)
|
||||
target_tan1 /= norm(target_tan1)
|
||||
|
||||
# Initialization with a target point as control point
|
||||
# Ensures very smooth, minimal solution
|
||||
out = minimize(
|
||||
_fit_tangents,
|
||||
[0.5, 0] if initial_guess is None else initial_guess,
|
||||
args=(target_tan0, target_tan1)
|
||||
)
|
||||
|
||||
if not out.success:
|
||||
print('CurveEdgeFactory::Curve From Tangents::WARNING::Optimization not successful')
|
||||
if verbose:
|
||||
print(out)
|
||||
|
||||
cp = out.x.tolist()
|
||||
|
||||
return CurveEdge(start, end, control_points=[cp], relative=True)
|
||||
|
||||
class EdgeSeqFactory:
|
||||
"""Create EdgeSequence objects for some common edge sequence patterns
|
||||
"""
|
||||
@staticmethod
|
||||
def from_svg_path(path: svgpath.Path, dist_tol=0.05, verbose=False):
|
||||
"""Convert SVG path given as svgpathtool Path object to an EdgeSequence
|
||||
|
||||
* dist_tol: tolerance for vertex closeness to be considered the same
|
||||
vertex
|
||||
NOTE: Assumes that the path can be chained
|
||||
"""
|
||||
# Convert as is
|
||||
edges = []
|
||||
for seg in path._segments:
|
||||
# skip segments of length zero
|
||||
if close_enough(seg.length(), tol=dist_tol):
|
||||
if verbose:
|
||||
print('Skipped: ', seg)
|
||||
continue
|
||||
edges.append(EdgeFactory.from_svg_curve(seg))
|
||||
|
||||
# Chain the edges
|
||||
if len(edges) > 1:
|
||||
for i in range(1, len(edges)):
|
||||
|
||||
if not all(close_enough(s, e, tol=dist_tol)
|
||||
for s, e in zip(edges[i].start, edges[i - 1].end)):
|
||||
raise ValueError(
|
||||
'EdgeSequence::from_svg_path::input path is not chained')
|
||||
|
||||
edges[i].start = edges[i - 1].end
|
||||
return EdgeSequence(*edges, verbose=verbose)
|
||||
|
||||
@staticmethod
|
||||
def from_verts(*verts, loop=False):
|
||||
"""Generate sequence of straight edges from given vertices. If loop==True,
|
||||
the method also closes the edge sequence as a loop
|
||||
"""
|
||||
seq = EdgeSequence(Edge(verts[0], verts[1]))
|
||||
for i in range(2, len(verts)):
|
||||
seq.append(Edge(seq[-1].end, verts[i]))
|
||||
|
||||
if loop:
|
||||
seq.append(Edge(seq[-1].end, seq[0].start))
|
||||
|
||||
seq.isChained() # print warning if smth is wrong
|
||||
return seq
|
||||
|
||||
@staticmethod
|
||||
def from_fractions(start, end, frac=None):
|
||||
"""A sequence of edges between start and end wich lengths are distributed
|
||||
as specified in frac list
|
||||
Parameters:
|
||||
* frac -- list of legth fractions. Every entry is in (0, 1],
|
||||
all entries sums up to 1
|
||||
"""
|
||||
frac = [abs(f) for f in frac]
|
||||
if not close_enough(fsum := sum(frac), 1, 1e-4):
|
||||
raise RuntimeError(f'EdgeSequence::ERROR::fraction is incorrect. The sum {fsum} is not 1')
|
||||
|
||||
vec = np.asarray(end) - np.asarray(start)
|
||||
verts = [start]
|
||||
for i in range(len(frac) - 1):
|
||||
verts.append(
|
||||
[verts[-1][0] + frac[i]*vec[0],
|
||||
verts[-1][1] + frac[i]*vec[1]]
|
||||
)
|
||||
verts.append(end)
|
||||
|
||||
return EdgeSeqFactory.from_verts(*verts)
|
||||
|
||||
@staticmethod
|
||||
def side_with_cut(start=(0, 0), end=(1, 0), start_cut=0, end_cut=0):
|
||||
""" Edge with internal vertices that allows to stitch only part of the border represented
|
||||
by the long side edge
|
||||
|
||||
start_cut and end_cut specify the fraction of the edge to to add extra vertices at
|
||||
"""
|
||||
nstart, nend = np.array(start), np.array(end)
|
||||
verts = [start]
|
||||
|
||||
if start_cut > 0:
|
||||
verts.append((start + start_cut * (nend-nstart)).tolist())
|
||||
if end_cut > 0:
|
||||
verts.append((end - end_cut * (nend-nstart)).tolist())
|
||||
verts.append(end)
|
||||
|
||||
edges = EdgeSeqFactory.from_verts(*verts)
|
||||
|
||||
return edges
|
||||
|
||||
# ------ Darts ------
|
||||
@staticmethod
|
||||
def dart_shape(width, side_len=None, depth=None):
|
||||
"""Shape of simple triangular dart:
|
||||
specified by desired width and either the dart side length or depth
|
||||
"""
|
||||
|
||||
if side_len is None and depth is None:
|
||||
raise ValueError(
|
||||
'EdgeFactory::ERROR::dart shape is not fully specified.'
|
||||
' Add dart side length or dart perpendicular'
|
||||
)
|
||||
|
||||
if depth is None:
|
||||
if width / 2 > side_len:
|
||||
raise ValueError(
|
||||
f'EdgeFactory::ERROR::Requested dart shape (w={width}, side={side_len}) '
|
||||
'does not form a valid triangle')
|
||||
depth = np.sqrt((side_len**2 - (width / 2)**2))
|
||||
|
||||
return EdgeSeqFactory.from_verts([0, 0], [width / 2, -depth], [width, 0])
|
||||
|
||||
# --- SVG ----
|
||||
@staticmethod
|
||||
def halfs_from_svg(svg_filepath, target_height=None):
|
||||
"""Load a shape from an SVG and split it in half (vertically)
|
||||
|
||||
* target_height -- scales the shape s.t. it's height matches the given
|
||||
number
|
||||
|
||||
Shapes restrictions:
|
||||
1) every path in the provided SVG is assumed to form a closed loop
|
||||
that has exactly 2 intersection points with a vertical line
|
||||
passing though the middle of the shape
|
||||
2) The paths should not be nested (inside each other) or intersect
|
||||
as to not create disconnected pieces of the edge when used in
|
||||
shape projection
|
||||
"""
|
||||
paths, _ = svgpath.svg2paths(svg_filepath)
|
||||
|
||||
# Scaling
|
||||
if target_height is not None:
|
||||
bbox = bbox_paths(paths)
|
||||
scale = target_height / (bbox[-1] - bbox[-2])
|
||||
paths = [p.scaled(scale) for p in paths]
|
||||
|
||||
# Get the half-shapes
|
||||
left, right = split_half_svg_paths(paths)
|
||||
|
||||
# Turn into Edge Sequences
|
||||
left_seqs = [EdgeSeqFactory.from_svg_path(p) for p in left]
|
||||
right_seqs = [EdgeSeqFactory.from_svg_path(p) for p in right]
|
||||
|
||||
# In SVG OY is looking downward, we are using OY looking upward
|
||||
# Flip the shape to align
|
||||
bbox = bbox_paths(paths)
|
||||
center_y = (bbox[2] + bbox[3]) / 2
|
||||
left_seqs = [p.reflect([bbox[0], center_y],
|
||||
[bbox[1], center_y]) for p in left_seqs]
|
||||
right_seqs = [p.reflect([bbox[0], center_y],
|
||||
[bbox[1], center_y]) for p in right_seqs]
|
||||
|
||||
# Edge orientation s.t. the shortcut directions align with OY
|
||||
# It preserves the correct relative placement of the shapes later
|
||||
for p in left_seqs:
|
||||
if (p.shortcut()[1][1] - p.shortcut()[0][1]) < 0:
|
||||
p.reverse()
|
||||
for p in right_seqs:
|
||||
if (p.shortcut()[1][1] - p.shortcut()[0][1]) < 0:
|
||||
p.reverse()
|
||||
|
||||
return left_seqs, right_seqs
|
||||
|
||||
# --- For Curves ---
|
||||
def _fit_pass_point(cp, target_location):
|
||||
""" Fit the control point of basic [[0, 0] -> [1, 0]] Quadratic Bezier s.t.
|
||||
it passes through the target location.
|
||||
|
||||
* cp - initial guess for Quadratic Bezier control point coordinates
|
||||
(relative to the edge)
|
||||
* target_location -- target to fit extremum to --
|
||||
expressed in RELATIVE coordinates to your desired edge
|
||||
"""
|
||||
control_bezier = np.array([
|
||||
[0, 0],
|
||||
cp,
|
||||
[1, 0]
|
||||
])
|
||||
params = list_to_c(control_bezier)
|
||||
curve = svgpath.QuadraticBezier(*params)
|
||||
|
||||
inter_segment = svgpath.Line(
|
||||
target_location[0] + 1j * target_location[1] * 2,
|
||||
target_location[0] + 1j * (- target_location[1] * 2)
|
||||
)
|
||||
|
||||
intersect_t = curve.intersect(inter_segment)
|
||||
point = curve.point(intersect_t[0][0])
|
||||
|
||||
diff = abs(point - list_to_c(target_location))
|
||||
|
||||
return diff**2
|
||||
|
||||
|
||||
def _fit_tangents(cp, target_tangent_start, target_tangent_end, reg_strength=0.01):
|
||||
""" Fit the control point of basic [[0, 0] -> [1, 0]] Quadratic Bezier s.t.
|
||||
it's expremum is close to target location.
|
||||
|
||||
* cp - initial guess for Quadratic Bezier control point coordinates
|
||||
(relative to the edge)
|
||||
* target_location -- target to fit extremum to --
|
||||
expressed in RELATIVE coordinates to your desired edge
|
||||
"""
|
||||
control_bezier = np.array([
|
||||
[0, 0],
|
||||
cp,
|
||||
[1, 0]
|
||||
])
|
||||
params = list_to_c(control_bezier)
|
||||
curve = svgpath.QuadraticBezier(*params)
|
||||
|
||||
fin = 0
|
||||
if target_tangent_start is not None:
|
||||
# NOTE: tangents seems to use opposite left/right convention
|
||||
target0 = target_tangent_start[0] + 1j*target_tangent_start[1]
|
||||
fin += (abs(curve.unit_tangent(0) - target0))**2
|
||||
|
||||
if target_tangent_end is not None:
|
||||
target1 = target_tangent_end[0] + 1j*target_tangent_end[1]
|
||||
fin += (abs(curve.unit_tangent(1) - target1))**2
|
||||
|
||||
# NOTE: Tried _max_curvature() and Y value regularizaton,
|
||||
# but it seems like they are not needed
|
||||
return fin
|
||||
|
||||
|
||||
# ---- For SVG Loading ----
|
||||
|
||||
def split_half_svg_paths(paths):
|
||||
"""Sepate SVG paths in half over the vertical line -- for insertion into an
|
||||
edge side
|
||||
|
||||
Paths shapes restrictions:
|
||||
1) every path in the provided list is assumed to form a closed loop
|
||||
that has
|
||||
exactly 2 intersection points with a vetrical line passing though the
|
||||
middle of the shape
|
||||
2) The paths geometry should not be nested
|
||||
as to not create disconnected pieces of the edge when used in
|
||||
shape projection
|
||||
|
||||
"""
|
||||
# Shape Bbox
|
||||
bbox = bbox_paths(paths)
|
||||
center_x = (bbox[0] + bbox[1]) / 2
|
||||
|
||||
# Mid-Intersection
|
||||
inter_segment = svgpath.Line(
|
||||
center_x + 1j * bbox[2],
|
||||
center_x + 1j * bbox[3]
|
||||
)
|
||||
|
||||
right, left = [], []
|
||||
for p in paths:
|
||||
# Intersect points
|
||||
intersect_t = p.intersect(inter_segment)
|
||||
|
||||
if len(intersect_t) != 2:
|
||||
raise ValueError(f'SplitSVGHole::ERROR::Each Provided Svg path should cross vertical like exactly 2 times')
|
||||
|
||||
# Split
|
||||
from_T, to_T = intersect_t[0][0][0], intersect_t[1][0][0]
|
||||
if to_T < from_T:
|
||||
from_T, to_T = to_T, from_T
|
||||
|
||||
side_1 = p.cropped(from_T, to_T)
|
||||
# This order should preserve continuity
|
||||
side_2 = svgpath.Path(
|
||||
*p.cropped(to_T, 1)._segments,
|
||||
*p.cropped(0, from_T)._segments)
|
||||
|
||||
# Collect correctly
|
||||
if side_1.bbox()[2] > center_x:
|
||||
side_1, side_2 = side_2, side_1
|
||||
|
||||
right.append(side_2)
|
||||
left.append(side_1)
|
||||
|
||||
return left, right
|
||||
365
pygarment/garmentcode/interface.py
Normal file
365
pygarment/garmentcode/interface.py
Normal file
@@ -0,0 +1,365 @@
|
||||
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)
|
||||
606
pygarment/garmentcode/operators.py
Normal file
606
pygarment/garmentcode/operators.py
Normal file
@@ -0,0 +1,606 @@
|
||||
"""Shortcuts for common operations on panels and components"""
|
||||
from copy import deepcopy, copy
|
||||
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
from scipy.spatial.transform import Rotation as R
|
||||
from scipy.optimize import minimize
|
||||
import svgpathtools as svgpath
|
||||
|
||||
from pygarment.garmentcode.edge import Edge, CurveEdge, EdgeSequence, ILENGTH_S_TOL
|
||||
from pygarment.garmentcode.interface import Interface
|
||||
from pygarment.garmentcode.utils import vector_angle, close_enough, c_to_list, c_to_np
|
||||
from pygarment.garmentcode.utils import list_to_c
|
||||
from pygarment.garmentcode.base import BaseComponent
|
||||
|
||||
|
||||
# ANCHOR ----- Edge Sequences Modifiers ----
|
||||
def cut_corner(target_shape: EdgeSequence, target_interface: Interface,
|
||||
verbose: bool = False):
|
||||
""" Cut the corner made of edges 1 and 2 following the shape of target_shape
|
||||
This routine updated the panel geometry and interfaces appropriately
|
||||
|
||||
Parameters:
|
||||
* 'target_shape' is an EdgeSequence that is expected to contain one
|
||||
Edge or sequence of chained Edges
|
||||
(next one starts from the end vertex of the one before)
|
||||
# NOTE: 'target_shape' might be scaled (along the main direction)
|
||||
to fit the corner size
|
||||
* Panel to modify
|
||||
* target_interface -- the chained pairs of edges that form the corner
|
||||
to cut, s.t. the end vertex of eid1 is at the corner
|
||||
# NOTE: Onto edges are expected to be straight lines for simplicity
|
||||
|
||||
# NOTE There might be slight computational errors in the resulting
|
||||
shape, that are more pronounced on svg visualizations due to
|
||||
scaling and rasterization
|
||||
|
||||
Side-Effects:
|
||||
* Modified the panel shape to insert new edges
|
||||
* Adds new interface object corresponding to new edges to the
|
||||
panel interface list
|
||||
|
||||
Returns:
|
||||
* Newly inserted edges
|
||||
* New interface object corresponding to new edges
|
||||
"""
|
||||
# TODO Support any number of edges in the target corner edges
|
||||
|
||||
# ---- Evaluate optimal projection of the target shape onto the corner
|
||||
corner_shape = target_shape.copy()
|
||||
panel = target_interface.panel[0] # TODO Support multiple panels???
|
||||
target_edges = target_interface.edges
|
||||
|
||||
# Get rid of directions by working on vertices
|
||||
if target_edges[0].start is target_edges[-1].end:
|
||||
# Orginal edges have beed reversed in normalization or smth
|
||||
target_edges.edges.reverse() # UPD the order
|
||||
|
||||
if corner_shape[0].start is corner_shape[-1].end:
|
||||
# Orginal edges have beed reversed in normalization or smth
|
||||
corner_shape.edges.reverse() # UPD the order
|
||||
|
||||
if corner_shape[0].start[1] > corner_shape[-1].end[1]:
|
||||
# now corner shape is oriented the same way as vertices
|
||||
corner_shape.reverse()
|
||||
corner_shape.snap_to([0,0])
|
||||
|
||||
shortcut = corner_shape.shortcut()
|
||||
|
||||
# Curves (can be defined outside)
|
||||
curve1 = target_edges[0].as_curve()
|
||||
curve2 = target_edges[1].as_curve()
|
||||
|
||||
# align order with the projecting shape, s.t.
|
||||
# curve2 is always the lower one
|
||||
swaped = False
|
||||
if target_edges[0].start[1] > target_edges[-1].end[1]:
|
||||
curve1, curve2 = curve2, curve1
|
||||
swaped = True
|
||||
# NOW curve1 is lower then curve2
|
||||
|
||||
# ----- FIND OPTIMAL PLACE -----
|
||||
start = [0.5, 0.5]
|
||||
out = minimize(
|
||||
_fit_location_corner, start,
|
||||
args=(shortcut[1] - shortcut[0], curve1, curve2),
|
||||
bounds=[(0, 1), (0, 1)])
|
||||
|
||||
if verbose and not out.success:
|
||||
print(f'Cut_corner::ERROR::finding the projection (translation) is unsuccessful. Likely an error in edges choice')
|
||||
print(out)
|
||||
|
||||
if verbose and not close_enough(out.fun):
|
||||
print(f'Cut_corner::WARNING::projection on {target_interface} finished with fun={out.fun}')
|
||||
print(out)
|
||||
|
||||
loc = out.x
|
||||
point1 = c_to_list(curve1.point(loc[0]))
|
||||
# re-align corner_shape with found shifts
|
||||
corner_shape.snap_to(point1)
|
||||
|
||||
# ----- UPD panel ----
|
||||
# Complete to the full corner -- connect with the initial vertices
|
||||
if swaped:
|
||||
# The edges are aligned as v2 -> vc -> v1
|
||||
corner_shape.reverse()
|
||||
loc[0], loc[1] = loc[1], loc[0]
|
||||
|
||||
# Insert a new shape
|
||||
cut_edge1, _ = target_edges[0].subdivide_param([loc[0], 1-loc[0]])
|
||||
_, cut_edge2 = target_edges[1].subdivide_param([loc[1], 1-loc[1]])
|
||||
|
||||
cut_edge1.end = corner_shape[0].start # Connect with new insert
|
||||
cut_edge2.start = corner_shape[-1].end
|
||||
|
||||
corner_shape.insert(0, cut_edge1)
|
||||
corner_shape.append(cut_edge2)
|
||||
|
||||
# Substitute edges in the panel definition
|
||||
panel.edges.pop(target_edges[0])
|
||||
panel.edges.substitute(target_edges[1], corner_shape)
|
||||
|
||||
# Update interface definitions
|
||||
target_edges = EdgeSequence(target_edges.edges) # keep the same edge references,
|
||||
# but not the same edge sequence reference
|
||||
# In case it matches one of the interfaces (we don't want target edges to be overriden)
|
||||
iter = panel.interfaces if isinstance(panel.interfaces, list) else panel.interfaces.values()
|
||||
for intr in iter:
|
||||
# Substitute old edges with what's left from them after cutting
|
||||
if target_edges[0] in intr.edges:
|
||||
intr.edges.substitute(target_edges[0], corner_shape[0])
|
||||
if target_edges[1] in intr.edges:
|
||||
intr.edges.substitute(target_edges[1], corner_shape[-1])
|
||||
|
||||
# Add new interface corresponding to the introduced cut
|
||||
new_int = Interface(panel, corner_shape[1:-1])
|
||||
if isinstance(panel.interfaces, list):
|
||||
panel.interfaces.append(new_int)
|
||||
else:
|
||||
panel.interfaces[f'int_{len(panel.interfaces)}'] = new_int
|
||||
|
||||
return corner_shape[1:-1], new_int
|
||||
|
||||
|
||||
def cut_into_edge(target_shape, base_edge:Edge, offset=0, right=True,
|
||||
flip_target=False, tol=1e-2):
|
||||
""" Insert edges of the target_shape into the given base_edge, starting
|
||||
from offset edges in target shape are rotated s.t. start -> end
|
||||
vertex vector is aligned with the edge
|
||||
|
||||
NOTE: Supports making multiple cuts in one go maintaining the relative
|
||||
distances between cuts
|
||||
provided that
|
||||
* they are all specified in the same coordinate system
|
||||
* (for now) the openings (shortcuts) of each cut are aligned with
|
||||
OY direction
|
||||
|
||||
Parameters:
|
||||
* target_shape -- list of single edge, chained edges, or multiple
|
||||
chaind EdgeSequences to be inserted in the edge.
|
||||
* base_edge -- edge object, defining the border
|
||||
* Offset -- position of the center of the target shape along the edge.
|
||||
* right -- which direction the cut should be oriented w.r.t. the
|
||||
direction of base edge
|
||||
* flip_target -- reflect the shape w.r.t its central perpendicular
|
||||
(default=False, no action taken)
|
||||
|
||||
Returns:
|
||||
* Newly created edges that accomodate the cut
|
||||
* Edges corresponding to the target shape
|
||||
* Edges that lie on the original base edge
|
||||
"""
|
||||
|
||||
# TODO Not only for Y-aligned shapes
|
||||
# TODOLOW Add a parameter: Align target_shape by center or from the start of the offset
|
||||
# NOTE: the optimization routine might be different for the two options
|
||||
|
||||
if isinstance(target_shape, EdgeSequence):
|
||||
return cut_into_edge_single(
|
||||
target_shape, base_edge, offset, right, tol)
|
||||
|
||||
# center of the shape
|
||||
shortcuts = np.asarray([e.shortcut() for e in target_shape])
|
||||
median_y = (shortcuts[:, :, 1].max() + shortcuts[:, :, 1].min()) / 2
|
||||
|
||||
# Flip the shapes if requested
|
||||
if flip_target:
|
||||
target_shape = [s.copy() for s in target_shape]
|
||||
# Flip
|
||||
target_shape = [s.reflect([0, median_y], [1, median_y])
|
||||
for s in target_shape]
|
||||
# Flip the order as well to reflect orientation change
|
||||
target_shape = [s.reverse() for s in target_shape]
|
||||
|
||||
# Calculate relative offsets to place the whole shape at the target offset
|
||||
shortcuts = np.asarray([e.shortcut() for e in target_shape])
|
||||
rel_offsets = [(s[0][1] + s[1][1]) / 2 - median_y for s in shortcuts]
|
||||
per_seq_offsets = [offset + r for r in rel_offsets]
|
||||
|
||||
# Project from farthest to closest
|
||||
sorted_tup = sorted(zip(per_seq_offsets, target_shape), reverse=True)
|
||||
proj_edge, int_edges = base_edge, EdgeSequence(base_edge)
|
||||
new_in_edges = EdgeSequence()
|
||||
all_new_edges = EdgeSequence(base_edge)
|
||||
for off, shape in sorted_tup:
|
||||
new_edge, in_edges, new_interface = cut_into_edge(
|
||||
shape, proj_edge, offset=off, right=right, tol=tol)
|
||||
|
||||
all_new_edges.substitute(proj_edge, new_edge)
|
||||
int_edges.substitute(proj_edge, new_interface)
|
||||
new_in_edges.append(in_edges)
|
||||
proj_edge = new_edge[0]
|
||||
|
||||
return all_new_edges, new_in_edges, int_edges
|
||||
|
||||
|
||||
def cut_into_edge_single(target_shape, base_edge: Edge, offset=0, right=True,
|
||||
tol=1e-2, verbose: bool = False):
|
||||
""" Insert edges of the target_shape into the given base_edge, starting
|
||||
from offset
|
||||
edges in target shape are rotated s.t. start -> end vertex vector is
|
||||
aligned with the edge
|
||||
|
||||
Parameters:
|
||||
* target_shape -- list of single edge or chained edges to be inserted
|
||||
in the edge.
|
||||
* base_edge -- edge object, defining the border
|
||||
* right -- which direction the cut should be oriented w.r.t. the
|
||||
direction of base edge
|
||||
* Offset -- position of the center of the target shape along the edge.
|
||||
|
||||
Returns:
|
||||
* Newly created edges that accommodate the cut
|
||||
* Edges corresponding to the target shape
|
||||
* Edges that lie on the original base edge
|
||||
"""
|
||||
|
||||
target_shape = EdgeSequence(target_shape)
|
||||
new_edges = target_shape.copy().snap_to([0, 0]) # copy and normalize translation of vertices
|
||||
|
||||
# Simplify to vectors
|
||||
shortcut = new_edges.shortcut() # "Interface" of the shape to insert
|
||||
target_shape_w = norm(shortcut)
|
||||
edge_len = base_edge.length()
|
||||
|
||||
if offset < target_shape_w / 2 - tol or offset > (edge_len - target_shape_w / 2) + tol:
|
||||
# NOTE: This is not a definitive check, and the cut might still not fit, depending on the base_edge curvature
|
||||
raise ValueError(f'Operators-CutingIntoEdge::ERROR::offset value is not within the base_edge length')
|
||||
|
||||
# find starting vertex for insertion & place edges there
|
||||
curve = base_edge.as_curve()
|
||||
rel_offset = curve.ilength(offset, s_tol=ILENGTH_S_TOL)
|
||||
|
||||
# ----- OPTIMIZATION ---
|
||||
start = [0.1, 0.1]
|
||||
out = minimize(
|
||||
_fit_location_edge, start,
|
||||
args=(rel_offset, target_shape_w, curve),
|
||||
bounds=[(0, 1)])
|
||||
shift = out.x
|
||||
|
||||
# Error checks
|
||||
if verbose and not out.success:
|
||||
print(f'Cut_edge::ERROR::finding the projection (translation) is unsuccessful. Likely an error in edges choice')
|
||||
|
||||
if not close_enough(out.fun, tol=0.01):
|
||||
if verbose:
|
||||
print(out)
|
||||
raise RuntimeError(f'Cut_edge::ERROR::projection on {base_edge} finished with fun={out.fun}')
|
||||
|
||||
if rel_offset + shift[0] > 1 + tol or (rel_offset - shift[1]) < 0 - tol:
|
||||
raise RuntimeError(
|
||||
f'Cut_edge::ERROR::projection on {base_edge} is out of edge bounds: '
|
||||
f'[{rel_offset - shift[1], rel_offset + shift[0]}].'
|
||||
' Check the offset value')
|
||||
|
||||
# All good -- integrate the target shape
|
||||
ins_point = c_to_np(curve.point(rel_offset - shift[1])) if (rel_offset - shift[1]) > tol else base_edge.start
|
||||
fin_point = c_to_np(curve.point(rel_offset + shift[0])) if (rel_offset + shift[0]) < 1 - tol else base_edge.end
|
||||
|
||||
# Align the shape with an edge
|
||||
# find rotation to apply on target shape
|
||||
insert_vector = np.asarray(fin_point) - np.asarray(ins_point)
|
||||
angle = vector_angle(shortcut[1] - shortcut[0], insert_vector)
|
||||
new_edges.rotate(angle)
|
||||
|
||||
# place
|
||||
new_edges.snap_to(ins_point)
|
||||
|
||||
# Check orientation
|
||||
avg_vertex = np.asarray(new_edges.verts()).mean(0)
|
||||
right_position = np.sign(np.cross(insert_vector, avg_vertex - np.asarray(new_edges[0].start))) == -1
|
||||
if not right and right_position or right and not right_position:
|
||||
# flip shape to match the requested direction
|
||||
new_edges.reflect(new_edges[0].start, new_edges[-1].end)
|
||||
|
||||
# Integrate edges
|
||||
# NOTE: no need to create extra edges if the the shape is incerted right at the beggining or end of the edge
|
||||
base_edge_leftovers = EdgeSequence()
|
||||
start_id, end_id = 0, len(new_edges)
|
||||
|
||||
if ins_point is base_edge.start:
|
||||
new_edges[0].start = base_edge.start # Connect into the original edge
|
||||
else:
|
||||
# TODOLOW more elegant subroutine
|
||||
start_part = base_edge.subdivide_param([rel_offset - shift[1], 1 - (rel_offset - shift[1])])[0]
|
||||
start_part.end = new_edges[0].start
|
||||
new_edges.insert(0, start_part)
|
||||
base_edge_leftovers.append(new_edges[0])
|
||||
start_id = 1
|
||||
|
||||
if fin_point is base_edge.end:
|
||||
new_edges[-1].end = base_edge.end # Connect into the original edge
|
||||
else:
|
||||
end_part = base_edge.subdivide_param([rel_offset + shift[0], 1 - (rel_offset + shift[0])])[-1]
|
||||
end_part.start = new_edges[-1].end
|
||||
new_edges.append(end_part)
|
||||
base_edge_leftovers.append(new_edges[-1])
|
||||
end_id = -1
|
||||
|
||||
return new_edges, new_edges[start_id:end_id], base_edge_leftovers
|
||||
|
||||
|
||||
def _fit_location_corner(l, diff_target, curve1, curve2,
|
||||
verbose: bool = False):
|
||||
"""Find the points on two curves s.t. vector between them is the same as
|
||||
shortcut"""
|
||||
|
||||
# Current points on curves
|
||||
point1 = c_to_np(curve1.point(l[0]))
|
||||
point2 = c_to_np(curve2.point(l[1]))
|
||||
diff_curr = point2 - point1
|
||||
|
||||
if verbose:
|
||||
print('Location Progression: ', (diff_curr[0] - diff_target[0])**2,
|
||||
(diff_curr[1] - diff_target[1])**2)
|
||||
|
||||
return ((diff_curr[0] - diff_target[0])**2
|
||||
+ (diff_curr[1] - diff_target[1])**2)
|
||||
|
||||
|
||||
def _fit_location_edge(shift, location, width_target, curve,
|
||||
verbose: bool = False):
|
||||
"""Find the points on two curves s.t. vector between them is the same as
|
||||
shortcut"""
|
||||
|
||||
# Current points on curves
|
||||
pointc = c_to_np(curve.point(location)) # TODO this is constant
|
||||
point1 = c_to_np(curve.point(location + shift[0]))
|
||||
point2 = c_to_np(curve.point(location - shift[1]))
|
||||
|
||||
if verbose:
|
||||
print('Location Progression: ', (_dist(point1, point2) - width_target)**2)
|
||||
|
||||
# regularize points to be at the same distance from center
|
||||
reg_symmetry = (_dist(point1, pointc) - _dist(point2, pointc))**2
|
||||
|
||||
return (_dist(point1, point2) - width_target)**2 + reg_symmetry
|
||||
|
||||
|
||||
# ANCHOR ----- Panel operations ------
|
||||
def distribute_Y(component, n_copies, odd_copy_shift=0, name_tag='panel'):
|
||||
"""Distribute copies of component over the circle around Oy"""
|
||||
copies = [ component ]
|
||||
component.name = f'{name_tag}_0' # Unique
|
||||
delta_rotation = R.from_euler('XYZ', [0, 360 / n_copies, 0], degrees=True)
|
||||
|
||||
for i in range(n_copies - 1):
|
||||
new_component = deepcopy(copies[-1])
|
||||
new_component.name = f'{name_tag}_{i + 1}' # Unique
|
||||
new_component.rotate_by(delta_rotation)
|
||||
new_component.translate_to(delta_rotation.apply(new_component.translation))
|
||||
|
||||
copies.append(new_component)
|
||||
|
||||
# shift around to resolve collisions (hopefully)
|
||||
if odd_copy_shift:
|
||||
for i in range(n_copies):
|
||||
if not i % 2:
|
||||
copies[i].translate_by(copies[i].norm() * odd_copy_shift)
|
||||
|
||||
return copies
|
||||
|
||||
|
||||
def distribute_horisontally(component, n_copies, stride=20, name_tag='panel'):
|
||||
"""Distribute copies of component over the straight horisontal line
|
||||
perpendicular to the norm"""
|
||||
copies = [ component ]
|
||||
component.name = f'{name_tag}_0' # Unique
|
||||
|
||||
if isinstance(component, BaseComponent):
|
||||
translation_dir = component.rotation.apply([0, 0, 1]) # Horisontally along the panel
|
||||
# FIXME What if it's looking up?
|
||||
translation_dir = np.cross(translation_dir, [0, 1, 0]) # perpendicular to Y
|
||||
translation_dir = translation_dir / norm(translation_dir)
|
||||
delta_translation = translation_dir * stride
|
||||
else:
|
||||
translation_dir = [1, 0, 0]
|
||||
|
||||
for i in range(n_copies - 1):
|
||||
new_component = deepcopy(copies[-1]) # TODO proper copy
|
||||
new_component.name = f'{name_tag}_{i + 1}' # Unique
|
||||
new_component.translate_by(delta_translation)
|
||||
|
||||
copies.append(new_component)
|
||||
|
||||
return copies
|
||||
|
||||
|
||||
# ANCHOR ----- Sleeve support -----
|
||||
def even_armhole_openings(front_opening, back_opening, tol=1e-2, verbose: bool = False):
|
||||
"""
|
||||
Rearrange sleeve openings for front and back s.t. their projection
|
||||
on vertical line is the same, while preserving the overall shape.
|
||||
Allows for creation of two symmetric sleeve panels from them
|
||||
|
||||
!! Important: assumes that the front opening is longer then back opening
|
||||
"""
|
||||
# Construct sleeve panel shapes from opening inverses
|
||||
cfront, cback = front_opening.copy(), back_opening.copy()
|
||||
cback.reflect([0, 0], [1, 0]).reverse().snap_to(cfront[-1].end)
|
||||
|
||||
# Cutout
|
||||
slope = np.array([cfront[0].start, cback[-1].end])
|
||||
slope_vec = slope[1] - slope[0]
|
||||
slope_perp = np.asarray([-slope_vec[1], slope_vec[0]])
|
||||
slope_midpoint = (slope[0] + slope[1]) / 2
|
||||
|
||||
# Intersection with the sleeve itself line
|
||||
# svgpath tools allow solution regardless of egde types
|
||||
inter_segment = svgpath.Line(
|
||||
list_to_c(slope_midpoint - 20 * slope_perp),
|
||||
list_to_c(slope_midpoint + 20 * slope_perp)
|
||||
)
|
||||
target_segment = cfront[-1].as_curve()
|
||||
|
||||
intersect_t = target_segment.intersect(inter_segment)
|
||||
if len(intersect_t) != 1 and verbose:
|
||||
print(
|
||||
f'Redistribute Sleeve Openings::WARNING::{len(intersect_t)} intersection points instead of one. '
|
||||
f'Front and back opening curves might be the same with lengths: {cfront.length()}, {cback.length()}'
|
||||
)
|
||||
|
||||
if (len(intersect_t) >= 1
|
||||
and not (close_enough(intersect_t[0][0], 0, tol=tol) # checking if they are already ok separated
|
||||
or close_enough(intersect_t[0][0], 1, tol=tol))):
|
||||
# The current separation is not satisfactory
|
||||
# Update the opening shapes
|
||||
intersect_t = intersect_t[0][0]
|
||||
subdiv = front_opening.edges[-1].subdivide_param([intersect_t, 1 - intersect_t])
|
||||
front_opening.substitute(-1, subdiv[0])
|
||||
|
||||
# Move this part to the back opening
|
||||
subdiv[1].start, subdiv[1].end = copy(subdiv[1].start), copy(subdiv[1].end) # Disconnect vertices in subdivided version
|
||||
subdiv.pop(0) # TODOLOW No reflect in the edge class??
|
||||
subdiv.reflect([0, 0], [1, 0]).reverse().snap_to(back_opening[-1].end)
|
||||
subdiv[0].start = back_opening[-1].end
|
||||
|
||||
back_opening.append(subdiv[0])
|
||||
|
||||
# Align the slope with OY direction
|
||||
# for correct size of sleeve panels
|
||||
slope_angle = np.arctan(-slope_vec[0] / slope_vec[1])
|
||||
front_opening.rotate(-slope_angle)
|
||||
back_opening.rotate(slope_angle)
|
||||
|
||||
return front_opening, back_opening
|
||||
|
||||
|
||||
# ANCHOR ----- Curve tools -----
|
||||
def _avg_curvature(curve, points_estimates=100):
|
||||
"""Average curvature in a curve"""
|
||||
# NOTE: this work slow, but direct evaluation seems
|
||||
# infeasible
|
||||
# Some hints here:
|
||||
# https://math.stackexchange.com/questions/220900/bezier-curvature
|
||||
t_space = np.linspace(0, 1, points_estimates)
|
||||
return sum([curve.curvature(t) for t in t_space]) / points_estimates
|
||||
|
||||
|
||||
def _max_curvature(curve, points_estimates=100):
|
||||
"""Average curvature in a curve"""
|
||||
# NOTE: this work slow, but direct evaluation seems
|
||||
# infeasible
|
||||
# Some hints here: https://math.stackexchange.com/questions/1954845/bezier-curvature-extrema
|
||||
t_space = np.linspace(0, 1, points_estimates)
|
||||
return max([curve.curvature(t) for t in t_space])
|
||||
|
||||
|
||||
def _bend_extend_2_tangent(
|
||||
shift, cp, target_len, direction,
|
||||
target_tangent_start, target_tangent_end,
|
||||
point_estimates=50):
|
||||
"""Evaluate how well curve preserves the length and tangents
|
||||
|
||||
NOTE: point_estimates controls average curvature evaluation.
|
||||
The higher the number, the more stable the optimization,
|
||||
but higher computational cost
|
||||
"""
|
||||
|
||||
control = np.array([
|
||||
cp[0],
|
||||
[cp[1][0] + shift[0], cp[1][1] + shift[1]],
|
||||
[cp[2][0] + shift[2], cp[2][1] + shift[3]],
|
||||
cp[-1] + direction * shift[4]
|
||||
])
|
||||
|
||||
params = control[:, 0] + 1j*control[:, 1]
|
||||
curve_inverse = svgpath.CubicBezier(*params)
|
||||
|
||||
length_diff = (curve_inverse.length() - target_len)**2 # preservation
|
||||
|
||||
tan_0_diff = (abs(curve_inverse.unit_tangent(0) - target_tangent_start))**2
|
||||
tan_1_diff = (abs(curve_inverse.unit_tangent(1) - target_tangent_end))**2
|
||||
|
||||
# NOTE: tried regularizing based on Y value in relative coordinates (for speed),
|
||||
# But it doesn't produce good results
|
||||
curvature_reg = _max_curvature(curve_inverse, points_estimates=point_estimates)**2
|
||||
|
||||
end_expantion_reg = 0.001*shift[-1]**2
|
||||
|
||||
return length_diff + tan_0_diff + tan_1_diff + curvature_reg + end_expantion_reg
|
||||
|
||||
|
||||
def curve_match_tangents(curve, target_tan0, target_tan1, target_len=None,
|
||||
return_as_edge=False, verbose: bool = False):
|
||||
"""Update the curve to have the desired tangent directions at endpoints
|
||||
while preserving curve length or desired target length ('target_len') and overall direction
|
||||
|
||||
Returns
|
||||
* control points for the final CubicBezier curves
|
||||
* Or CurveEdge instance, if return_as_edge=True
|
||||
|
||||
NOTE: Only Cubic Bezier curves are supported
|
||||
NOTE: Expects good enough initialization ('curve') that approximated desired solution
|
||||
"""
|
||||
if not isinstance(curve, svgpath.CubicBezier):
|
||||
raise NotImplementedError(
|
||||
f'Curve_match_tangents::ERROR::Only Cubic Bezier curves are supported ',
|
||||
f'(got {type(curve)})')
|
||||
|
||||
curve_cps = c_to_np(curve.bpoints())
|
||||
|
||||
direction = curve_cps[-1] - curve_cps[0]
|
||||
direction /= np.linalg.norm(direction)
|
||||
|
||||
target_tan0 = target_tan0 / np.linalg.norm(target_tan0)
|
||||
target_tan1 = target_tan1 / np.linalg.norm(target_tan1)
|
||||
|
||||
# match tangents with the requested ones while preserving length
|
||||
out = minimize(
|
||||
_bend_extend_2_tangent, # with tangent matching
|
||||
[0, 0, 0, 0, 0],
|
||||
args=(
|
||||
curve_cps,
|
||||
curve.length() if target_len is None else target_len,
|
||||
direction,
|
||||
list_to_c(target_tan0),
|
||||
list_to_c(target_tan1),
|
||||
70 # NOTE: Low values cause instable resutls
|
||||
),
|
||||
method='L-BFGS-B',
|
||||
)
|
||||
if not out.success:
|
||||
if verbose:
|
||||
print(f'Curve_match_tangents::WARNING::optimization not successfull')
|
||||
print(out)
|
||||
|
||||
shift = out.x
|
||||
|
||||
fin_curve_cps = [
|
||||
curve_cps[0].tolist(),
|
||||
[curve_cps[1][0] + shift[0], curve_cps[1][1] + shift[1]],
|
||||
[curve_cps[2][0] + shift[2], curve_cps[2][1] + shift[3]],
|
||||
(curve_cps[-1] + direction*shift[-1]).tolist(),
|
||||
]
|
||||
|
||||
if return_as_edge:
|
||||
fin_inv_edge = CurveEdge(
|
||||
start=fin_curve_cps[0],
|
||||
end=fin_curve_cps[-1],
|
||||
control_points=fin_curve_cps[1:3],
|
||||
relative=False
|
||||
)
|
||||
return fin_inv_edge
|
||||
|
||||
return fin_curve_cps
|
||||
|
||||
|
||||
# ---- Utils ----
|
||||
|
||||
def _dist(v1, v2):
|
||||
return norm(v2-v1)
|
||||
|
||||
|
||||
def _fit_scale(s, shortcut, v1, v2, vc, d_v1, d_v2):
|
||||
"""Evaluate how good a shortcut fits the corner if the vertices are shifted
|
||||
a little along the line"""
|
||||
# Shortcut can be used as 2D vector, not a set of 2D points, e.g.
|
||||
shifted = deepcopy(shortcut)
|
||||
shifted[0] += (shortcut[0] - shortcut[1]) * s[0] # this only changes the end vertex though
|
||||
shifted[1] += (shortcut[1] - shortcut[0]) * s[1] # this only changes the end vertex though
|
||||
|
||||
return ((d_v1 - _dist(shifted[0], v1) - _dist(shifted[0], vc))**2
|
||||
+ (d_v2 - _dist(shifted[1], v2) - _dist(shifted[1], vc))**2
|
||||
)
|
||||
408
pygarment/garmentcode/panel.py
Normal file
408
pygarment/garmentcode/panel.py
Normal file
@@ -0,0 +1,408 @@
|
||||
import numpy as np
|
||||
from copy import copy
|
||||
from argparse import Namespace
|
||||
from scipy.spatial.transform import Rotation as R
|
||||
|
||||
from pygarment.pattern.core import BasicPattern
|
||||
from pygarment.garmentcode.base import BaseComponent
|
||||
from pygarment.garmentcode.edge import Edge, EdgeSequence, CircleEdge
|
||||
from pygarment.garmentcode.utils import close_enough, vector_align_3D
|
||||
from pygarment.garmentcode.operators import cut_into_edge
|
||||
from pygarment.garmentcode.interface import Interface
|
||||
|
||||
|
||||
class Panel(BaseComponent):
|
||||
""" A Base class for defining a Garment component corresponding to a single
|
||||
flat fiece of fabric
|
||||
|
||||
Defined as a collection of edges on a 2D grid with specified 3D placement
|
||||
(world coordinates)
|
||||
|
||||
NOTE: All operations methods return 'self' object to allow sequential
|
||||
applications
|
||||
|
||||
"""
|
||||
def __init__(self, name, label='') -> None:
|
||||
"""Base class for panel creations
|
||||
* Name: panel name. Expected to be a unique identifier of a panel object
|
||||
* label: additional panel label (non-unique)
|
||||
"""
|
||||
super().__init__(name)
|
||||
|
||||
self.label = label
|
||||
self.translation = np.zeros(3)
|
||||
self.rotation = R.from_euler('XYZ', [0, 0, 0]) # zero rotation
|
||||
# NOTE: initiating with empty sequence allows .append() to it safely
|
||||
self.edges = EdgeSequence()
|
||||
|
||||
# Info
|
||||
def pivot_3D(self):
|
||||
"""Pivot point of a panel in 3D"""
|
||||
return self.point_to_3D([0, 0])
|
||||
|
||||
def length(self, longest_dim=False):
|
||||
"""Length of a panel element in cm
|
||||
|
||||
Defaults the to the vertical length of a 2D bounding box
|
||||
* longest_dim -- if set, returns the longest dimention out of the bounding box dimentions
|
||||
"""
|
||||
bbox = self.bbox()
|
||||
|
||||
x = abs(bbox[1][0] - bbox[0][0])
|
||||
y = abs(bbox[1][1] - bbox[0][1])
|
||||
|
||||
return max(x, y) if longest_dim else y
|
||||
|
||||
def is_self_intersecting(self):
|
||||
"""Check whether the panel has self-intersection"""
|
||||
edge_curves = []
|
||||
for e in self.edges:
|
||||
if isinstance(e, CircleEdge):
|
||||
# NOTE: Intersections for Arcs (Circle edge) fails in svgpathtools:
|
||||
# They are not well implemented in svgpathtools, see
|
||||
# https://github.com/mathandy/svgpathtools/issues/121
|
||||
# https://github.com/mathandy/svgpathtools/blob/fcb648b9bb9591d925876d3b51649fa175b40524/svgpathtools/path.py#L1960
|
||||
# Hence using linear approximation for robustness:
|
||||
edge_curves += [eseg.as_curve() for eseg in e.linearize(n_verts_inside=10)]
|
||||
else:
|
||||
edge_curves.append(e.as_curve())
|
||||
|
||||
# NOTE: simple pairwise checks of edges
|
||||
for i1 in range(0, len(edge_curves)):
|
||||
for i2 in range(i1 + 1, len(edge_curves)):
|
||||
intersect_t = edge_curves[i1].intersect(edge_curves[i2])
|
||||
|
||||
# Check exceptions -- intersection at the vertex
|
||||
for i in range(len(intersect_t)):
|
||||
t1, t2 = intersect_t[i]
|
||||
if t2 < t1:
|
||||
t1, t2 = t2, t1
|
||||
if close_enough(t1, 0) and close_enough(t2, 1):
|
||||
intersect_t[i] = None
|
||||
intersect_t = [el for el in intersect_t if el is not None]
|
||||
|
||||
if intersect_t: # Any other case of intersections
|
||||
return True
|
||||
return False
|
||||
|
||||
# ANCHOR - Operations -- update object in-place
|
||||
def set_panel_label(self, label: str, overwrite=True):
|
||||
"""If overwrite is not enabled, only updates the label if it's empty."""
|
||||
if not self.label or overwrite:
|
||||
self.label = label
|
||||
|
||||
def set_pivot(self, point_2d, replicate_placement=False):
|
||||
"""Specify 2D point w.r.t. panel local space
|
||||
to be used as pivot for translation and rotation
|
||||
|
||||
Parameters:
|
||||
* point_2d -- desired point 2D point w.r.t current pivot (origin)
|
||||
of panel local space
|
||||
* replicate_placement -- will replicate the location of the panel
|
||||
as it was before pivot change
|
||||
default - False (no adjustment, the panel may "jump" in 3D)
|
||||
"""
|
||||
point_2d = copy(point_2d) # Remove unwanted object reference
|
||||
# In case an actual vertex was used as a target point
|
||||
|
||||
if replicate_placement:
|
||||
self.translation = self.point_to_3D(point_2d)
|
||||
# FIXME Replicate rotation
|
||||
|
||||
# UPD vertex locations relative to new pivot
|
||||
for v in self.edges.verts():
|
||||
v[0] -= int(point_2d[0])
|
||||
v[1] -= int(point_2d[1])
|
||||
|
||||
def top_center_pivot(self):
|
||||
"""One of the most useful pivots
|
||||
is the one in the middle of the top edge of the panel
|
||||
"""
|
||||
vertices = np.asarray(self.edges.verts())
|
||||
|
||||
# out of 2D bounding box sides' midpoints choose the one that is
|
||||
# highest in 3D
|
||||
top_right = vertices.max(axis=0)
|
||||
low_left = vertices.min(axis=0)
|
||||
mid_x = (top_right[0] + low_left[0]) / 2
|
||||
mid_y = (top_right[1] + low_left[1]) / 2
|
||||
mid_points_2D = [
|
||||
[mid_x, top_right[1]],
|
||||
[mid_x, low_left[1]],
|
||||
[top_right[0], mid_y],
|
||||
[low_left[0], mid_y]
|
||||
]
|
||||
mid_points_3D = np.vstack(tuple(
|
||||
[self.point_to_3D(coords) for coords in mid_points_2D]
|
||||
))
|
||||
top_mid_point = mid_points_3D[:, 1].argmax()
|
||||
|
||||
self.set_pivot(mid_points_2D[top_mid_point])
|
||||
return self
|
||||
|
||||
def translate_by(self, delta_vector):
|
||||
"""Translate panel by a vector"""
|
||||
self.translation = self.translation + np.array(delta_vector)
|
||||
# NOTE: One may also want to have autonorm only on the assembly?
|
||||
self.autonorm()
|
||||
return self
|
||||
|
||||
def translate_to(self, new_translation):
|
||||
"""Set panel translation to be exactly that vector"""
|
||||
self.translation = np.asarray(new_translation)
|
||||
self.autonorm()
|
||||
return self
|
||||
|
||||
def rotate_by(self, delta_rotation: R):
|
||||
"""Rotate panel by a given rotation
|
||||
* delta_rotation: scipy rotation object
|
||||
"""
|
||||
self.rotation = delta_rotation * self.rotation
|
||||
self.autonorm()
|
||||
return self
|
||||
|
||||
def rotate_to(self, new_rot: R):
|
||||
"""Set panel rotation to be exactly the given rotation
|
||||
* new_rot: scipy rotation object
|
||||
"""
|
||||
if not isinstance(new_rot, R):
|
||||
raise ValueError(f'{self.__class__.__name__}::ERROR::Only accepting rotations in scipy format')
|
||||
self.rotation = new_rot
|
||||
self.autonorm()
|
||||
return self
|
||||
|
||||
def rotate_align(self, vector):
|
||||
"""Set panel rotation s.t. it's norm is aligned with a given 3D
|
||||
vector"""
|
||||
|
||||
vector = np.asarray(vector)
|
||||
vector = vector / np.linalg.norm(vector)
|
||||
n = self.norm()
|
||||
self.rotate_by(vector_align_3D(n, vector))
|
||||
return self
|
||||
|
||||
def center_x(self):
|
||||
"""Adjust translation over x s.t. the center of the panel is aligned
|
||||
with the Y axis (center of the body)"""
|
||||
|
||||
center_3d = self.point_to_3D(self._center_2D())
|
||||
self.translation[0] += -center_3d[0]
|
||||
return self
|
||||
|
||||
def autonorm(self):
|
||||
"""Update right/wrong side orientation, s.t. the normal of the surface
|
||||
looks outside he world origin,
|
||||
taking into account the shape and the global position.
|
||||
|
||||
This should provide correct panel orientation in most cases.
|
||||
|
||||
NOTE: for best results, call autonorm after translation
|
||||
specification
|
||||
"""
|
||||
norm_dr = self.norm()
|
||||
|
||||
# NOTE: Nothing happens if self.translation is zero
|
||||
if np.dot(norm_dr, self.translation) < 0:
|
||||
# Swap if wrong
|
||||
self.edges.reverse()
|
||||
|
||||
def mirror(self, axis=None):
|
||||
"""Swap this panel with its mirror image
|
||||
|
||||
Axis specifies 2D axis to swap around: Y axis by default
|
||||
"""
|
||||
if axis is None:
|
||||
axis = [0, 1]
|
||||
# Case Around Y
|
||||
if close_enough(axis[0], tol=1e-4): # reflection around Y
|
||||
|
||||
# Vertices
|
||||
self.edges.reflect([0, 0], [0, 1])
|
||||
|
||||
# Position
|
||||
self.translation[0] *= -1
|
||||
|
||||
# Rotations
|
||||
curr_euler = self.rotation.as_euler('XYZ')
|
||||
curr_euler[1] *= -1
|
||||
curr_euler[2] *= -1
|
||||
self.rotate_to(R.from_euler('XYZ', curr_euler))
|
||||
|
||||
# Fix right/wrong side
|
||||
self.autonorm()
|
||||
else:
|
||||
# TODO Any other axis
|
||||
raise NotImplementedError(f'{self.name}::ERROR::Mirrowing over arbitrary axis is not implemented')
|
||||
return self
|
||||
|
||||
def add_dart(self, dart_shape, edge, offset, right=True, edge_seq=None, int_edge_seq=None, ):
|
||||
""" Shortcut for adding a dart to a panel:
|
||||
* Performs insertion of the dart_shape in the given edge (parameters are the same
|
||||
as in pyp.ops.cut_into_edge)
|
||||
* Creates stitch to connect the dart sides
|
||||
* Modifies edge_sequnces with full set (edge_seq) or only the interface part (int_edge_seq)
|
||||
of the created edges, if those are provided
|
||||
|
||||
Returns new edges after insertion, and the interface part (excludes dart edges)
|
||||
"""
|
||||
edges_new, dart_edges, int_new = cut_into_edge(
|
||||
dart_shape,
|
||||
edge,
|
||||
offset=offset,
|
||||
right=right)
|
||||
|
||||
self.stitching_rules.append(
|
||||
(Interface(self, dart_edges[0]), Interface(self, dart_edges[1])))
|
||||
|
||||
# Update the edges if given
|
||||
if edge_seq is not None:
|
||||
edge_seq.substitute(edge, edges_new)
|
||||
edges_new = edge_seq
|
||||
if int_edge_seq is not None:
|
||||
int_edge_seq.substitute(edge, int_new)
|
||||
int_new = int_edge_seq
|
||||
|
||||
return edges_new, int_new
|
||||
|
||||
# ANCHOR - Build the panel -- get serializable representation
|
||||
def assembly(self):
|
||||
"""Convert panel into serialazable representation
|
||||
|
||||
NOTE: panel EdgeSequence is assumed to be a single loop of edges
|
||||
"""
|
||||
# FIXME Some panels have weird resulting alignemnt when th
|
||||
# is pivot setup is removed -- there is a bug somewhere
|
||||
|
||||
# always start from zero for consistency between panels
|
||||
self.set_pivot(self.edges[0].start, replicate_placement=True)
|
||||
|
||||
# Basics
|
||||
panel = Namespace(
|
||||
translation=self.translation.tolist(),
|
||||
rotation=self.rotation.as_euler('XYZ', degrees=True).tolist(),
|
||||
vertices=[self.edges[0].start],
|
||||
edges=[])
|
||||
|
||||
for i in range(len(self.edges)):
|
||||
vertices, edge = self.edges[i].assembly()
|
||||
|
||||
# add new vertices
|
||||
if panel.vertices[-1] == vertices[0]: # We care if both point to the same vertex location, not necessarily the same vertex object
|
||||
vert_shift = len(panel.vertices) - 1 # first edge vertex = last vertex already in the loop
|
||||
panel.vertices += vertices[1:]
|
||||
else:
|
||||
vert_shift = len(panel.vertices)
|
||||
panel.vertices += vertices
|
||||
|
||||
# upd vertex references in edges according to new vertex ids in
|
||||
# the panel vertex loop
|
||||
edge['endpoints'] = [id + vert_shift for id in edge['endpoints']]
|
||||
|
||||
edge_shift = len(panel.edges) # before adding new ones
|
||||
self.edges[i].geometric_id = edge_shift # remember the mapping of logical edge to geometric id in panel loop
|
||||
panel.edges.append(edge)
|
||||
|
||||
# Check closing of the loop and upd vertex reference for the last edge
|
||||
if panel.vertices[-1] == panel.vertices[0]:
|
||||
panel.vertices.pop()
|
||||
panel.edges[-1]['endpoints'][-1] = 0
|
||||
|
||||
# Add panel label, if known
|
||||
if self.label:
|
||||
panel.label = self.label
|
||||
|
||||
spattern = BasicPattern()
|
||||
spattern.name = self.name
|
||||
spattern.pattern['panels'] = {self.name: vars(panel)}
|
||||
|
||||
# Assembly stitching info (panel might have inner stitches)
|
||||
spattern.pattern['stitches'] = self.stitching_rules.assembly()
|
||||
|
||||
return spattern
|
||||
|
||||
# ANCHOR utils
|
||||
def _center_2D(self, n_verts_inside = 3):
|
||||
"""Approximate Location of the panel center.
|
||||
|
||||
NOTE: uses crude linear approximation for curved edges,
|
||||
n_verts_inside = number of vertices (excluding the start
|
||||
and end vertices) used to create a linearization of an edge
|
||||
"""
|
||||
# NOTE: assuming that edges are organized in a loop and share vertices
|
||||
lin_edges = EdgeSequence([e.linearize(n_verts_inside)
|
||||
for e in self.edges])
|
||||
verts = lin_edges.verts()
|
||||
|
||||
return np.mean(verts, axis=0)
|
||||
|
||||
def point_to_3D(self, point_2d):
|
||||
"""Calculate 3D location of a point given in the local 2D plane """
|
||||
point_2d = np.asarray(point_2d)
|
||||
if len(point_2d) == 2:
|
||||
point_2d = np.append(point_2d, 0)
|
||||
|
||||
point_3d = self.rotation.apply(point_2d)
|
||||
point_3d += self.translation
|
||||
return point_3d
|
||||
|
||||
def norm(self):
|
||||
"""Normal direction for the current panel using bounding box"""
|
||||
|
||||
# To make norm evaluation work for non-convex panels
|
||||
# Determine points located on bounding box (b_verts_2d), compute
|
||||
# norm of consecutive b_verts_3d and the b_verts_3d mean (b_center_3d),
|
||||
# then weight the norms.
|
||||
# The dominant norm direction should be the correct one
|
||||
|
||||
_, b_verts_2d = self.edges.bbox()
|
||||
b_verts_3d = [self.point_to_3D(bv_2d) for bv_2d in b_verts_2d]
|
||||
b_center_3d = np.mean((b_verts_3d), axis=0)
|
||||
|
||||
norms = []
|
||||
num_b_verts_3d = len(b_verts_3d)
|
||||
for i in range(num_b_verts_3d):
|
||||
vert_0 = b_verts_3d[i]
|
||||
vert_1 = b_verts_3d[(i + 1) % num_b_verts_3d]
|
||||
# Pylance + NP error for unreachanble code -- see https://github.com/numpy/numpy/issues/22146
|
||||
# Works ok for numpy 1.23.4+
|
||||
norm = np.cross(vert_0 - b_center_3d, vert_1 - b_center_3d)
|
||||
norm /= np.linalg.norm(norm)
|
||||
norms.append(norm)
|
||||
|
||||
# Current norm direction
|
||||
avg_norm = sum(norms) / len(norms)
|
||||
|
||||
if close_enough(np.linalg.norm(avg_norm), 0):
|
||||
# Indecisive averaging, so using just one of the norms
|
||||
# NOTE: sometimes happens on thin arcs
|
||||
avg_norm = norms[0]
|
||||
if self.verbose:
|
||||
print(f'{self.__class__.__name__}::{self.name}::WARNING::Norm evaluation failed, assigning norm based on the first edge')
|
||||
|
||||
final_norm = avg_norm / np.linalg.norm(avg_norm)
|
||||
|
||||
# solve float errors
|
||||
for i, ni in enumerate(final_norm):
|
||||
if np.isclose([ni], [0.0]):
|
||||
final_norm[i] = 0.0
|
||||
|
||||
return final_norm
|
||||
|
||||
def bbox(self):
|
||||
"""Evaluate 2D bounding box"""
|
||||
# Using curve linearization for more accurate approximation of bbox
|
||||
lin_edges = EdgeSequence([e.linearize() for e in self.edges])
|
||||
verts_2d = np.asarray(lin_edges.verts())
|
||||
|
||||
return verts_2d.min(axis=0), verts_2d.max(axis=0)
|
||||
|
||||
|
||||
def bbox3D(self):
|
||||
"""Evaluate 3D bounding box of the current panel"""
|
||||
|
||||
# Using curve linearization for more accurate approximation of bbox
|
||||
lin_edges = EdgeSequence([e.linearize() for e in self.edges])
|
||||
verts_2d = lin_edges.verts()
|
||||
verts_3d = np.asarray([self.point_to_3D(v) for v in verts_2d])
|
||||
|
||||
return verts_3d.min(axis=0), verts_3d.max(axis=0)
|
||||
153
pygarment/garmentcode/params.py
Normal file
153
pygarment/garmentcode/params.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""Parameter class wrappers around parameter files allowing definition of computed parameters
|
||||
"""
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from copy import deepcopy
|
||||
import random
|
||||
|
||||
from pygarment.garmentcode.utils import nested_get, nested_set, close_enough
|
||||
|
||||
|
||||
class BodyParametrizationBase:
|
||||
"""Base class for body parametrization wrappers that allows definition of
|
||||
dependent parameters
|
||||
"""
|
||||
|
||||
def __init__(self, param_file='') -> None:
|
||||
|
||||
self.params = {}
|
||||
self.load(param_file)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.params[key]
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Return an iterator of dict keys
|
||||
"""
|
||||
return iter(self.params)
|
||||
|
||||
# Updates
|
||||
def __setitem__(self, key, value):
|
||||
self.params[key] = value
|
||||
self.eval_dependencies(key)
|
||||
|
||||
def load(self, param_file):
|
||||
"""Load new values from file"""
|
||||
with open(param_file, 'r') as f:
|
||||
dict = yaml.safe_load(f)['body']
|
||||
self.params.update(dict)
|
||||
self.eval_dependencies() # Parameters have been updated
|
||||
|
||||
def load_from_dict(self, in_dict):
|
||||
self.params.update(in_dict)
|
||||
self.eval_dependencies() # Parameters have been updated
|
||||
|
||||
# Processing
|
||||
def eval_dependencies(self, key=None):
|
||||
"""Evaluate dependent attributes, e.g. after a new value has been set
|
||||
|
||||
Define your dependent parameters in the overload of this function
|
||||
|
||||
* key -- the information on what field is being updated
|
||||
"""
|
||||
pass
|
||||
|
||||
# Save
|
||||
def save(self, path, name='body_measurements'):
|
||||
with open(Path(path) / f'{name}.yaml', 'w') as f:
|
||||
yaml.dump(
|
||||
{'body': self.params},
|
||||
f,
|
||||
default_flow_style=False
|
||||
)
|
||||
|
||||
|
||||
class DesignSampler:
|
||||
"""Base class for design parameters sampling """
|
||||
|
||||
def __init__(self, param_file='') -> None:
|
||||
|
||||
self.params = {}
|
||||
if param_file:
|
||||
self.load(param_file)
|
||||
|
||||
def load(self, param_file):
|
||||
"""Load new values from file"""
|
||||
with open(param_file, 'r') as f:
|
||||
dict = yaml.safe_load(f)['design']
|
||||
self.params.update(dict)
|
||||
|
||||
def default(self):
|
||||
return self.params
|
||||
|
||||
# ---- Randomization of values ----
|
||||
def randomize(self):
|
||||
"""Generate random values for the current design parameters"""
|
||||
|
||||
random_params = deepcopy(self.params)
|
||||
|
||||
# NOTE dealing with the nested dict
|
||||
self._randomize_subset(random_params, [])
|
||||
|
||||
return random_params
|
||||
|
||||
def _randomize_subset(self, random_params, path):
|
||||
|
||||
subset = nested_get(random_params, path) if path else random_params
|
||||
for key in subset:
|
||||
if 'v' in subset[key].keys():
|
||||
self._randomize_value(random_params, path + [key])
|
||||
else:
|
||||
self._randomize_subset(random_params, path + [key])
|
||||
|
||||
def _randomize_value(self, random_params, path):
|
||||
""" Randomize the value of one parameter
|
||||
Path is leading to the leaf of param dict. value.
|
||||
"""
|
||||
|
||||
range = nested_get(random_params, path + ['range'])
|
||||
p_type = nested_get(random_params, path + ['type'])
|
||||
|
||||
# Check Defaults
|
||||
try:
|
||||
def_prob = nested_get(random_params, path + ['default_prob'])
|
||||
except KeyError as e: # Default probability not given -> Sample uniformly
|
||||
def_prob = None
|
||||
|
||||
def_value = nested_get(self.params, path + ['v'])
|
||||
if self.__use_default(def_prob):
|
||||
new_val = def_value
|
||||
else:
|
||||
if 'select' in p_type or p_type == 'bool' or 'file' in p_type: # All discrete types
|
||||
if p_type == 'select_null' and None not in range:
|
||||
range.append(None)
|
||||
# Exclude default
|
||||
if def_prob is not None:
|
||||
range.remove(def_value)
|
||||
new_val = random.choice(range)
|
||||
elif p_type == 'int':
|
||||
new_val = self.__randint_exclude(range, None if def_prob is None else def_value)
|
||||
elif p_type == 'float':
|
||||
new_val = self.__uniform_exclude(range, None if def_prob is None else def_value)
|
||||
|
||||
nested_set(random_params, path + ['v'], new_val)
|
||||
|
||||
def __use_default(self, probability):
|
||||
if probability is None:
|
||||
return False
|
||||
return random.random() < probability
|
||||
|
||||
def __randint_exclude(self, range, exclude):
|
||||
rand_v = random.randint(*range)
|
||||
if exclude is not None and rand_v == exclude:
|
||||
return self.__randint_exclude(range, exclude)
|
||||
|
||||
return rand_v
|
||||
|
||||
def __uniform_exclude(self, range, exclude):
|
||||
rand_v = random.uniform(*range)
|
||||
if exclude is not None and close_enough(rand_v, exclude):
|
||||
return self.__uniform_exclude(range, exclude)
|
||||
|
||||
return rand_v
|
||||
164
pygarment/garmentcode/utils.py
Normal file
164
pygarment/garmentcode/utils.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from typing import TypeVar, Generic, Sequence, Callable
|
||||
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
from scipy.spatial.transform import Rotation
|
||||
import svgpathtools as svgpath
|
||||
|
||||
|
||||
|
||||
# proper inserstions by key with bicest module in python <3.10
|
||||
# https://stackoverflow.com/questions/27672494/how-to-use-bisect-insort-left-with-a-key
|
||||
|
||||
T = TypeVar('T')
|
||||
V = TypeVar('V')
|
||||
|
||||
|
||||
class KeyWrapper(Generic[T, V]):
|
||||
def __init__(self, iterable: Sequence[T], key: Callable[[T], V]):
|
||||
self.it = iterable
|
||||
self.key = key
|
||||
|
||||
def __getitem__(self, i: int) -> V:
|
||||
return self.key(self.it[i])
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.it)
|
||||
|
||||
|
||||
def vector_angle(v1, v2):
|
||||
"""Find an angle between two 2D vectors"""
|
||||
v1, v2 = np.asarray(v1), np.asarray(v2)
|
||||
cos = np.dot(v1, v2) / (norm(v1) * norm(v2))
|
||||
angle = np.arccos(cos)
|
||||
# Cross to indicate correct relative orienataion of v2 w.r.t. v1
|
||||
cross = np.cross(v1, v2)
|
||||
|
||||
if abs(cross) > 1e-5:
|
||||
angle *= np.sign(cross)
|
||||
return angle
|
||||
|
||||
|
||||
def R2D(angle):
|
||||
"""2D rotation matrix by an angle"""
|
||||
return np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]])
|
||||
|
||||
|
||||
def vector_align_3D(v1, v2):
|
||||
"""Find a rotation to align v1 with v2"""
|
||||
|
||||
v1, v2 = np.asarray(v1), np.asarray(v2)
|
||||
cos = np.dot(v1, v2) / (norm(v1) * norm(v2))
|
||||
cos = max(min(cos, 1), -1) # NOTE: getting rid of numbers like 1.000002 that appear due to numerical instability
|
||||
|
||||
angle = np.arccos(cos)
|
||||
|
||||
# Cross to get the axis of rotation
|
||||
cross = np.cross(v1, v2)
|
||||
cross = cross / norm(cross)
|
||||
|
||||
return Rotation.from_rotvec(cross * angle)
|
||||
|
||||
|
||||
def close_enough(f1, f2=0, tol=1e-4):
|
||||
"""Compare two floats correctly """
|
||||
return abs(f1 - f2) < tol
|
||||
|
||||
|
||||
def bbox_paths(paths):
|
||||
"""Bounding box of a list of paths/Edge Sequences"""
|
||||
|
||||
bboxes = np.array([p.bbox() for p in paths])
|
||||
return (min(bboxes[:, 0]), max(bboxes[:, 1]),
|
||||
min(bboxes[:, 2]), max(bboxes[:, 3]))
|
||||
|
||||
|
||||
def lin_interpolation(val1, val2, factor):
|
||||
"""Linear interpolation between val1 and val2 with factor [0, 1]
|
||||
|
||||
with factor == 0, output is val1
|
||||
with factor == 1, output is val2
|
||||
"""
|
||||
if factor < 0 or factor > 1:
|
||||
raise ValueError(f'lin_interpolation::ERROR::Expected a factor \in [0, 1], got {factor}')
|
||||
|
||||
return (1 - factor) * val1 + factor * val2
|
||||
|
||||
|
||||
# ---- Complex numbers converters -----
|
||||
def c_to_list(num):
|
||||
"""Convert complex number to a list of 2 elements
|
||||
Allows processing of lists of complex numbers
|
||||
"""
|
||||
|
||||
if isinstance(num, (list, tuple, set, np.ndarray)):
|
||||
return [c_to_list(n) for n in num]
|
||||
else:
|
||||
return [num.real, num.imag]
|
||||
|
||||
|
||||
def c_to_np(num):
|
||||
"""Convert complex number to a numpy array of 2 elements
|
||||
Allows processing of lists of complex numbers
|
||||
"""
|
||||
if isinstance(num, (list, tuple, set, np.ndarray)):
|
||||
return np.asarray([c_to_list(n) for n in num])
|
||||
else:
|
||||
return np.asarray([num.real, num.imag])
|
||||
|
||||
|
||||
def list_to_c(num):
|
||||
"""Convert 2D list or list of 2D lists into complex number/list of complex
|
||||
numbers"""
|
||||
if isinstance(num[0], (list, tuple, set, np.ndarray)):
|
||||
return [complex(n[0], n[1]) for n in num]
|
||||
else:
|
||||
return complex(num[0], num[1])
|
||||
|
||||
|
||||
# ---- Nested Dictionaries shortcuts ----
|
||||
# https://stackoverflow.com/a/37704379
|
||||
def nested_get(dic, keys):
|
||||
for key in keys:
|
||||
dic = dic[key]
|
||||
return dic
|
||||
|
||||
|
||||
def nested_set(dic, keys, value):
|
||||
for key in keys[:-1]:
|
||||
dic = dic.setdefault(key, {})
|
||||
dic[keys[-1]] = value
|
||||
|
||||
|
||||
def nested_del(dic, keys):
|
||||
for key in keys[:-1]:
|
||||
dic = dic[key]
|
||||
del dic[keys[-1]]
|
||||
|
||||
|
||||
# ----- Curves -----
|
||||
def curve_extreme_points(curve, on_x=False, on_y=True):
|
||||
"""Return extreme points 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
|
||||
poly = svgpath.bezier2polynomial(curve, return_poly1d=True)
|
||||
|
||||
x_extremizers, y_extremizers = [], []
|
||||
if on_y:
|
||||
y = svgpath.imag(poly)
|
||||
dy = y.deriv()
|
||||
|
||||
y_extremizers = svgpath.polyroots(dy, realroots=True,
|
||||
condition=lambda r: 0 < r < 1)
|
||||
if on_x:
|
||||
x = svgpath.real(poly)
|
||||
dx = x.deriv()
|
||||
x_extremizers = svgpath.polyroots(dx, realroots=True,
|
||||
condition=lambda r: 0 < r < 1)
|
||||
all_extremizers = x_extremizers + y_extremizers
|
||||
|
||||
extreme_points = np.array([c_to_list(curve.point(t))
|
||||
for t in all_extremizers])
|
||||
|
||||
return extreme_points
|
||||
Reference in New Issue
Block a user