From fcc82cde0c6e40375668882d98209c61ea922414 Mon Sep 17 00:00:00 2001 From: quoccuongLE Date: Fri, 7 Feb 2025 10:36:06 +0100 Subject: [PATCH 1/8] feat: Update gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 645d0f1e..83116491 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,6 @@ Logs* output* # Nicegui -.nicegui \ No newline at end of file +.nicegui +.venv/ +assets/bodies/drippy/ \ No newline at end of file From 178cd9a5eb0bb7c5c65e55901796ae632ef1466d Mon Sep 17 00:00:00 2001 From: quoccuongLE Date: Fri, 7 Feb 2025 10:58:11 +0100 Subject: [PATCH 2/8] feat: Add registry --- pygarment/registry.py | 101 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 pygarment/registry.py diff --git a/pygarment/registry.py b/pygarment/registry.py new file mode 100644 index 00000000..0c3a9122 --- /dev/null +++ b/pygarment/registry.py @@ -0,0 +1,101 @@ +# Copyright 2022 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Registry utility.""" + + +def register(registered_collection, reg_key): + """Register decorated function or class to collection. + + Register decorated function or class into registered_collection, in a + hierarchical order. For example, when reg_key="my_model/my_exp/my_config_0" + the decorated function or class is stored under + registered_collection["my_model"]["my_exp"]["my_config_0"]. + This decorator is supposed to be used together with the lookup() function in + this file. + + Args: + registered_collection: a dictionary. The decorated function or class will be + put into this collection. + reg_key: The key for retrieving the registered function or class. If reg_key + is a string, it can be hierarchical like my_model/my_exp/my_config_0 + Returns: + A decorator function + Raises: + KeyError: when function or class to register already exists. + """ + def decorator(fn_or_cls): + """Put fn_or_cls in the dictionary.""" + if isinstance(reg_key, str): + hierarchy = reg_key.split("/") + collection = registered_collection + for h_idx, entry_name in enumerate(hierarchy[:-1]): + if entry_name not in collection: + collection[entry_name] = {} + collection = collection[entry_name] + if not isinstance(collection, dict): + raise KeyError( + "Collection path {} at position {} already registered as " + "a function or class.".format(entry_name, h_idx)) + leaf_reg_key = hierarchy[-1] + else: + collection = registered_collection + leaf_reg_key = reg_key + + if leaf_reg_key in collection: + raise KeyError("Function or class {} registered multiple times.".format( + leaf_reg_key)) + + collection[leaf_reg_key] = fn_or_cls + return fn_or_cls + return decorator + + +def lookup(registered_collection, reg_key): + """Lookup and return decorated function or class in the collection. + + Lookup decorated function or class in registered_collection, in a + hierarchical order. For example, when + reg_key="my_model/my_exp/my_config_0", + this function will return + registered_collection["my_model"]["my_exp"]["my_config_0"]. + + Args: + registered_collection: a dictionary. The decorated function or class will be + retrieved from this collection. + reg_key: The key for retrieving the registered function or class. If reg_key + is a string, it can be hierarchical like my_model/my_exp/my_config_0 + Returns: + The registered function or class. + Raises: + LookupError: when reg_key cannot be found. + """ + if isinstance(reg_key, str): + hierarchy = reg_key.split("/") + collection = registered_collection + for h_idx, entry_name in enumerate(hierarchy): + if entry_name not in collection: + raise LookupError( + f"collection path {entry_name} at position {h_idx} is never " + f"registered. Please make sure the {entry_name} and its library is " + "imported and linked to the trainer binary.") + collection = collection[entry_name] + return collection + else: + if reg_key not in registered_collection: + raise LookupError( + f"registration key {reg_key} is never " + f"registered. Please make sure the {reg_key} and its library is " + "imported and linked to the trainer binary.") + return registered_collection[reg_key] From 73b3b305cfdc3812c9d1014ed3e3f8965c2dd865 Mon Sep 17 00:00:00 2001 From: quoccuongLE Date: Sat, 15 Feb 2025 11:22:40 +0100 Subject: [PATCH 3/8] feat: moving files --- pygarment/__init__.py | 16 +- .../garmentcode/{ => component}/component.py | 15 +- pygarment/garmentcode/component/factory.py | 33 ++ pygarment/garmentcode/edge/circle_edge.py | 327 ++++++++++++++++ pygarment/garmentcode/{ => edge}/edge.py | 351 +++++------------- .../garmentcode/{ => edge}/edge_factory.py | 63 +++- pygarment/garmentcode/edge/factory.py | 33 ++ pygarment/garmentcode/interface.py | 2 +- .../garmentcode/{ => operators}/operators.py | 2 +- pygarment/garmentcode/panel/factory.py | 33 ++ pygarment/garmentcode/{ => panel}/panel.py | 14 +- 11 files changed, 590 insertions(+), 299 deletions(-) rename pygarment/garmentcode/{ => component}/component.py (94%) create mode 100644 pygarment/garmentcode/component/factory.py create mode 100644 pygarment/garmentcode/edge/circle_edge.py rename pygarment/garmentcode/{ => edge}/edge.py (72%) rename pygarment/garmentcode/{ => edge}/edge_factory.py (92%) create mode 100644 pygarment/garmentcode/edge/factory.py rename pygarment/garmentcode/{ => operators}/operators.py (99%) create mode 100644 pygarment/garmentcode/panel/factory.py rename pygarment/garmentcode/{ => panel}/panel.py (97%) diff --git a/pygarment/__init__.py b/pygarment/__init__.py index be011c71..92c4c5c1 100644 --- a/pygarment/__init__.py +++ b/pygarment/__init__.py @@ -3,19 +3,19 @@ """ # Building blocks -from pygarment.garmentcode.component import Component -from pygarment.garmentcode.panel import Panel -from pygarment.garmentcode.edge import Edge, CircleEdge, CurveEdge, EdgeSequence +from pygarment.garmentcode.component.component import Component +from pygarment.garmentcode.panel.panel import Panel +from pygarment.garmentcode.edge.edge import Edge, CircleEdge, CurveEdge, EdgeSequence from pygarment.garmentcode.connector import Stitches from pygarment.garmentcode.interface import Interface -from pygarment.garmentcode.edge_factory import EdgeSeqFactory -from pygarment.garmentcode.edge_factory import CircleEdgeFactory -from pygarment.garmentcode.edge_factory import EdgeFactory -from pygarment.garmentcode.edge_factory import CurveEdgeFactory +from pygarment.garmentcode.edge.edge_factory import EdgeSeqFactory +from pygarment.garmentcode.edge.edge_factory import CircleEdgeFactory +from pygarment.garmentcode.edge.edge_factory import EdgeFactory +from pygarment.garmentcode.edge.edge_factory import CurveEdgeFactory # Operations -import pygarment.garmentcode.operators as ops +import pygarment.garmentcode.operators.operators as ops import pygarment.garmentcode.utils as utils # Parameter support diff --git a/pygarment/garmentcode/component.py b/pygarment/garmentcode/component/component.py similarity index 94% rename from pygarment/garmentcode/component.py rename to pygarment/garmentcode/component/component.py index 1ea1cf78..2405a4ef 100644 --- a/pygarment/garmentcode/component.py +++ b/pygarment/garmentcode/component/component.py @@ -3,6 +3,7 @@ from pygarment.garmentcode.base import BaseComponent from pygarment.pattern.wrappers import VisPattern +from pygarment.garmentcode.component import factory class Component(BaseComponent): @@ -11,12 +12,12 @@ class Component(BaseComponent): # TODOLOW Overload copy -- respecting edge sequences -- never had any problems though - def __init__(self, name) -> None: + def __init__(self, name: str) -> None: super().__init__(name) self.subs = [] # list of generative subcomponents - def set_panel_label(self, label: str, overwrite=True): + def set_panel_label(self, label: str, overwrite: bool = True): """Propagate given label to all sub-panels (in subcomponents)""" subs = self._get_subcomponents() for sub in subs: @@ -46,7 +47,7 @@ def translate_by(self, delta_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() @@ -65,7 +66,7 @@ def rotate_by(self, delta_rotation: R): 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 @@ -112,7 +113,7 @@ def assembly(self): def bbox3D(self): """Evaluate 3D bounding box of the current component""" - + subs = self._get_subcomponents() bboxes = [s.bbox3D() for s in subs] @@ -145,3 +146,7 @@ def _get_subcomponents(self): for att in all_attrs if isinstance(att, BaseComponent)] + self.subs)) + +@factory.register_builder("component") +def build_component(name: str): + return Component(name=name) diff --git a/pygarment/garmentcode/component/factory.py b/pygarment/garmentcode/component/factory.py new file mode 100644 index 00000000..5250cc07 --- /dev/null +++ b/pygarment/garmentcode/component/factory.py @@ -0,0 +1,33 @@ +from pygarment import registry + +_REGISTERED_COMPONENT_CLS = {} +_REGISTERED_COMPONENT_CFG = {} + + +def register_builder(key: str): + """Decorates a builder. + + The builder should be a Callable (a class or a function). + Args: + key: A `str` of key to look up the builder. + + Returns: + A callable for using as class decorator that registers the decorated class + for creation from an instance of task_config_cls. + """ + return registry.register(_REGISTERED_COMPONENT_CLS, key) + + +def build(config: dict, name: str | None = None, **kwargs): + builder = registry.lookup(_REGISTERED_COMPONENT_CLS, name) + return builder(config=config, **kwargs) + + +def get_config(name: str): + """Looks up the `Config` according to the `name`.""" + cfg_creater = registry.lookup(_REGISTERED_COMPONENT_CFG, name) + return cfg_creater() + + +def register_config(key: str): + return registry.register(_REGISTERED_COMPONENT_CFG, key) diff --git a/pygarment/garmentcode/edge/circle_edge.py b/pygarment/garmentcode/edge/circle_edge.py new file mode 100644 index 00000000..61b6e069 --- /dev/null +++ b/pygarment/garmentcode/edge/circle_edge.py @@ -0,0 +1,327 @@ +import numpy as np +import svgpathtools as svgpath +from numpy.linalg import norm + +from pygarment.garmentcode.utils import (c_to_list, close_enough, list_to_c, + vector_angle) +from pygarment.pattern.utils import rel_to_abs_2d + +from .edge import Edge, EdgeSequence + + +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.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.0), 1.0)) + + 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 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.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 + ) diff --git a/pygarment/garmentcode/edge.py b/pygarment/garmentcode/edge/edge.py similarity index 72% rename from pygarment/garmentcode/edge.py rename to pygarment/garmentcode/edge/edge.py index 182a1471..df152f56 100644 --- a/pygarment/garmentcode/edge.py +++ b/pygarment/garmentcode/edge/edge.py @@ -4,11 +4,10 @@ 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.garmentcode.utils import R2D, close_enough, c_to_list from pygarment.pattern.utils import rel_to_abs_2d, abs_to_rel_2d +# from .edge_sequence import EdgeSequence # TODO: Circular import error + ILENGTH_S_TOL = 1e-10 # NOTE: tolerance value for evaluating curve parameter (t) from acr length @@ -258,201 +257,6 @@ def assembly(self): 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""" @@ -494,7 +298,7 @@ def __init__(self, start=None, end=None, control_points=None, def length(self): """Length of Bezier curve edge""" curve = self.as_curve() - + return curve.length() def __str__(self) -> str: @@ -505,20 +309,20 @@ def __str__(self) -> str: 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? + from pygarment.garmentcode.edge.edge_factory import EdgeFactory # TODOLOW: ami - better solution? curve = self.as_curve() # Sub-curves @@ -559,7 +363,7 @@ def reverse(self): 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""" @@ -567,7 +371,7 @@ def reflect_features(self): p[1] = -p[1] return self - + def as_curve(self, absolute=True): """As svgpath curve object @@ -581,7 +385,7 @@ def as_curve(self, absolute=True): 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) @@ -638,12 +442,13 @@ def assembly(self): } 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 + 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 @@ -660,7 +465,7 @@ def __getitem__(self, i): return self.edges[i] def index(self, elem): - # Find the same object (by reference) + # 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) @@ -674,8 +479,8 @@ def __contains__(self, item): return any([item is e for e in self.edges]) def __str__(self) -> str: - return 'EdgeSeq: ' + str(self.edges) - + return "EdgeSeq: " + str(self.edges) + def __repr__(self) -> str: return self.__str__() @@ -692,16 +497,18 @@ def isChained(self): return False for i in range(1, len(self.edges)): - if self.edges[i].start is not self.edges[i-1].end: + 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') + 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 + """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]) @@ -715,7 +522,9 @@ 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 + 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 @@ -724,11 +533,11 @@ def verts(self): 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 + + # NOTE May not reflect true shortcut if the egdes were flipped but + the order remained """ - return np.array([self[0].start, self[-1].end]) + return np.array([self[0].start, self[-1].end]) def bbox(self): """ @@ -749,7 +558,7 @@ def bbox(self): ma = verts_2d.max(axis=0) xs = [mi[0], ma[0]] ys = [mi[1], ma[1]] - #return points on bounding box + # return points on bounding box b_points = [] for v in verts_2d: if v[0] in xs or v[1] in ys: @@ -758,8 +567,8 @@ def bbox(self): 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]] + 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]] @@ -778,7 +587,9 @@ def append(self, item): 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)}') + raise ValueError( + f"{self.__class__.__name__}::ERROR::Trying to add object of incompatible type {type(item)}" + ) return self def insert(self, i, item): @@ -788,9 +599,11 @@ def insert(self, i, item): 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)') + 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) @@ -799,13 +612,13 @@ def pop(self, i): 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 + 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 + if orig < 0: + orig = len(self) + orig self.pop(orig) self.insert(orig, new) return self @@ -819,16 +632,14 @@ def reverse(self): # EdgeSequence-specific def translate_by(self, shift): - """Translate the edge seq vertices s.t. the first vertex is at new_origin - """ + """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 - """ + """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) @@ -847,18 +658,18 @@ def close_loop(self): def rotate(self, angle): """Rotate edge sequence by angle in place, using first point as a reference - Parameters: + 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) @@ -876,27 +687,31 @@ def extend(self, factor): # 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(): + 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') + print( + f"{self.__class__.__name__}::WARNING::Extending looped edge sequences is not available" + ) return self - else: + 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 + verts_projection[i] = (nverts_coords[i] - fixed).dot( + target_line + ) * target_line new_verts = verts_coords - (1 - factor) * verts_projection @@ -913,11 +728,13 @@ def reflect(self, v0, v1): 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] - ]) - + 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 @@ -930,10 +747,10 @@ def reflect(self, v0, v1): def propagate_label(self, label): """Propagate label to sub-edges - NOTE: Recommended to perform after all edge modification + 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 + # TODO Edge labels on cuts/reassemble in the """ for e in self.edges: e.label = label @@ -949,34 +766,36 @@ def copy(self): # 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[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, + """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 - 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: + 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)): + 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 diff --git a/pygarment/garmentcode/edge_factory.py b/pygarment/garmentcode/edge/edge_factory.py similarity index 92% rename from pygarment/garmentcode/edge_factory.py rename to pygarment/garmentcode/edge/edge_factory.py index dcd7e6dd..20fb6acd 100644 --- a/pygarment/garmentcode/edge_factory.py +++ b/pygarment/garmentcode/edge/edge_factory.py @@ -1,16 +1,15 @@ import numpy as np -from numpy.linalg import norm import svgpathtools as svgpath +from numpy.linalg import norm 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 +from pygarment.garmentcode.edge.edge import (CircleEdge, CurveEdge, Edge, + EdgeSequence) +from pygarment.garmentcode.utils import (bbox_paths, c_to_list, close_enough, + list_to_c, vector_angle) +from pygarment.pattern.utils import abs_to_rel_2d, rel_to_abs_2d + +from pygarment.garmentcode.edge import factory class EdgeFactory: @@ -40,6 +39,12 @@ def from_svg_curve(seg): return CurveEdge(start, end, cp, relative=False) + +@factory.register_builder("edge") +def build_edge(seg, type: str = "from_svg_curve"): + return EdgeFactory.from_svg_curve(seg) + + class CircleEdgeFactory: @staticmethod def from_points_angle(start, end, arc_angle, right=True): @@ -72,7 +77,7 @@ def from_points_radius(start, end, radius, large_arc=False, right=True): # Find circle center str_dist = norm(np.asarray(end) - np.asarray(start)) - # NOTE: close enough values may give negative + # 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. @@ -152,6 +157,16 @@ def from_three_points(start, end, point_on_arc, relative=False): start, end, radius=rad, large_arc=mid_dist > rad, right=angle > 0) + +@factory.register_builder("circle arc") +def build_circle_arc(method: str, **kwargs): + _build = getattr(CircleEdgeFactory, method) + if callable(_build): + return _build(**kwargs) + else: + raise ValueError(f"Method {method} does not exist in the CircleEdgeFactory") + + class CurveEdgeFactory: @staticmethod def curve_3_points(start, end, target, verbose=False): @@ -195,11 +210,11 @@ def curve_from_tangents(start, end, target_tan0=None, target_tan1=None, 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( @@ -217,6 +232,16 @@ def curve_from_tangents(start, end, target_tan0=None, target_tan1=None, return CurveEdge(start, end, control_points=[cp], relative=True) + +@factory.register_builder("curve edge") +def build_curve_edge(method: str, **kwargs): + _build = getattr(CurveEdgeFactory, method) + if callable(_build): + return _build(**kwargs) + else: + raise ValueError(f"Method {method} does not exist in the CurveEdgeFactory") + + class EdgeSeqFactory: """Create EdgeSequence objects for some common edge sequence patterns """ @@ -261,7 +286,7 @@ def from_verts(*verts, loop=False): if loop: seq.append(Edge(seq[-1].end, seq[0].start)) - + seq.isChained() # print warning if smth is wrong return seq @@ -285,7 +310,7 @@ def from_fractions(start, end, frac=None): verts[-1][1] + frac[i]*vec[1]] ) verts.append(end) - + return EdgeSeqFactory.from_verts(*verts) @staticmethod @@ -381,6 +406,16 @@ def halfs_from_svg(svg_filepath, target_height=None): return left_seqs, right_seqs + +@factory.register_builder("edge sequence") +def build_edge_seq(method: str, **kwargs): + _build = getattr(EdgeSeqFactory, method) + if callable(_build): + return _build(**kwargs) + else: + raise ValueError(f"Method {method} does not exist in the EdgeSeqFactory") + + # --- For Curves --- def _fit_pass_point(cp, target_location): """ Fit the control point of basic [[0, 0] -> [1, 0]] Quadratic Bezier s.t. diff --git a/pygarment/garmentcode/edge/factory.py b/pygarment/garmentcode/edge/factory.py new file mode 100644 index 00000000..48c76fa2 --- /dev/null +++ b/pygarment/garmentcode/edge/factory.py @@ -0,0 +1,33 @@ +from pygarment import registry + +_REGISTERED_EDGE_CLS = {} +_REGISTERED_EDGE_CFG = {} + + +def register_builder(key: str): + """Decorates a builder. + + The builder should be a Callable (a class or a function). + Args: + key: A `str` of key to look up the builder. + + Returns: + A callable for using as class decorator that registers the decorated class + for creation from an instance of task_config_cls. + """ + return registry.register(_REGISTERED_EDGE_CLS, key) + + +def build(config: dict, name: str | None = None, **kwargs): + builder = registry.lookup(_REGISTERED_EDGE_CLS, name) + return builder(config=config, **kwargs) + + +def get_config(name: str): + """Looks up the `Config` according to the `name`.""" + cfg_creater = registry.lookup(_REGISTERED_EDGE_CFG, name) + return cfg_creater() + + +def register_config(key: str): + return registry.register(_REGISTERED_EDGE_CFG, key) diff --git a/pygarment/garmentcode/interface.py b/pygarment/garmentcode/interface.py index 4745a27a..23a67468 100644 --- a/pygarment/garmentcode/interface.py +++ b/pygarment/garmentcode/interface.py @@ -3,7 +3,7 @@ from numpy.linalg import norm import numpy as np -from pygarment.garmentcode.edge import EdgeSequence, Edge +from pygarment.garmentcode.edge.edge import EdgeSequence, Edge from pygarment.garmentcode.utils import close_enough diff --git a/pygarment/garmentcode/operators.py b/pygarment/garmentcode/operators/operators.py similarity index 99% rename from pygarment/garmentcode/operators.py rename to pygarment/garmentcode/operators/operators.py index feb81d43..b98c8484 100644 --- a/pygarment/garmentcode/operators.py +++ b/pygarment/garmentcode/operators/operators.py @@ -7,7 +7,7 @@ from scipy.optimize import minimize import svgpathtools as svgpath -from pygarment.garmentcode.edge import Edge, CurveEdge, EdgeSequence, ILENGTH_S_TOL +from pygarment.garmentcode.edge.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 diff --git a/pygarment/garmentcode/panel/factory.py b/pygarment/garmentcode/panel/factory.py new file mode 100644 index 00000000..b7daa353 --- /dev/null +++ b/pygarment/garmentcode/panel/factory.py @@ -0,0 +1,33 @@ +from pygarment import registry + +_REGISTERED_PANEL_CLS = {} +_REGISTERED_PANEL_CFG = {} + + +def register_builder(key: str): + """Decorates a builder. + + The builder should be a Callable (a class or a function). + Args: + key: A `str` of key to look up the builder. + + Returns: + A callable for using as class decorator that registers the decorated class + for creation from an instance of task_config_cls. + """ + return registry.register(_REGISTERED_PANEL_CLS, key) + + +def build(config: dict, name: str | None = None, **kwargs): + builder = registry.lookup(_REGISTERED_PANEL_CLS, name) + return builder(config=config, **kwargs) + + +def get_config(name: str): + """Looks up the `Config` according to the `name`.""" + cfg_creater = registry.lookup(_REGISTERED_PANEL_CFG, name) + return cfg_creater() + + +def register_config(key: str): + return registry.register(_REGISTERED_PANEL_CFG, key) diff --git a/pygarment/garmentcode/panel.py b/pygarment/garmentcode/panel/panel.py similarity index 97% rename from pygarment/garmentcode/panel.py rename to pygarment/garmentcode/panel/panel.py index 75f8fd64..ad81a4e3 100644 --- a/pygarment/garmentcode/panel.py +++ b/pygarment/garmentcode/panel/panel.py @@ -5,10 +5,11 @@ from pygarment.pattern.core import BasicPattern from pygarment.garmentcode.base import BaseComponent -from pygarment.garmentcode.edge import Edge, EdgeSequence, CircleEdge +from pygarment.garmentcode.edge.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.operators.operators import cut_into_edge from pygarment.garmentcode.interface import Interface +from pygarment.garmentcode.panel import factory class Panel(BaseComponent): @@ -22,7 +23,7 @@ class Panel(BaseComponent): applications """ - def __init__(self, name, label='') -> None: + def __init__(self, name: str, label: str = "") -> 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) @@ -405,4 +406,9 @@ def bbox3D(self): 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) \ No newline at end of file + return verts_3d.min(axis=0), verts_3d.max(axis=0) + + +@factory.register_builder("panel") +def create_panel(name: str, label: str=""): + return Panel(name=name, label=label) From 668b7f95b1ffcfbdda1252145a9dedf614af364a Mon Sep 17 00:00:00 2001 From: quoccuongLE Date: Sat, 15 Feb 2025 22:19:08 +0100 Subject: [PATCH 4/8] feat: refacto assets --- assets/garment_programs/bands.py | 262 ---------- assets/garment_programs/bands/__init__.py | 17 + assets/garment_programs/bands/base.py | 301 +++++++++++ assets/garment_programs/bands/factory.py | 33 ++ assets/garment_programs/bodice.py | 491 ------------------ assets/garment_programs/bodice/__init__.py | 3 + .../bodice/bodice_halves/__init__.py | 3 + .../bodice/bodice_halves/bodice_halves.py | 311 +++++++++++ .../bodice/bodice_halves/bodice_panels.py | 186 +++++++ .../{ => bodice/bodice_halves}/tee.py | 6 +- assets/garment_programs/bodice/factory.py | 33 ++ assets/garment_programs/bodice/shirts.py | 93 ++++ assets/garment_programs/bottoms/__init__.py | 16 + .../{ => bottoms}/circle_skirt.py | 19 +- assets/garment_programs/bottoms/factory.py | 33 ++ .../garment_programs/{ => bottoms}/godet.py | 12 +- .../garment_programs/{ => bottoms}/pants.py | 28 +- .../{ => bottoms}/skirt_levels.py | 10 +- .../{ => bottoms}/skirt_paneled.py | 14 +- assets/garment_programs/collars.py | 370 ------------- assets/garment_programs/collars/__init__.py | 4 + .../collars/collar_halves/__init__.py | 19 + .../collars/collar_halves/base.py | 116 +++++ .../collars/collar_halves/factory.py | 33 ++ assets/garment_programs/collars/factory.py | 33 ++ .../collars/no_panels_collar.py | 44 ++ .../garment_programs/collars/panel_collars.py | 314 +++++++++++ assets/garment_programs/meta_garment.py | 18 +- assets/garment_programs/sleeves/__init__.py | 3 + .../sleeves/armhole_shapes/__init__.py | 3 + .../sleeves/armhole_shapes/base.py | 124 +++++ .../sleeves/armhole_shapes/factory.py | 33 ++ assets/garment_programs/sleeves/factory.py | 33 ++ .../garment_programs/{ => sleeves}/sleeves.py | 187 +++---- 34 files changed, 1909 insertions(+), 1296 deletions(-) delete mode 100644 assets/garment_programs/bands.py create mode 100644 assets/garment_programs/bands/__init__.py create mode 100644 assets/garment_programs/bands/base.py create mode 100644 assets/garment_programs/bands/factory.py delete mode 100644 assets/garment_programs/bodice.py create mode 100644 assets/garment_programs/bodice/__init__.py create mode 100644 assets/garment_programs/bodice/bodice_halves/__init__.py create mode 100644 assets/garment_programs/bodice/bodice_halves/bodice_halves.py create mode 100644 assets/garment_programs/bodice/bodice_halves/bodice_panels.py rename assets/garment_programs/{ => bodice/bodice_halves}/tee.py (96%) create mode 100644 assets/garment_programs/bodice/factory.py create mode 100644 assets/garment_programs/bodice/shirts.py create mode 100644 assets/garment_programs/bottoms/__init__.py rename assets/garment_programs/{ => bottoms}/circle_skirt.py (97%) create mode 100644 assets/garment_programs/bottoms/factory.py rename assets/garment_programs/{ => bottoms}/godet.py (92%) rename assets/garment_programs/{ => bottoms}/pants.py (94%) rename assets/garment_programs/{ => bottoms}/skirt_levels.py (90%) rename assets/garment_programs/{ => bottoms}/skirt_paneled.py (98%) delete mode 100644 assets/garment_programs/collars.py create mode 100644 assets/garment_programs/collars/__init__.py create mode 100644 assets/garment_programs/collars/collar_halves/__init__.py create mode 100644 assets/garment_programs/collars/collar_halves/base.py create mode 100644 assets/garment_programs/collars/collar_halves/factory.py create mode 100644 assets/garment_programs/collars/factory.py create mode 100644 assets/garment_programs/collars/no_panels_collar.py create mode 100644 assets/garment_programs/collars/panel_collars.py create mode 100644 assets/garment_programs/sleeves/__init__.py create mode 100644 assets/garment_programs/sleeves/armhole_shapes/__init__.py create mode 100644 assets/garment_programs/sleeves/armhole_shapes/base.py create mode 100644 assets/garment_programs/sleeves/armhole_shapes/factory.py create mode 100644 assets/garment_programs/sleeves/factory.py rename assets/garment_programs/{ => sleeves}/sleeves.py (68%) diff --git a/assets/garment_programs/bands.py b/assets/garment_programs/bands.py deleted file mode 100644 index b829457c..00000000 --- a/assets/garment_programs/bands.py +++ /dev/null @@ -1,262 +0,0 @@ -import pygarment as pyg -from assets.garment_programs.circle_skirt import CircleArcPanel -from assets.garment_programs import skirt_paneled -from assets.garment_programs.base_classes import BaseBand - -class StraightBandPanel(pyg.Panel): - """One panel for a panel skirt""" - - def __init__(self, name, width, depth, match_int_proportion=None) -> None: - super().__init__(name) - - # define edge loop - self.edges = pyg.EdgeSeqFactory.from_verts( - [0, 0], [0, depth], [width, depth], [width, 0], loop=True) - - # define interface - self.interfaces = { - 'right': pyg.Interface(self, self.edges[0]), - 'top': pyg.Interface(self, - self.edges[1], - ruffle=width / match_int_proportion if match_int_proportion is not None else 1. - ).reverse(True), - 'left': pyg.Interface(self, self.edges[2]), - 'bottom': pyg.Interface(self, - self.edges[3], - ruffle=width / match_int_proportion if match_int_proportion is not None else 1. - ) - } - - # Default translation - self.top_center_pivot() - self.center_x() - - -class StraightWB(BaseBand): - """Simple 2 panel waistband""" - def __init__(self, body, design, rise=1.) -> None: - """Simple 2 panel waistband - - * rise -- the rise value of the bottoms that the WB is attached to - Adapts the shape of the waistband to sit tight on top - of the given rise level (top measurement). If 1. or anything less than waistband width, - the rise is ignored and the StraightWB is created to sit well on the waist - - """ - super().__init__(body, design, rise=rise) - - # Measurements - self.waist = design['waistband']['waist']['v'] * body['waist'] - self.waist_back_frac = body['waist_back_width'] / body['waist'] - self.hips = body['hips'] * design['waistband']['waist']['v'] - self.hips_back_frac = body['hip_back_width'] / body['hips'] - - # Params - self.width = design['waistband']['width']['v'] - self.rise = rise - # Check correct values - if self.rise + self.width > 1: - self.rise = 1 - self.width - - self.top_width = pyg.utils.lin_interpolation( - self.hips, self.waist, self.rise + self.width) - self.top_back_fraction = pyg.utils.lin_interpolation( - self.hips_back_frac, self.waist_back_frac, self.rise + self.width) - - self.width = self.width * body['hips_line'] - - self.define_panels() - - self.front.translate_by([0, body['_waist_level'], 20]) - self.back.translate_by([0, body['_waist_level'], -15]) - - self.stitching_rules = pyg.Stitches( - (self.front.interfaces['right'], self.back.interfaces['right']), - (self.front.interfaces['left'], self.back.interfaces['left']) - ) - - self.interfaces = { - 'bottom_f': self.front.interfaces['bottom'], - 'bottom_b': self.back.interfaces['bottom'], - - 'top_f': self.front.interfaces['top'], - 'top_b': self.back.interfaces['top'], - - 'bottom': pyg.Interface.from_multiple( - self.front.interfaces['bottom'], - self.back.interfaces['bottom']), - 'top': pyg.Interface.from_multiple( - self.front.interfaces['top'], - self.back.interfaces['top']), - } - - def define_panels(self): - back_width = self.top_width * self.top_back_fraction - - self.front = StraightBandPanel( - 'wb_front', - self.top_width - back_width, - self.width, - match_int_proportion=self.body['waist'] - self.body['waist_back_width'] - ) - - self.back = StraightBandPanel( - 'wb_back', - back_width, - self.width, - match_int_proportion=self.body['waist_back_width'] - ) - - -class FittedWB(StraightWB): - """Also known as Yoke: a waistband that ~follows the body curvature, - and hence sits tight - Made out of two circular arc panels - """ - def __init__(self, body, design, rise=1.) -> None: - """A waistband that ~follows the body curvature, and hence sits tight - - * rise -- the rise value of the bottoms that the WB is attached to - Adapts the shape of the waistband to sit tight on top - of the given rise level. If 1. or anything less than waistband width, - the rise is ignored and the FittedWB is created to sit well on the waist - """ - self.bottom_width = None - self.bottom_back_fraction = None - super().__init__(body, design, rise) - - def define_panels(self): - self.bottom_width = pyg.utils.lin_interpolation( - self.hips, self.waist, self.rise) - self.bottom_back_fraction = pyg.utils.lin_interpolation( - self.hips_back_frac, self.waist_back_frac, self.rise) - - self.front = CircleArcPanel.from_all_length( - 'wb_front', - self.width, - self.top_width * (1 - self.top_back_fraction), - self.bottom_width * (1 - self.bottom_back_fraction), - match_top_int_proportion=self.body['waist'] - self.body['waist_back_width'], - match_bottom_int_proportion=self.body['waist'] - self.body['waist_back_width'] - ) - - self.back = CircleArcPanel.from_all_length( - 'wb_back', - self.width, - self.top_width * self.top_back_fraction, - self.bottom_width * self.bottom_back_fraction, - match_top_int_proportion=self.body['waist_back_width'], - match_bottom_int_proportion=self.body['waist_back_width'] - ) - - -class CuffBand(BaseBand): - """ Cuff class for sleeves or pants - band-like piece of fabric with optional "skirt" - """ - def __init__(self, tag, design, length=None) -> None: - super().__init__(body=None, design=design, tag=tag) - - self.design = design['cuff'] - - if length is None: - length = self.design['cuff_len']['v'] - - self.front = StraightBandPanel( - f'{tag}_cuff_f', self.design['b_width']['v'] / 2, length) - self.front.translate_by([0, 0, 15]) - self.back = StraightBandPanel( - f'{tag}_cuff_b', self.design['b_width']['v'] / 2, length) - self.back.translate_by([0, 0, -15]) - - self.stitching_rules = pyg.Stitches( - (self.front.interfaces['right'], self.back.interfaces['right']), - (self.front.interfaces['left'], self.back.interfaces['left']) - ) - - self.interfaces = { - 'bottom': pyg.Interface.from_multiple( - self.front.interfaces['bottom'], - self.back.interfaces['bottom']), - 'top_front': self.front.interfaces['top'], - 'top_back': self.back.interfaces['top'], - 'top': pyg.Interface.from_multiple( - self.front.interfaces['top'], - self.back.interfaces['top']), - } - - -class CuffSkirt(BaseBand): - """A skirt-like flared cuff """ - - def __init__(self, tag, design, length=None) -> None: - super().__init__(body=None, design=design, tag=tag) - - self.design = design['cuff'] - width = self.design['b_width']['v'] - flare_diff = (self.design['skirt_flare']['v'] - 1) * width / 2 - - if length is None: - length = self.design['cuff_len']['v'] - - self.front = skirt_paneled.SkirtPanel( - f'{tag}_cuff_skirt_f', ruffles=self.design['skirt_ruffle']['v'], - waist_length=width / 2, length=length, - flare=flare_diff) - self.front.translate_by([0, 0, 15]) - self.back = skirt_paneled.SkirtPanel( - f'{tag}_cuff_skirt_b', ruffles=self.design['skirt_ruffle']['v'], - waist_length=width / 2, length=length, - flare=flare_diff) - self.back.translate_by([0, 0, -15]) - - self.stitching_rules = pyg.Stitches( - (self.front.interfaces['right'], self.back.interfaces['right']), - (self.front.interfaces['left'], self.back.interfaces['left']) - ) - - self.interfaces = { - 'top': pyg.Interface.from_multiple( - self.front.interfaces['top'], self.back.interfaces['top']), - 'top_front': self.front.interfaces['top'], - 'top_back': self.back.interfaces['top'], - 'bottom': pyg.Interface.from_multiple( - self.front.interfaces['bottom'], - self.back.interfaces['bottom']), - } - - -class CuffBandSkirt(pyg.Component): - """ Cuff class for sleeves or pants - band-like piece of fabric with optional "skirt" - """ - def __init__(self, tag, design) -> None: - super().__init__(self.__class__.__name__) - - self.cuff = CuffBand( - tag, - design, - length=design['cuff']['cuff_len']['v'] * (1 - design['cuff']['skirt_fraction']['v']) - ) - self.skirt = CuffSkirt( - tag, - design, - length=design['cuff']['cuff_len']['v'] * design['cuff']['skirt_fraction']['v'] - ) - - # Align - self.skirt.place_below(self.cuff) - - self.stitching_rules = pyg.Stitches( - (self.cuff.interfaces['bottom'], self.skirt.interfaces['top']), - ) - - self.interfaces = { - 'top': self.cuff.interfaces['top'], - 'top_front': self.cuff.interfaces['top_front'], - 'top_back': self.cuff.interfaces['top_back'], - 'bottom': self.skirt.interfaces['bottom'] - } - - def length(self): - return self.cuff.length() + self.skirt.length() \ No newline at end of file diff --git a/assets/garment_programs/bands/__init__.py b/assets/garment_programs/bands/__init__.py new file mode 100644 index 00000000..d11963f7 --- /dev/null +++ b/assets/garment_programs/bands/__init__.py @@ -0,0 +1,17 @@ +from .base import ( + StraightBandPanel, + StraightWB, + FittedWB, + CuffBand, + CuffSkirt, + CuffBandSkirt, +) + +__all__ = [ + "StraightBandPanel", + "StraightWB", + "FittedWB", + "CuffBand", + "CuffSkirt", + "CuffBandSkirt", +] diff --git a/assets/garment_programs/bands/base.py b/assets/garment_programs/bands/base.py new file mode 100644 index 00000000..9ae924b7 --- /dev/null +++ b/assets/garment_programs/bands/base.py @@ -0,0 +1,301 @@ +import pygarment as pyg +from assets.garment_programs.bands import factory +from assets.garment_programs.base_classes import BaseBand +from assets.garment_programs.bottoms import skirt_paneled +from assets.garment_programs.bottoms.circle_skirt import CircleArcPanel + + +@factory.register_builder("StraightBandPanel") +class StraightBandPanel(pyg.Panel): + """One panel for a panel skirt""" + + def __init__( + self, string_name: str, width: float, depth: float, match_int_proportion: bool = None + ) -> None: + super().__init__(string_name) + + # define edge loop + self.edges = pyg.EdgeSeqFactory.from_verts( + [0, 0], [0, depth], [width, depth], [width, 0], loop=True + ) + + # define interface + self.interfaces = { + "right": pyg.Interface(self, self.edges[0]), + "top": pyg.Interface( + self, + self.edges[1], + ruffle=( + width / match_int_proportion + if match_int_proportion is not None + else 1.0 + ), + ).reverse(True), + "left": pyg.Interface(self, self.edges[2]), + "bottom": pyg.Interface( + self, + self.edges[3], + ruffle=( + width / match_int_proportion + if match_int_proportion is not None + else 1.0 + ), + ), + } + + # Default translation + self.top_center_pivot() + self.center_x() + + +@factory.register_builder("StraightWB") +class StraightWB(BaseBand): + """Simple 2 panel waistband""" + + def __init__(self, body: dict, design: dict, rise: float = 1.0) -> None: + """Simple 2 panel waistband + + * rise -- the rise value of the bottoms that the WB is attached to + Adapts the shape of the waistband to sit tight on top + of the given rise level (top measurement). If 1. or anything less than waistband width, + the rise is ignored and the StraightWB is created to sit well on the waist + + """ + super().__init__(body, design, rise=rise) + + # Measurements + self.waist = design["waistband"]["waist"]["v"] * body["waist"] + self.waist_back_frac = body["waist_back_width"] / body["waist"] + self.hips = body["hips"] * design["waistband"]["waist"]["v"] + self.hips_back_frac = body["hip_back_width"] / body["hips"] + + # Params + self.width = design["waistband"]["width"]["v"] + self.rise = rise + # Check correct values + if self.rise + self.width > 1: + self.rise = 1 - self.width + + self.top_width = pyg.utils.lin_interpolation( + self.hips, self.waist, self.rise + self.width + ) + self.top_back_fraction = pyg.utils.lin_interpolation( + self.hips_back_frac, self.waist_back_frac, self.rise + self.width + ) + + self.width = self.width * body["hips_line"] + + self.define_panels() + + self.front.translate_by([0, body["_waist_level"], 20]) + self.back.translate_by([0, body["_waist_level"], -15]) + + self.stitching_rules = pyg.Stitches( + (self.front.interfaces["right"], self.back.interfaces["right"]), + (self.front.interfaces["left"], self.back.interfaces["left"]), + ) + + self.interfaces = { + "bottom_f": self.front.interfaces["bottom"], + "bottom_b": self.back.interfaces["bottom"], + "top_f": self.front.interfaces["top"], + "top_b": self.back.interfaces["top"], + "bottom": pyg.Interface.from_multiple( + self.front.interfaces["bottom"], self.back.interfaces["bottom"] + ), + "top": pyg.Interface.from_multiple( + self.front.interfaces["top"], self.back.interfaces["top"] + ), + } + + def define_panels(self): + back_width = self.top_width * self.top_back_fraction + + self.front = StraightBandPanel( + "wb_front", + self.top_width - back_width, + self.width, + match_int_proportion=self.body["waist"] - self.body["waist_back_width"], + ) + + self.back = StraightBandPanel( + "wb_back", + back_width, + self.width, + match_int_proportion=self.body["waist_back_width"], + ) + + +@factory.register_builder("fitted straight waistband") +class FittedWB(StraightWB): + """Also known as Yoke: a waistband that ~follows the body curvature, + and hence sits tight + Made out of two circular arc panels + """ + + def __init__(self, body: dict, design: dict, rise: float = 1.0) -> None: + """A waistband that ~follows the body curvature, and hence sits tight + + * rise -- the rise value of the bottoms that the WB is attached to + Adapts the shape of the waistband to sit tight on top + of the given rise level. If 1. or anything less than waistband width, + the rise is ignored and the FittedWB is created to sit well on the waist + """ + self.bottom_width = None + self.bottom_back_fraction = None + super().__init__(body, design, rise) + + def define_panels(self): + self.bottom_width = pyg.utils.lin_interpolation( + self.hips, self.waist, self.rise + ) + self.bottom_back_fraction = pyg.utils.lin_interpolation( + self.hips_back_frac, self.waist_back_frac, self.rise + ) + + self.front = CircleArcPanel.from_all_length( + "wb_front", + self.width, + self.top_width * (1 - self.top_back_fraction), + self.bottom_width * (1 - self.bottom_back_fraction), + match_top_int_proportion=self.body["waist"] - self.body["waist_back_width"], + match_bottom_int_proportion=self.body["waist"] + - self.body["waist_back_width"], + ) + + self.back = CircleArcPanel.from_all_length( + "wb_back", + self.width, + self.top_width * self.top_back_fraction, + self.bottom_width * self.bottom_back_fraction, + match_top_int_proportion=self.body["waist_back_width"], + match_bottom_int_proportion=self.body["waist_back_width"], + ) + + +@factory.register_builder("CuffBand") +class CuffBand(BaseBand): + """Cuff class for sleeves or pants + band-like piece of fabric with optional "skirt" + """ + + def __init__(self, tag: str, design: dict, length: float = None) -> None: + super().__init__(body=None, design=design, tag=tag) + + self.design = design["cuff"] + + if length is None: + length = self.design["cuff_len"]["v"] + + self.front = StraightBandPanel( + f"{tag}_cuff_f", self.design["b_width"]["v"] / 2, length + ) + self.front.translate_by([0, 0, 15]) + self.back = StraightBandPanel( + f"{tag}_cuff_b", self.design["b_width"]["v"] / 2, length + ) + self.back.translate_by([0, 0, -15]) + + self.stitching_rules = pyg.Stitches( + (self.front.interfaces["right"], self.back.interfaces["right"]), + (self.front.interfaces["left"], self.back.interfaces["left"]), + ) + + self.interfaces = { + "bottom": pyg.Interface.from_multiple( + self.front.interfaces["bottom"], self.back.interfaces["bottom"] + ), + "top_front": self.front.interfaces["top"], + "top_back": self.back.interfaces["top"], + "top": pyg.Interface.from_multiple( + self.front.interfaces["top"], self.back.interfaces["top"] + ), + } + + +@factory.register_builder("CuffSkirt") +class CuffSkirt(BaseBand): + """A skirt-like flared cuff""" + + def __init__(self, tag: str, design: dict, length: float = None) -> None: + super().__init__(body=None, design=design, tag=tag) + + self.design = design["cuff"] + width = self.design["b_width"]["v"] + flare_diff = (self.design["skirt_flare"]["v"] - 1) * width / 2 + + if length is None: + length = self.design["cuff_len"]["v"] + + self.front = skirt_paneled.SkirtPanel( + f"{tag}_cuff_skirt_f", + ruffles=self.design["skirt_ruffle"]["v"], + waist_length=width / 2, + length=length, + flare=flare_diff, + ) + self.front.translate_by([0, 0, 15]) + self.back = skirt_paneled.SkirtPanel( + f"{tag}_cuff_skirt_b", + ruffles=self.design["skirt_ruffle"]["v"], + waist_length=width / 2, + length=length, + flare=flare_diff, + ) + self.back.translate_by([0, 0, -15]) + + self.stitching_rules = pyg.Stitches( + (self.front.interfaces["right"], self.back.interfaces["right"]), + (self.front.interfaces["left"], self.back.interfaces["left"]), + ) + + self.interfaces = { + "top": pyg.Interface.from_multiple( + self.front.interfaces["top"], self.back.interfaces["top"] + ), + "top_front": self.front.interfaces["top"], + "top_back": self.back.interfaces["top"], + "bottom": pyg.Interface.from_multiple( + self.front.interfaces["bottom"], self.back.interfaces["bottom"] + ), + } + + +@factory.register_builder("CuffBandSkirt") +class CuffBandSkirt(pyg.Component): + """Cuff class for sleeves or pants + band-like piece of fabric with optional "skirt" + """ + + def __init__(self, tag: str, design: dict) -> None: + super().__init__(self.__class__.__name__) + + self.cuff = CuffBand( + tag, + design, + length=design["cuff"]["cuff_len"]["v"] + * (1 - design["cuff"]["skirt_fraction"]["v"]), + ) + self.skirt = CuffSkirt( + tag, + design, + length=design["cuff"]["cuff_len"]["v"] + * design["cuff"]["skirt_fraction"]["v"], + ) + + # Align + self.skirt.place_below(self.cuff) + + self.stitching_rules = pyg.Stitches( + (self.cuff.interfaces["bottom"], self.skirt.interfaces["top"]), + ) + + self.interfaces = { + "top": self.cuff.interfaces["top"], + "top_front": self.cuff.interfaces["top_front"], + "top_back": self.cuff.interfaces["top_back"], + "bottom": self.skirt.interfaces["bottom"], + } + + def length(self): + return self.cuff.length() + self.skirt.length() diff --git a/assets/garment_programs/bands/factory.py b/assets/garment_programs/bands/factory.py new file mode 100644 index 00000000..290703f4 --- /dev/null +++ b/assets/garment_programs/bands/factory.py @@ -0,0 +1,33 @@ +from pygarment import registry + +_REGISTERED_BAND_CLS = {} +_REGISTERED_BAND_CFG = {} + + +def register_builder(key: str): + """Decorates a builder. + + The builder should be a Callable (a class or a function). + Args: + key: A `str` of key to look up the builder. + + Returns: + A callable for using as class decorator that registers the decorated class + for creation from an instance of task_config_cls. + """ + return registry.register(_REGISTERED_BAND_CLS, key) + + +def build(config: dict | None = None, name: str | None = None, **kwargs): + builder = registry.lookup(_REGISTERED_BAND_CLS, name) + return builder(config=config, **kwargs) + + +def get_config(name: str): + """Looks up the `Config` according to the `name`.""" + cfg_creater = registry.lookup(_REGISTERED_BAND_CFG, name) + return cfg_creater() + + +def register_config(key: str): + return registry.register(_REGISTERED_BAND_CFG, key) diff --git a/assets/garment_programs/bodice.py b/assets/garment_programs/bodice.py deleted file mode 100644 index 74480385..00000000 --- a/assets/garment_programs/bodice.py +++ /dev/null @@ -1,491 +0,0 @@ -from copy import deepcopy -import numpy as np - -import pygarment as pyg - - -from assets.garment_programs.base_classes import BaseBodicePanel -from assets.garment_programs import sleeves -from assets.garment_programs import collars -from assets.garment_programs import tee -from scipy.spatial.transform import Rotation as R - -class BodiceFrontHalf(BaseBodicePanel): - def __init__(self, name, body, design) -> None: - super().__init__(name, body, design) - - m_bust = body['bust'] - m_waist = body['waist'] - - # sizes - bust_point = body['bust_points'] / 2 - front_frac = (body['bust'] - body['back_width']) / 2 / body['bust'] - - self.width = front_frac * m_bust - waist = (m_waist - body['waist_back_width']) / 2 - sh_tan = np.tan(np.deg2rad(body['_shoulder_incl'])) - shoulder_incl = sh_tan * self.width - bottom_d_width = (self.width - waist) * 2 / 3 - - adjustment = sh_tan * (self.width - body['shoulder_w'] / 2) - max_len = body['waist_over_bust_line'] - adjustment - - # side length is adjusted due to shoulder inclination - # for the correct sleeve fitting - fb_diff = (front_frac - (0.5 - front_frac)) * body['bust'] - back_adjustment = sh_tan * (body['back_width'] / 2 - body['shoulder_w'] / 2) - side_len = body['waist_line'] - back_adjustment - sh_tan * fb_diff - - self.edges.append(pyg.EdgeSeqFactory.from_verts( - [0, 0], - [-self.width, 0], - [-self.width, max_len], - [0, max_len + shoulder_incl] - )) - self.edges.close_loop() - - # Side dart - bust_line = body['waist_line'] - body['_bust_line'] - side_d_depth = 0.75 * (self.width - bust_point) # NOTE: calculated value - side_d_width = max_len - side_len - s_edge, side_interface = self.add_dart( - pyg.EdgeSeqFactory.dart_shape(side_d_width, side_d_depth), - self.edges[1], - offset=bust_line + side_d_width / 2) - self.edges.substitute(1, s_edge) - - # Take some fabric from the top to match the shoulder width - s_edge[-1].end[0] += (x_upd:=self.width - body['shoulder_w'] / 2) - s_edge[-1].end[1] += (sh_tan * x_upd) - - # Bottom dart - b_edge, b_interface = self.add_dart( - pyg.EdgeSeqFactory.dart_shape(bottom_d_width, 0.9 * bust_line), - self.edges[0], - offset=bust_point + bottom_d_width / 2 - ) - self.edges.substitute(0, b_edge) - # Take some fabric from side in the bottom (!: after side dart insertion) - b_edge[-1].end[0] = - (waist + bottom_d_width) - - # Interfaces - self.interfaces = { - 'outside': pyg.Interface(self, side_interface), # side_interface, # pyp.Interface(self, [side_interface]), #, self.edges[-3]]), - 'inside': pyg.Interface(self, self.edges[-1]), - 'shoulder': pyg.Interface(self, self.edges[-2]), - 'bottom': pyg.Interface(self, b_interface), - - # Reference to the corner for sleeve and collar projections - 'shoulder_corner': pyg.Interface( - self, [self.edges[-3], self.edges[-2]]), - 'collar_corner': pyg.Interface( - self, [self.edges[-2], self.edges[-1]]) - } - - # default placement - self.translate_by([0, body['height'] - body['head_l'] - max_len - shoulder_incl, 0]) - - -class BodiceBackHalf(BaseBodicePanel): - """Panel for the back of basic fitted bodice block""" - - def __init__(self, name, body, design) -> None: - super().__init__(name, body, design) - - # Overall measurements - self.width = body['back_width'] / 2 - waist = body['waist_back_width'] / 2 - # NOTE: no inclination on the side, since there is not much to begin with - waist_width = self.width if waist < self.width else waist - shoulder_incl = (sh_tan:=np.tan(np.deg2rad(body['_shoulder_incl']))) * self.width - - # Adjust to make sure length is measured from the shoulder - # and not the de-fact side of the garment - back_adjustment = sh_tan * (self.width - body['shoulder_w'] / 2) - length = body['waist_line'] - back_adjustment - - # Base edge loop - edge_0 = pyg.CurveEdgeFactory.curve_from_tangents( - start=[0, shoulder_incl / 4], # back a little shorter - end=[-waist_width, 0], - target_tan0=[-1, 0] - ) - self.edges.append(edge_0) - self.edges.append(pyg.EdgeSeqFactory.from_verts( - edge_0.end, - [-self.width, body['waist_line'] - body['_bust_line']], # from the bottom - [-self.width, length], - [0, length + shoulder_incl], # Add some fabric for the neck (inclination of shoulders) - )) - self.edges.close_loop() - - # Take some fabric from the top to match the shoulder width - self.interfaces = { - 'outside': pyg.Interface( - self, [self.edges[1], self.edges[2]]), - 'inside': pyg.Interface(self, self.edges[-1]), - 'shoulder': pyg.Interface(self, self.edges[-2]), - 'bottom': pyg.Interface(self, self.edges[0]), - # Reference to the corners for sleeve and collar projections - 'shoulder_corner': pyg.Interface( - self, pyg.EdgeSequence(self.edges[-3], self.edges[-2])), - 'collar_corner': pyg.Interface( - self, pyg.EdgeSequence(self.edges[-2], self.edges[-1])) - } - - # Bottom dart as cutout -- for straight line - if waist < self.get_width(self.edges[2].end[1] - self.edges[2].start[1]): - w_diff = waist_width - waist - side_adj = 0 if w_diff < 4 else w_diff / 6 # NOTE: don't take from sides if the difference is too small - bottom_d_width = w_diff - side_adj - bottom_d_width /= 2 # double darts - bottom_d_depth = 1. * (length - body['_bust_line']) # calculated value - bottom_d_position = body['bum_points'] / 2 - - # TODOLOW Avoid hardcoding for matching with the bottoms? - dist = bottom_d_position * 0.5 # Dist between darts -> dist between centers - b_edge, b_interface = self.add_dart( - pyg.EdgeSeqFactory.dart_shape(bottom_d_width, 0.9 * bottom_d_depth), - self.edges[0], - offset=bottom_d_position + dist / 2 + bottom_d_width + bottom_d_width / 2, - ) - b_edge, b_interface = self.add_dart( - pyg.EdgeSeqFactory.dart_shape(bottom_d_width, bottom_d_depth), - b_edge[0], - offset=bottom_d_position - dist / 2 + bottom_d_width / 2, - edge_seq=b_edge, - int_edge_seq=b_interface, - ) - - self.edges.substitute(0, b_edge) - self.interfaces['bottom'] = pyg.Interface(self, b_interface) - - # Remove fabric from the sides if the diff is big enough - b_edge[-1].end[0] += side_adj - - # default placement - self.translate_by([0, body['height'] - body['head_l'] - length - shoulder_incl, 0]) - - def get_width(self, level): - return self.width - -class BodiceHalf(pyg.Component): - """Definition of a half of an upper garment with sleeves and collars""" - - def __init__(self, name, body, design, fitted=True) -> None: - super().__init__(name) - - design = deepcopy(design) # Recalculate freely! - - # Torso - if fitted: - self.ftorso = BodiceFrontHalf( - f'{name}_ftorso', body, design).translate_by([0, 0, 30]) - self.btorso = BodiceBackHalf( - f'{name}_btorso', body, design).translate_by([0, 0, -25]) - else: - self.ftorso = tee.TorsoFrontHalfPanel( - f'{name}_ftorso', body, design).translate_by([0, 0, 30]) - self.btorso = tee.TorsoBackHalfPanel( - f'{name}_btorso', body, design).translate_by([0, 0, -25]) - - # Interfaces - self.interfaces.update({ - 'f_bottom': self.ftorso.interfaces['bottom'], - 'b_bottom': self.btorso.interfaces['bottom'], - 'front_in': self.ftorso.interfaces['inside'], - 'back_in': self.btorso.interfaces['inside'] - }) - - # Sleeves/collar cuts - self.sleeve = None - self.collar_comp = None - self.eval_dep_params(body, design) - - if design['shirt']['strapless']['v'] and fitted: # NOTE: Strapless design only for fitted tops - self.make_strapless(body, design) - else: - # Sleeves and collars - self.add_sleeves(name, body, design) - self.add_collars(name, body, design) - self.stitching_rules.append(( - self.ftorso.interfaces['shoulder'], - self.btorso.interfaces['shoulder'] - )) # tops - - # Main connectivity - self.stitching_rules.append(( - self.ftorso.interfaces['outside'], self.btorso.interfaces['outside'])) # sides - - def eval_dep_params(self, body, design): - - # Sleeves - # NOTE assuming the vertical side is the first argument - max_cwidth = self.ftorso.interfaces['shoulder_corner'].edges[0].length() - 1 # cm - min_cwidth = body['_armscye_depth'] - v = design['sleeve']['connecting_width']['v'] - design['sleeve']['connecting_width']['v'] = min(min_cwidth + min_cwidth * v, max_cwidth) - - # Collars - # NOTE: Assuming the first is the top edge - # Width - # TODOLOW What if sleeve inclination is variable? - # NOTE: Back panel is more narrow, so using it - max_w = body['_base_sleeve_balance'] - 2 # 1 cm from default sleeve - min_w = body['neck_w'] - - if design['collar']['width']['v'] >= 0: - design['collar']['width']['v'] = width = pyg.utils.lin_interpolation(min_w, max_w, design['collar']['width']['v']) - else: - design['collar']['width']['v'] = width = pyg.utils.lin_interpolation(0, min_w, 1 + design['collar']['width']['v']) - - # Depth - # Collar depth is given w.r.t. length. - # adjust for the shoulder inclination - tg = np.tan(np.deg2rad(body['_shoulder_incl'])) - f_depth_adj = tg * (self.ftorso.get_width(0) - width / 2) - b_depth_adj = tg * (self.btorso.get_width(0) - width / 2) - - max_f_len = self.ftorso.interfaces['collar_corner'].edges[1].length() - tg * self.ftorso.get_width(0) - 1 # cm - max_b_len = self.btorso.interfaces['collar_corner'].edges[1].length() - tg * self.btorso.get_width(0) - 1 # cm - - design['collar']['f_strapless_depth'] = {} - design['collar']['f_strapless_depth']['v'] = min( - design['collar']['fc_depth']['v'] * body['_bust_line'], - max_f_len) - design['collar']['fc_depth']['v'] = design['collar']['f_strapless_depth']['v'] + f_depth_adj - - - design['collar']['b_strapless_depth'] = {} - design['collar']['b_strapless_depth']['v'] = min( - design['collar']['bc_depth']['v'] * body['_bust_line'], - max_b_len) - design['collar']['bc_depth']['v'] = design['collar']['b_strapless_depth']['v'] + b_depth_adj - - def add_sleeves(self, name, body, design): - self.sleeve = sleeves.Sleeve( - name, body, design, - front_w=self.ftorso.get_width, - back_w=self.btorso.get_width - ) - - _, f_sleeve_int = pyg.ops.cut_corner( - self.sleeve.interfaces['in_front_shape'].edges, - self.ftorso.interfaces['shoulder_corner'], - verbose=self.verbose - ) - _, b_sleeve_int = pyg.ops.cut_corner( - self.sleeve.interfaces['in_back_shape'].edges, - self.btorso.interfaces['shoulder_corner'], - verbose=self.verbose - ) - - if not design['sleeve']['sleeveless']['v']: - # Ordering - bodice_sleeve_int = pyg.Interface.from_multiple( - f_sleeve_int.reverse(with_edge_dir_reverse=True), - b_sleeve_int.reverse(), - ) - self.stitching_rules.append(( - self.sleeve.interfaces['in'], - bodice_sleeve_int - )) - - # NOTE: This is a heuristic tuned for arm poses 30 deg-60 deg - # used in the dataset - # FIXME Needs a better general solution - gap = -1 - body['arm_pose_angle'] / 10 - self.sleeve.place_by_interface( - self.sleeve.interfaces['in'], - bodice_sleeve_int, - gap=gap, - alignment='top', - ) - - # Add edge labels - f_sleeve_int.edges.propagate_label(f'{self.name}_armhole') - b_sleeve_int.edges.propagate_label(f'{self.name}_armhole') - - def add_collars(self, name, body, design): - # Front - collar_type = getattr( - collars, - str(design['collar']['component']['style']['v']), - collars.NoPanelsCollar - ) - - self.collar_comp = collar_type(name, body, design) - - # Project shape - _, fc_interface = pyg.ops.cut_corner( - self.collar_comp.interfaces['front_proj'].edges, - self.ftorso.interfaces['collar_corner'], - verbose=self.verbose - ) - _, bc_interface = pyg.ops.cut_corner( - self.collar_comp.interfaces['back_proj'].edges, - self.btorso.interfaces['collar_corner'], - verbose=self.verbose - ) - - # Add stitches/interfaces - if 'bottom' in self.collar_comp.interfaces: - self.stitching_rules.append(( - pyg.Interface.from_multiple(fc_interface, bc_interface), - self.collar_comp.interfaces['bottom'] - )) - - # Upd front interfaces accordingly - if 'front' in self.collar_comp.interfaces: - self.interfaces['front_collar'] = self.collar_comp.interfaces['front'] - self.interfaces['front_in'] = pyg.Interface.from_multiple( - self.ftorso.interfaces['inside'], self.interfaces['front_collar'] - ) - if 'back' in self.collar_comp.interfaces: - self.interfaces['back_collar'] = self.collar_comp.interfaces['back'] - self.interfaces['back_in'] = pyg.Interface.from_multiple( - self.btorso.interfaces['inside'], self.interfaces['back_collar'] - ) - - # Add edge labels - fc_interface.edges.propagate_label(f'{self.name}_collar') - bc_interface.edges.propagate_label(f'{self.name}_collar') - - def make_strapless(self, body, design): - - out_depth = design['sleeve']['connecting_width']['v'] - f_in_depth = design['collar']['f_strapless_depth']['v'] - b_in_depth = design['collar']['b_strapless_depth']['v'] - - # Shoulder adjustment for the back - # TODOLOW Shoulder adj evaluation should be a function - shoulder_angle = np.deg2rad(body['_shoulder_incl']) - sleeve_balance = body['_base_sleeve_balance'] / 2 - back_w = self.btorso.get_width(0) - shoulder_adj = np.tan(shoulder_angle) * (back_w - sleeve_balance) - out_depth -= shoulder_adj - - # Upd back - self._adjust_top_level(self.btorso, out_depth, b_in_depth) - - # Front depth determined by ~compensating for lenght difference - len_back = self.btorso.interfaces['outside'].edges.length() - len_front = self.ftorso.interfaces['outside'].edges.length() - self._adjust_top_level(self.ftorso, out_depth, f_in_depth, target_remove=(len_front - len_back)) - - # Placement - # NOTE: The commented line places the top a bit higher, increasing the chanced of correct drape - # Surcumvented by attachment constraint, so removed for nicer alignment in asymmetric garments - # self.translate_by([0, out_depth - body['_armscye_depth'] * 0.75, 0]) # adjust for better localisation - - # Add a label - self.ftorso.interfaces['shoulder'].edges.propagate_label('strapless_top') - self.btorso.interfaces['shoulder'].edges.propagate_label('strapless_top') - - - def _adjust_top_level(self, panel, out_level, in_level, target_remove=None): - """Crops the top of the bodice front/back panel for strapless style - - * out_length_diff -- if set, determined the length difference that should be compensates - after cutting the depth - """ - # TODOLOW Should this be the panel's function? - - panel_top = panel.interfaces['shoulder'].edges[0] - min_y = min(panel_top.start[1], panel_top.end[1]) - - # Order vertices - ins, out = panel_top.start, panel_top.end - if panel_top.start[1] < panel_top.end[1]: - ins, out = out, ins - - # Inside is a simple vertical line and can be adjusted by chaning Y value - ins[1] = min_y - in_level - - # Outside could be inclined, so needs further calculations - outside_edge = panel.interfaces['outside'].edges[-1] - bot, top = outside_edge.start, outside_edge.end - if bot is out: - bot, top = top, bot - - if target_remove is not None: - # Adjust the depth to remove this length exactly - angle_sin = abs(out[1] - bot[1]) / outside_edge.length() - curr_remove = out_level / angle_sin - length_diff = target_remove - curr_remove - adjustment = length_diff * angle_sin - out_level += adjustment - - angle_cotan = abs(out[0] - bot[0]) / abs(out[1] - bot[1]) - out[0] -= out_level * angle_cotan - out[1] = min_y - out_level - - - def length(self): - return self.btorso.length() - -class Shirt(pyg.Component): - """Panel for the front of upper garments with darts to properly fit it to - the shape""" - - def __init__(self, body, design, fitted=False) -> None: - name_with_params = f"{self.__class__.__name__}" - super().__init__(name_with_params) - - design = self.eval_dep_params(design) - - self.right = BodiceHalf(f'right', body, design, fitted=fitted) - self.left = BodiceHalf( - f'left', body, - design['left'] if design['left']['enable_asym']['v'] else design, - fitted=fitted).mirror() - - self.stitching_rules.append((self.right.interfaces['front_in'], - self.left.interfaces['front_in'])) - self.stitching_rules.append((self.right.interfaces['back_in'], - self.left.interfaces['back_in'])) - - # Adjust interface ordering for correct connectivity - self.interfaces = { # Bottom connection - 'bottom': pyg.Interface.from_multiple( - self.right.interfaces['f_bottom'].reverse(), - self.left.interfaces['f_bottom'], - self.left.interfaces['b_bottom'].reverse(), - self.right.interfaces['b_bottom'],) - } - - def eval_dep_params(self, design): - # NOTE: Support for full collars with partially strapless top - # or combination of paneled collar styles - # requres further development - # TODOLOW enable this one to work - if design['left']['enable_asym']['v']: - # Force no collars since they are not compatible with each other - design = deepcopy(design) - design['collar']['component']['style']['v'] = None - design['left']['collar']['component'] = dict(style=dict(v=None)) - - # Left-right design compatibility - design['left']['shirt'].update(length={}) - design['left']['shirt']['length']['v'] = design['shirt']['length']['v'] - - design['left']['collar'].update(fc_depth={}, bc_depth={}) - design['left']['collar']['fc_depth']['v'] = design['collar']['fc_depth']['v'] - design['left']['collar']['bc_depth']['v'] = design['collar']['bc_depth']['v'] - - return design - - def length(self): - return self.right.length() - -class FittedShirt(Shirt): - """Creates fitted shirt - - NOTE: Separate class is used for selection convenience. - Even though most of the processing is the same - (hence implemented with the same components except for panels), - design parametrization differs significantly. - With that, we decided to separate the top level names - """ - def __init__(self, body, design) -> None: - super().__init__(body, design, fitted=True) diff --git a/assets/garment_programs/bodice/__init__.py b/assets/garment_programs/bodice/__init__.py new file mode 100644 index 00000000..9108d60f --- /dev/null +++ b/assets/garment_programs/bodice/__init__.py @@ -0,0 +1,3 @@ +from .shirts import Shirt, FittedShirt + +__all__ = ["Shirt", "FittedShirt"] diff --git a/assets/garment_programs/bodice/bodice_halves/__init__.py b/assets/garment_programs/bodice/bodice_halves/__init__.py new file mode 100644 index 00000000..3269b460 --- /dev/null +++ b/assets/garment_programs/bodice/bodice_halves/__init__.py @@ -0,0 +1,3 @@ +from .bodice_halves import BodiceHalf +from .bodice_panels import BodiceFrontHalf, BodiceBackHalf +from .tee import TorsoFrontHalfPanel, TorsoBackHalfPanel \ No newline at end of file diff --git a/assets/garment_programs/bodice/bodice_halves/bodice_halves.py b/assets/garment_programs/bodice/bodice_halves/bodice_halves.py new file mode 100644 index 00000000..a9ffba9d --- /dev/null +++ b/assets/garment_programs/bodice/bodice_halves/bodice_halves.py @@ -0,0 +1,311 @@ +from copy import deepcopy + +import numpy as np + +from assets.garment_programs.sleeves import sleeves +import pygarment as pyg +from assets.garment_programs.bodice.bodice_halves.tee import ( + TorsoFrontHalfPanel, + TorsoBackHalfPanel, +) +from assets.garment_programs.bodice.bodice_halves.bodice_panels import ( + BodiceBackHalf, + BodiceFrontHalf, +) +# from assets.garment_programs.collars.collar_halves import base as collars +from assets.garment_programs.collars import factory as collar_factory + + +class BodiceHalf(pyg.Component): + """Definition of a half of an upper garment with sleeves and collars""" + + def __init__( + self, name: str, body: dict, design: dict, fitted: bool = True + ) -> None: + super().__init__(name) + + design = deepcopy(design) # Recalculate freely! + + # Torso + if fitted: + self.ftorso = BodiceFrontHalf(f"{name}_ftorso", body, design).translate_by( + [0, 0, 30] + ) + self.btorso = BodiceBackHalf(f"{name}_btorso", body, design).translate_by( + [0, 0, -25] + ) + else: + self.ftorso = TorsoFrontHalfPanel( + f"{name}_ftorso", body, design + ).translate_by([0, 0, 30]) + self.btorso = TorsoBackHalfPanel( + f"{name}_btorso", body, design + ).translate_by([0, 0, -25]) + + # Interfaces + self.interfaces.update( + { + "f_bottom": self.ftorso.interfaces["bottom"], + "b_bottom": self.btorso.interfaces["bottom"], + "front_in": self.ftorso.interfaces["inside"], + "back_in": self.btorso.interfaces["inside"], + } + ) + + # Sleeves/collar cuts + self.sleeve = None + self.collar_comp = None + self.eval_dep_params(body, design) + + if ( + design["shirt"]["strapless"]["v"] and fitted + ): # NOTE: Strapless design only for fitted tops + self.make_strapless(body, design) + else: + # Sleeves and collars + self.add_sleeves(name, body, design) + self.add_collars(name, body, design) + self.stitching_rules.append( + (self.ftorso.interfaces["shoulder"], self.btorso.interfaces["shoulder"]) + ) # tops + + # Main connectivity + self.stitching_rules.append( + (self.ftorso.interfaces["outside"], self.btorso.interfaces["outside"]) + ) # sides + + def eval_dep_params(self, body: dict, design: dict): + + # Sleeves + # NOTE assuming the vertical side is the first argument + max_cwidth = ( + self.ftorso.interfaces["shoulder_corner"].edges[0].length() - 1 + ) # cm + min_cwidth = body["_armscye_depth"] + v = design["sleeve"]["connecting_width"]["v"] + design["sleeve"]["connecting_width"]["v"] = min( + min_cwidth + min_cwidth * v, max_cwidth + ) + + # Collars + # NOTE: Assuming the first is the top edge + # Width + # TODOLOW What if sleeve inclination is variable? + # NOTE: Back panel is more narrow, so using it + max_w = body["_base_sleeve_balance"] - 2 # 1 cm from default sleeve + min_w = body["neck_w"] + + if design["collar"]["width"]["v"] >= 0: + design["collar"]["width"]["v"] = width = pyg.utils.lin_interpolation( + min_w, max_w, design["collar"]["width"]["v"] + ) + else: + design["collar"]["width"]["v"] = width = pyg.utils.lin_interpolation( + 0, min_w, 1 + design["collar"]["width"]["v"] + ) + + # Depth + # Collar depth is given w.r.t. length. + # adjust for the shoulder inclination + tg = np.tan(np.deg2rad(body["_shoulder_incl"])) + f_depth_adj = tg * (self.ftorso.get_width(0) - width / 2) + b_depth_adj = tg * (self.btorso.get_width(0) - width / 2) + + max_f_len = ( + self.ftorso.interfaces["collar_corner"].edges[1].length() + - tg * self.ftorso.get_width(0) + - 1 + ) # cm + max_b_len = ( + self.btorso.interfaces["collar_corner"].edges[1].length() + - tg * self.btorso.get_width(0) + - 1 + ) # cm + + design["collar"]["f_strapless_depth"] = {} + design["collar"]["f_strapless_depth"]["v"] = min( + design["collar"]["fc_depth"]["v"] * body["_bust_line"], max_f_len + ) + design["collar"]["fc_depth"]["v"] = ( + design["collar"]["f_strapless_depth"]["v"] + f_depth_adj + ) + + design["collar"]["b_strapless_depth"] = {} + design["collar"]["b_strapless_depth"]["v"] = min( + design["collar"]["bc_depth"]["v"] * body["_bust_line"], max_b_len + ) + design["collar"]["bc_depth"]["v"] = ( + design["collar"]["b_strapless_depth"]["v"] + b_depth_adj + ) + + def add_sleeves(self, name: str, body: dict, design: dict): + self.sleeve = sleeves.Sleeve( + name, + body, + design, + front_w=self.ftorso.get_width, + back_w=self.btorso.get_width, + ) + + _, f_sleeve_int = pyg.ops.cut_corner( + self.sleeve.interfaces["in_front_shape"].edges, + self.ftorso.interfaces["shoulder_corner"], + verbose=self.verbose, + ) + _, b_sleeve_int = pyg.ops.cut_corner( + self.sleeve.interfaces["in_back_shape"].edges, + self.btorso.interfaces["shoulder_corner"], + verbose=self.verbose, + ) + + if not design["sleeve"]["sleeveless"]["v"]: + # Ordering + bodice_sleeve_int = pyg.Interface.from_multiple( + f_sleeve_int.reverse(with_edge_dir_reverse=True), + b_sleeve_int.reverse(), + ) + self.stitching_rules.append( + (self.sleeve.interfaces["in"], bodice_sleeve_int) + ) + + # NOTE: This is a heuristic tuned for arm poses 30 deg-60 deg + # used in the dataset + # FIXME Needs a better general solution + gap = -1 - body["arm_pose_angle"] / 10 + self.sleeve.place_by_interface( + self.sleeve.interfaces["in"], + bodice_sleeve_int, + gap=gap, + alignment="top", + ) + + # Add edge labels + f_sleeve_int.edges.propagate_label(f"{self.name}_armhole") + b_sleeve_int.edges.propagate_label(f"{self.name}_armhole") + + def add_collars(self, name: str, body: dict, design: dict): + # Front + # collar_type = getattr( + # collars, + # str(design["collar"]["component"]["style"]["v"]), + # default=collars.NoPanelsCollar, + # ) + # self.collar_comp = collar_type(name, body, design) + if design["collar"]["component"]["style"]["v"] in collar_factory._REGISTERED_COLLAR_CLS: + collar_type = design["collar"]["component"]["style"]["v"] + else: + collar_type = "NoPanelsCollar" + + self.collar_comp = collar_factory.build( + name=collar_type, string_name=name, body=body, design=design + ) + + # Project shape + _, fc_interface = pyg.ops.cut_corner( + self.collar_comp.interfaces["front_proj"].edges, + self.ftorso.interfaces["collar_corner"], + verbose=self.verbose, + ) + _, bc_interface = pyg.ops.cut_corner( + self.collar_comp.interfaces["back_proj"].edges, + self.btorso.interfaces["collar_corner"], + verbose=self.verbose, + ) + + # Add stitches/interfaces + if "bottom" in self.collar_comp.interfaces: + self.stitching_rules.append( + ( + pyg.Interface.from_multiple(fc_interface, bc_interface), + self.collar_comp.interfaces["bottom"], + ) + ) + + # Upd front interfaces accordingly + if "front" in self.collar_comp.interfaces: + self.interfaces["front_collar"] = self.collar_comp.interfaces["front"] + self.interfaces["front_in"] = pyg.Interface.from_multiple( + self.ftorso.interfaces["inside"], self.interfaces["front_collar"] + ) + if "back" in self.collar_comp.interfaces: + self.interfaces["back_collar"] = self.collar_comp.interfaces["back"] + self.interfaces["back_in"] = pyg.Interface.from_multiple( + self.btorso.interfaces["inside"], self.interfaces["back_collar"] + ) + + # Add edge labels + fc_interface.edges.propagate_label(f"{self.name}_collar") + bc_interface.edges.propagate_label(f"{self.name}_collar") + + def make_strapless(self, body: dict, design: dict): + + out_depth = design["sleeve"]["connecting_width"]["v"] + f_in_depth = design["collar"]["f_strapless_depth"]["v"] + b_in_depth = design["collar"]["b_strapless_depth"]["v"] + + # Shoulder adjustment for the back + # TODOLOW Shoulder adj evaluation should be a function + shoulder_angle = np.deg2rad(body["_shoulder_incl"]) + sleeve_balance = body["_base_sleeve_balance"] / 2 + back_w = self.btorso.get_width(0) + shoulder_adj = np.tan(shoulder_angle) * (back_w - sleeve_balance) + out_depth -= shoulder_adj + + # Upd back + self._adjust_top_level(self.btorso, out_depth, b_in_depth) + + # Front depth determined by ~compensating for lenght difference + len_back = self.btorso.interfaces["outside"].edges.length() + len_front = self.ftorso.interfaces["outside"].edges.length() + self._adjust_top_level( + self.ftorso, out_depth, f_in_depth, target_remove=(len_front - len_back) + ) + + # Placement + # NOTE: The commented line places the top a bit higher, increasing the chanced of correct drape + # Surcumvented by attachment constraint, so removed for nicer alignment in asymmetric garments + # self.translate_by([0, out_depth - body['_armscye_depth'] * 0.75, 0]) # adjust for better localisation + + # Add a label + self.ftorso.interfaces["shoulder"].edges.propagate_label("strapless_top") + self.btorso.interfaces["shoulder"].edges.propagate_label("strapless_top") + + def _adjust_top_level(self, panel, out_level: float, in_level: float, target_remove: bool = None): + """Crops the top of the bodice front/back panel for strapless style + + * out_length_diff -- if set, determined the length difference that should be compensates + after cutting the depth + """ + # TODOLOW Should this be the panel's function? + + panel_top = panel.interfaces["shoulder"].edges[0] + min_y = min(panel_top.start[1], panel_top.end[1]) + + # Order vertices + ins, out = panel_top.start, panel_top.end + if panel_top.start[1] < panel_top.end[1]: + ins, out = out, ins + + # Inside is a simple vertical line and can be adjusted by chaning Y value + ins[1] = min_y - in_level + + # Outside could be inclined, so needs further calculations + outside_edge = panel.interfaces["outside"].edges[-1] + bot, top = outside_edge.start, outside_edge.end + if bot is out: + bot, top = top, bot + + if target_remove is not None: + # Adjust the depth to remove this length exactly + angle_sin = abs(out[1] - bot[1]) / outside_edge.length() + curr_remove = out_level / angle_sin + length_diff = target_remove - curr_remove + adjustment = length_diff * angle_sin + out_level += adjustment + + angle_cotan = abs(out[0] - bot[0]) / abs(out[1] - bot[1]) + out[0] -= out_level * angle_cotan + out[1] = min_y - out_level + + def length(self): + return self.btorso.length() diff --git a/assets/garment_programs/bodice/bodice_halves/bodice_panels.py b/assets/garment_programs/bodice/bodice_halves/bodice_panels.py new file mode 100644 index 00000000..e125b2e3 --- /dev/null +++ b/assets/garment_programs/bodice/bodice_halves/bodice_panels.py @@ -0,0 +1,186 @@ +import numpy as np + +import pygarment as pyg +from assets.garment_programs.base_classes import BaseBodicePanel + + +class BodiceFrontHalf(BaseBodicePanel): + def __init__(self, name: str, body: dict, design: dict) -> None: + super().__init__(name, body, design) + + m_bust = body["bust"] + m_waist = body["waist"] + + # sizes + bust_point = body["bust_points"] / 2 + front_frac = (body["bust"] - body["back_width"]) / 2 / body["bust"] + + self.width = front_frac * m_bust + waist = (m_waist - body["waist_back_width"]) / 2 + sh_tan = np.tan(np.deg2rad(body["_shoulder_incl"])) + shoulder_incl = sh_tan * self.width + bottom_d_width = (self.width - waist) * 2 / 3 + + adjustment = sh_tan * (self.width - body["shoulder_w"] / 2) + max_len = body["waist_over_bust_line"] - adjustment + + # side length is adjusted due to shoulder inclination + # for the correct sleeve fitting + fb_diff = (front_frac - (0.5 - front_frac)) * body["bust"] + back_adjustment = sh_tan * (body["back_width"] / 2 - body["shoulder_w"] / 2) + side_len = body["waist_line"] - back_adjustment - sh_tan * fb_diff + + self.edges.append( + pyg.EdgeSeqFactory.from_verts( + [0, 0], + [-self.width, 0], + [-self.width, max_len], + [0, max_len + shoulder_incl], + ) + ) + self.edges.close_loop() + + # Side dart + bust_line = body["waist_line"] - body["_bust_line"] + side_d_depth = 0.75 * (self.width - bust_point) # NOTE: calculated value + side_d_width = max_len - side_len + s_edge, side_interface = self.add_dart( + pyg.EdgeSeqFactory.dart_shape(side_d_width, side_d_depth), + self.edges[1], + offset=bust_line + side_d_width / 2, + ) + self.edges.substitute(1, s_edge) + + # Take some fabric from the top to match the shoulder width + s_edge[-1].end[0] += (x_upd := self.width - body["shoulder_w"] / 2) + s_edge[-1].end[1] += sh_tan * x_upd + + # Bottom dart + b_edge, b_interface = self.add_dart( + pyg.EdgeSeqFactory.dart_shape(bottom_d_width, 0.9 * bust_line), + self.edges[0], + offset=bust_point + bottom_d_width / 2, + ) + self.edges.substitute(0, b_edge) + # Take some fabric from side in the bottom (!: after side dart insertion) + b_edge[-1].end[0] = -(waist + bottom_d_width) + + # Interfaces + self.interfaces = { + "outside": pyg.Interface( + self, side_interface + ), # side_interface, # pyp.Interface(self, [side_interface]), #, self.edges[-3]]), + "inside": pyg.Interface(self, self.edges[-1]), + "shoulder": pyg.Interface(self, self.edges[-2]), + "bottom": pyg.Interface(self, b_interface), + # Reference to the corner for sleeve and collar projections + "shoulder_corner": pyg.Interface(self, [self.edges[-3], self.edges[-2]]), + "collar_corner": pyg.Interface(self, [self.edges[-2], self.edges[-1]]), + } + + # default placement + self.translate_by( + [0, body["height"] - body["head_l"] - max_len - shoulder_incl, 0] + ) + + +class BodiceBackHalf(BaseBodicePanel): + """Panel for the back of basic fitted bodice block""" + + def __init__(self, name: str, body: dict, design: dict) -> None: + super().__init__(name, body, design) + + # Overall measurements + self.width = body["back_width"] / 2 + waist = body["waist_back_width"] / 2 + # NOTE: no inclination on the side, since there is not much to begin with + waist_width = self.width if waist < self.width else waist + shoulder_incl = ( + sh_tan := np.tan(np.deg2rad(body["_shoulder_incl"])) + ) * self.width + + # Adjust to make sure length is measured from the shoulder + # and not the de-fact side of the garment + back_adjustment = sh_tan * (self.width - body["shoulder_w"] / 2) + length = body["waist_line"] - back_adjustment + + # Base edge loop + edge_0 = pyg.CurveEdgeFactory.curve_from_tangents( + start=[0, shoulder_incl / 4], # back a little shorter + end=[-waist_width, 0], + target_tan0=[-1, 0], + ) + self.edges.append(edge_0) + self.edges.append( + pyg.EdgeSeqFactory.from_verts( + edge_0.end, + [ + -self.width, + body["waist_line"] - body["_bust_line"], + ], # from the bottom + [-self.width, length], + [ + 0, + length + shoulder_incl, + ], # Add some fabric for the neck (inclination of shoulders) + ) + ) + self.edges.close_loop() + + # Take some fabric from the top to match the shoulder width + self.interfaces = { + "outside": pyg.Interface(self, [self.edges[1], self.edges[2]]), + "inside": pyg.Interface(self, self.edges[-1]), + "shoulder": pyg.Interface(self, self.edges[-2]), + "bottom": pyg.Interface(self, self.edges[0]), + # Reference to the corners for sleeve and collar projections + "shoulder_corner": pyg.Interface( + self, pyg.EdgeSequence(self.edges[-3], self.edges[-2]) + ), + "collar_corner": pyg.Interface( + self, pyg.EdgeSequence(self.edges[-2], self.edges[-1]) + ), + } + + # Bottom dart as cutout -- for straight line + if waist < self.get_width(self.edges[2].end[1] - self.edges[2].start[1]): + w_diff = waist_width - waist + side_adj = ( + 0 if w_diff < 4 else w_diff / 6 + ) # NOTE: don't take from sides if the difference is too small + bottom_d_width = w_diff - side_adj + bottom_d_width /= 2 # double darts + bottom_d_depth = 1.0 * (length - body["_bust_line"]) # calculated value + bottom_d_position = body["bum_points"] / 2 + + # TODOLOW Avoid hardcoding for matching with the bottoms? + dist = bottom_d_position * 0.5 # Dist between darts -> dist between centers + b_edge, b_interface = self.add_dart( + pyg.EdgeSeqFactory.dart_shape(bottom_d_width, 0.9 * bottom_d_depth), + self.edges[0], + offset=bottom_d_position + + dist / 2 + + bottom_d_width + + bottom_d_width / 2, + ) + b_edge, b_interface = self.add_dart( + pyg.EdgeSeqFactory.dart_shape(bottom_d_width, bottom_d_depth), + b_edge[0], + offset=bottom_d_position - dist / 2 + bottom_d_width / 2, + edge_seq=b_edge, + int_edge_seq=b_interface, + ) + + self.edges.substitute(0, b_edge) + self.interfaces["bottom"] = pyg.Interface(self, b_interface) + + # Remove fabric from the sides if the diff is big enough + b_edge[-1].end[0] += side_adj + + # default placement + self.translate_by( + [0, body["height"] - body["head_l"] - length - shoulder_incl, 0] + ) + + def get_width(self, level): + return self.width diff --git a/assets/garment_programs/tee.py b/assets/garment_programs/bodice/bodice_halves/tee.py similarity index 96% rename from assets/garment_programs/tee.py rename to assets/garment_programs/bodice/bodice_halves/tee.py index e3675a7d..7b0c83c7 100644 --- a/assets/garment_programs/tee.py +++ b/assets/garment_programs/bodice/bodice_halves/tee.py @@ -12,7 +12,7 @@ class TorsoFrontHalfPanel(BaseBodicePanel): Fits to the bust size """ - def __init__(self, name, body, design) -> None: + def __init__(self, name: str, body: dict, design: dict) -> None: """ Front = True, provides the adjustments necessary for the front panel """ super().__init__(name, body, design) @@ -70,7 +70,7 @@ class TorsoBackHalfPanel(BaseBodicePanel): Fits to the bust size """ - def __init__(self, name, body, design) -> None: + def __init__(self, name: str, body: dict, design: dict) -> None: """ Front = True, provides the adjustments necessary for the front panel """ super().__init__(name, body, design) @@ -80,7 +80,7 @@ def __init__(self, name, body, design) -> None: m_width = design['width']['v'] * body['bust'] b_width = design['flare']['v'] * m_width - # sizes + # sizes body_width = body['back_width'] / 2 frac = body_width / body['bust'] self.width = frac * m_width diff --git a/assets/garment_programs/bodice/factory.py b/assets/garment_programs/bodice/factory.py new file mode 100644 index 00000000..8ca51912 --- /dev/null +++ b/assets/garment_programs/bodice/factory.py @@ -0,0 +1,33 @@ +from pygarment import registry + +_REGISTERED_BODICE_CLS = {} +_REGISTERED_BODICE_CFG = {} + + +def register_builder(key: str): + """Decorates a builder. + + The builder should be a Callable (a class or a function). + Args: + key: A `str` of key to look up the builder. + + Returns: + A callable for using as class decorator that registers the decorated class + for creation from an instance of task_config_cls. + """ + return registry.register(_REGISTERED_BODICE_CLS, key) + + +def build(config: dict, name: str | None = None, **kwargs): + builder = registry.lookup(_REGISTERED_BODICE_CLS, name) + return builder(config=config, **kwargs) + + +def get_config(name: str): + """Looks up the `Config` according to the `name`.""" + cfg_creater = registry.lookup(_REGISTERED_BODICE_CFG, name) + return cfg_creater() + + +def register_config(key: str): + return registry.register(_REGISTERED_BODICE_CFG, key) diff --git a/assets/garment_programs/bodice/shirts.py b/assets/garment_programs/bodice/shirts.py new file mode 100644 index 00000000..8404cc22 --- /dev/null +++ b/assets/garment_programs/bodice/shirts.py @@ -0,0 +1,93 @@ +from copy import deepcopy + +import pygarment as pyg +from assets.garment_programs.bodice import factory +from assets.garment_programs.bodice.bodice_halves.bodice_halves import BodiceHalf + + +class Shirt(pyg.Component): + """Panel for the front of upper garments with darts to properly fit it to + the shape""" + + def __init__(self, body: dict, design: dict, fitted: bool = False) -> None: + name_with_params = f"{self.__class__.__name__}" + super().__init__(name_with_params) + + design = self.eval_dep_params(design) + + self.right = BodiceHalf(f"right", body, design, fitted=fitted) + self.left = BodiceHalf( + f"left", + body, + design["left"] if design["left"]["enable_asym"]["v"] else design, + fitted=fitted, + ).mirror() + + self.stitching_rules.append( + (self.right.interfaces["front_in"], self.left.interfaces["front_in"]) + ) + self.stitching_rules.append( + (self.right.interfaces["back_in"], self.left.interfaces["back_in"]) + ) + + # Adjust interface ordering for correct connectivity + self.interfaces = { # Bottom connection + "bottom": pyg.Interface.from_multiple( + self.right.interfaces["f_bottom"].reverse(), + self.left.interfaces["f_bottom"], + self.left.interfaces["b_bottom"].reverse(), + self.right.interfaces["b_bottom"], + ) + } + + def eval_dep_params(self, design: dict): + # NOTE: Support for full collars with partially strapless top + # or combination of paneled collar styles + # requres further development + # TODOLOW enable this one to work + if design["left"]["enable_asym"]["v"]: + # Force no collars since they are not compatible with each other + design = deepcopy(design) + design["collar"]["component"]["style"]["v"] = None + design["left"]["collar"]["component"] = dict(style=dict(v=None)) + + # Left-right design compatibility + design["left"]["shirt"].update(length={}) + design["left"]["shirt"]["length"]["v"] = design["shirt"]["length"]["v"] + + design["left"]["collar"].update(fc_depth={}, bc_depth={}) + design["left"]["collar"]["fc_depth"]["v"] = design["collar"]["fc_depth"][ + "v" + ] + design["left"]["collar"]["bc_depth"]["v"] = design["collar"]["bc_depth"][ + "v" + ] + + return design + + def length(self): + return self.right.length() + + +class FittedShirt(Shirt): + """Creates fitted shirt + + NOTE: Separate class is used for selection convenience. + Even though most of the processing is the same + (hence implemented with the same components except for panels), + design parametrization differs significantly. + With that, we decided to separate the top level names + """ + + def __init__(self, body, design) -> None: + super().__init__(body, design, fitted=True) + + +@factory.register_builder("shirt") +def build_shirt(body_config: dict, design_config: dict): + return Shirt(body=body_config, design=design_config, fitted=False) + + +@factory.register_builder("fitted shirt") +def build_shirt(body_config: dict, design_config: dict): + return Shirt(body=body_config, design=design_config, fitted=True) diff --git a/assets/garment_programs/bottoms/__init__.py b/assets/garment_programs/bottoms/__init__.py new file mode 100644 index 00000000..48d80541 --- /dev/null +++ b/assets/garment_programs/bottoms/__init__.py @@ -0,0 +1,16 @@ +from .circle_skirt import AsymmSkirtCircle, SkirtCircle +from .godet import GodetSkirt +from .pants import Pants +from .skirt_levels import SkirtLevels +from .skirt_paneled import PencilSkirt, Skirt2, SkirtManyPanels + +__all__ = [ + "SkirtCircle", + "AsymmSkirtCircle", + "GodetSkirt", + "Pants", + "SkirtLevels", + "PencilSkirt", + "Skirt2", + "SkirtManyPanels", +] diff --git a/assets/garment_programs/circle_skirt.py b/assets/garment_programs/bottoms/circle_skirt.py similarity index 97% rename from assets/garment_programs/circle_skirt.py rename to assets/garment_programs/bottoms/circle_skirt.py index b5ee02f4..50046476 100644 --- a/assets/garment_programs/circle_skirt.py +++ b/assets/garment_programs/bottoms/circle_skirt.py @@ -2,18 +2,19 @@ import pygarment as pyg from assets.garment_programs.base_classes import StackableSkirtComponent +from assets.garment_programs.bottoms import factory class CircleArcPanel(pyg.Panel): """One panel circle skirt""" def __init__(self, - name, + string_name, top_rad, length, angle, match_top_int_proportion=None, match_bottom_int_proportion=None ) -> None: - super().__init__(name) + super().__init__(string_name) halfarc = angle / 2 @@ -76,6 +77,7 @@ def from_length_rad(name, length, top_width, rad, **kwargs): return CircleArcPanel(name, rad, length, arc, **kwargs) + class AsymHalfCirclePanel(pyg.Panel): """Panel for a asymmetrci circle skirt""" @@ -99,7 +101,7 @@ def __init__(self, self.edges.append(pyg.Edge( self.edges[-1].end, [dist_out / 2, 0])) - + # Bottom self.edges.append( pyg.CircleEdgeFactory.from_three_points( @@ -121,10 +123,12 @@ def __init__(self, 'left': pyg.Interface(self, self.edges[1]), 'right': pyg.Interface(self, self.edges[3]) } - + def length(self, *args): return self.interfaces['right'].edges.length() + +@factory.register_builder("SkirtCircle") class SkirtCircle(StackableSkirtComponent): """Simple circle skirt""" def __init__(self, body, design, tag='', length=None, rise=None, slit=True, asymm=False, min_len=5, **kwargs) -> None: @@ -194,7 +198,7 @@ def __init__(self, body, design, tag='', length=None, rise=None, slit=True, asym 'bottom_b': self.back.interfaces['bottom'], 'bottom': pyg.Interface.from_multiple(self.front.interfaces['bottom'], self.back.interfaces['bottom']) } - + def add_cut(self, panel, design, sk_length): """Add a cut to the skirt""" width, depth = design['cut']['width']['v'] * sk_length, design['cut']['depth']['v'] * sk_length @@ -223,12 +227,13 @@ def add_cut(self, panel, design, sk_length): panel.interfaces['bottom'].substitute( target_edge, interf_edges, [panel for _ in range(len(interf_edges))]) - + def length(self, *args): return self.front.length() +@factory.register_builder("AsymmSkirtCircle") class AsymmSkirtCircle(SkirtCircle): """Front/back asymmetric skirt""" def __init__(self, body, design, tag='', length=None, rise=None, slit=True, **kwargs): - super().__init__(body, design, tag, length, rise, slit, asymm=True) \ No newline at end of file + super().__init__(body, design, tag, length, rise, slit, asymm=True) diff --git a/assets/garment_programs/bottoms/factory.py b/assets/garment_programs/bottoms/factory.py new file mode 100644 index 00000000..6288c4d7 --- /dev/null +++ b/assets/garment_programs/bottoms/factory.py @@ -0,0 +1,33 @@ +from pygarment import registry + +_REGISTERED_BOTTOM_CLS = {} +_REGISTERED_BOTTOM_CFG = {} + + +def register_builder(key: str): + """Decorates a builder. + + The builder should be a Callable (a class or a function). + Args: + key: A `str` of key to look up the builder. + + Returns: + A callable for using as class decorator that registers the decorated class + for creation from an instance of task_config_cls. + """ + return registry.register(_REGISTERED_BOTTOM_CLS, key) + + +def build(config: dict | None = None, name: str | None = None, **kwargs): + builder = registry.lookup(_REGISTERED_BOTTOM_CLS, name) + return builder(config=config, **kwargs) + + +def get_config(name: str): + """Looks up the `Config` according to the `name`.""" + cfg_creater = registry.lookup(_REGISTERED_BOTTOM_CFG, name) + return cfg_creater() + + +def register_config(key: str): + return registry.register(_REGISTERED_BOTTOM_CFG, key) diff --git a/assets/garment_programs/godet.py b/assets/garment_programs/bottoms/godet.py similarity index 92% rename from assets/garment_programs/godet.py rename to assets/garment_programs/bottoms/godet.py index d4f2fd69..a5e4cbc2 100644 --- a/assets/garment_programs/godet.py +++ b/assets/garment_programs/bottoms/godet.py @@ -1,14 +1,15 @@ import math + import numpy as np import pygarment as pyg - from assets.garment_programs.base_classes import BaseBottoms -from assets.garment_programs import skirt_paneled as skirts +from assets.garment_programs.bottoms import factory +from assets.garment_programs.bottoms import skirt_paneled as skirts class Insert(pyg.Panel): - def __init__(self, id, width=30, depth=30) -> None: + def __init__(self, id: int, width: float = 30, depth: float = 30) -> None: super().__init__(f'Insert_{id}') self.edges = pyg.EdgeSeqFactory.from_verts( @@ -23,9 +24,10 @@ def __init__(self, id, width=30, depth=30) -> None: self.center_x() +@factory.register_builder("GodetSkirt") class GodetSkirt(BaseBottoms): - def __init__(self, body, design, rise=None) -> None: + def __init__(self, body: dict, design: dict, rise: float | None = None) -> None: super().__init__(body, design, rise=rise) gdesign = design['godet-skirt'] @@ -118,4 +120,4 @@ def get_rise(self): return self.base.get_rise() def length(self): - return self.base.length() \ No newline at end of file + return self.base.length() diff --git a/assets/garment_programs/pants.py b/assets/garment_programs/bottoms/pants.py similarity index 94% rename from assets/garment_programs/pants.py rename to assets/garment_programs/bottoms/pants.py index b7225980..e67a17d7 100644 --- a/assets/garment_programs/pants.py +++ b/assets/garment_programs/bottoms/pants.py @@ -1,9 +1,11 @@ from copy import deepcopy + import numpy as np import pygarment as pyg +from assets.garment_programs.bands import factory as band_factory from assets.garment_programs.base_classes import BaseBottoms -from assets.garment_programs import bands +from assets.garment_programs.bottoms import factory class PantPanel(pyg.Panel): @@ -174,10 +176,10 @@ def add_darts(self, top, dart_width, dart_depth, dart_position, double_dart=Fals ) return top_edges, int_edges - + class PantsHalf(BaseBottoms): - def __init__(self, tag, body, design, rise=None) -> None: + def __init__(self, tag: str, body: dict, design: dict, rise: float = None) -> None: super().__init__(body, design, tag, rise=rise) design = design['pants'] self.rise = design['rise']['v'] if rise is None else rise @@ -198,8 +200,8 @@ def __init__(self, tag, body, design, rise=None) -> None: if length - cuff_len < design['length']['range'][0]: # Min length from paramss # Cannot be longer then a pant cuff_len = length - design['length']['range'][0] - # Include the cuff into the overall length, - # unless the requested length is too short to fit the cuff + # Include the cuff into the overall length, + # unless the requested length is too short to fit the cuff # (to avoid negative length) length -= cuff_len length *= body['_leg_length'] @@ -236,7 +238,7 @@ def __init__(self, tag, body, design, rise=None) -> None: # add a cuff # TODOLOW This process is the same for sleeves -- make a function? if design['cuff']['type']['v']: - + pant_bottom = pyg.Interface.from_multiple( self.front.interfaces['bottom'], self.back.interfaces['bottom']) @@ -248,8 +250,11 @@ def __init__(self, tag, body, design, rise=None) -> None: cdesign['cuff']['cuff_len']['v'] = cuff_len # Init - cuff_class = getattr(bands, cdesign['cuff']['type']['v']) - self.cuff = cuff_class(f'pant_{tag}', cdesign) + # cuff_class = getattr(bands, cdesign['cuff']['type']['v']) + # self.cuff = cuff_class(f'pant_{tag}', cdesign) + self.cuff = band_factory.build( + name=cdesign["cuff"]["type"]["v"], tag=f"pant_{tag}" + ) # Position self.cuff.place_by_interface( @@ -275,11 +280,13 @@ def __init__(self, tag, body, design, rise=None) -> None: def length(self): if self.design['pants']['cuff']['type']['v']: return self.front.length() + self.cuff.length() - + return self.front.length() + +@factory.register_builder("Pants") class Pants(BaseBottoms): - def __init__(self, body, design, rise=None) -> None: + def __init__(self, body: dict, design: dict, rise: float = None) -> None: super().__init__(body, design) self.right = PantsHalf('r', body, design, rise) @@ -309,4 +316,3 @@ def get_rise(self): def length(self): return self.right.length() - diff --git a/assets/garment_programs/skirt_levels.py b/assets/garment_programs/bottoms/skirt_levels.py similarity index 90% rename from assets/garment_programs/skirt_levels.py rename to assets/garment_programs/bottoms/skirt_levels.py index 4d3985fd..213466d1 100644 --- a/assets/garment_programs/skirt_levels.py +++ b/assets/garment_programs/bottoms/skirt_levels.py @@ -1,12 +1,15 @@ -from assets.garment_programs.circle_skirt import * -from assets.garment_programs.skirt_paneled import * from copy import deepcopy +from assets.garment_programs.bottoms import factory +from assets.garment_programs.bottoms.circle_skirt import * +from assets.garment_programs.bottoms.skirt_paneled import * + +@factory.register_builder("SkirtLevels") class SkirtLevels(BaseBottoms): """Skirt constiting of multuple stitched skirts""" - def __init__(self, body, design, rise=None) -> None: + def __init__(self, body: dict, design: dict, rise: float | None = None) -> None: super().__init__(body, design, rise=rise) ldesign = design['levels-skirt'] @@ -77,4 +80,3 @@ def eval_length(self, ldesign, body): # Add hip_line (== zero length) self.base_len = body['hips_line'] * ldesign['rise']['v'] + self.base_len - diff --git a/assets/garment_programs/skirt_paneled.py b/assets/garment_programs/bottoms/skirt_paneled.py similarity index 98% rename from assets/garment_programs/skirt_paneled.py rename to assets/garment_programs/bottoms/skirt_paneled.py index a9f68b46..ad0020b3 100644 --- a/assets/garment_programs/skirt_paneled.py +++ b/assets/garment_programs/bottoms/skirt_paneled.py @@ -5,6 +5,7 @@ from assets.garment_programs.base_classes import StackableSkirtComponent from assets.garment_programs.base_classes import BaseBottoms from assets.garment_programs import shapes +from assets.garment_programs.bottoms import factory class SkirtPanel(pyg.Panel): @@ -118,7 +119,7 @@ def __init__( hip_side_incl = np.deg2rad(body['_hip_inclination']) flare = design['flare']['v'] low_width = body['hips'] * (flare - 1) / 4 + hips # Distribute the difference equally - # between front and back + # between front and back # adjust for a rise adj_hips_depth = hips_depth * hipline_ext dart_depth = hips_depth * dart_frac @@ -126,7 +127,7 @@ def __init__( # amount of extra fabric w_diff = hips - waist # Assume its positive since waist is smaller then hips - # We distribute w_diff among the side angle and a dart + # We distribute w_diff among the side angle and a dart hw_shift = np.tan(hip_side_incl) * adj_hips_depth # Small difference if hw_shift > w_diff: @@ -197,14 +198,14 @@ def __init__( self.edges.substitute(bottom, new_edges) bottom = int_edges - + if left_slit: frac = left_slit new_left_bottom = left_bottom.subdivide_len([1 - frac, frac]) left.substitute(left_bottom, new_left_bottom[0]) self.edges.substitute(left_bottom, new_left_bottom) left_bottom = new_left_bottom[0] - + if right_slit: frac = right_slit new_rbottom = right_bottom.subdivide_len([frac, 1 - frac]) @@ -304,6 +305,7 @@ def add_darts(self, top, dart_width, dart_depth, dart_position, double_dart=Fals # Full garments - Components +@factory.register_builder("PencilSkirt") class PencilSkirt(StackableSkirtComponent): def __init__(self, body, design, tag='', length=None, rise=None, slit=True, **kwargs) -> None: super().__init__(body, design, tag) @@ -391,6 +393,8 @@ def __init__(self, body, design, tag='', length=None, rise=None, slit=True, **kw def length(self): return self.front.length() + +@factory.register_builder("Skirt2") class Skirt2(StackableSkirtComponent): """Simple 2 panel skirt""" def __init__(self, body, design, tag='', length=None, rise=None, slit=True, top_ruffles=True, min_len=5) -> None: @@ -451,6 +455,7 @@ def length(self): return self.front.length() +@factory.register_builder("SkirtManyPanels") class SkirtManyPanels(BaseBottoms): """Round Skirt with many panels""" @@ -509,4 +514,3 @@ def __init__(self, body, design, tag='', rise=None, min_len=5) -> None: def length(self): return self.front.length() - diff --git a/assets/garment_programs/collars.py b/assets/garment_programs/collars.py deleted file mode 100644 index feedd4f4..00000000 --- a/assets/garment_programs/collars.py +++ /dev/null @@ -1,370 +0,0 @@ -import numpy as np -from scipy.spatial.transform import Rotation as R - -import pygarment as pyg - -from assets.garment_programs.bands import StraightBandPanel -from assets.garment_programs.circle_skirt import CircleArcPanel - - -# # ------ Collar shapes withough extra panels ------ - -def VNeckHalf(depth, width, **kwargs): - """Simple VNeck design""" - - edges = pyg.EdgeSequence(pyg.Edge([0, 0], [width / 2, -depth])) - return edges - -def SquareNeckHalf(depth, width, **kwargs): - """Square design""" - - edges = pyg.EdgeSeqFactory.from_verts([0, 0], [0, -depth], [width / 2, -depth]) - return edges - -def TrapezoidNeckHalf(depth, width, angle=90, verbose=True, **kwargs): - """Trapesoid neck design""" - - # Special case when angle = 180 (sin = 0) - if (pyg.utils.close_enough(angle, 180, tol=1) - or pyg.utils.close_enough(angle, 0, tol=1)): - # degrades into VNeck - return VNeckHalf(depth, width) - - rad_angle = np.deg2rad(angle) - - bottom_x = -depth * np.cos(rad_angle) / np.sin(rad_angle) - if bottom_x > width / 2: # Invalid angle/depth/width combination resulted in invalid shape - if verbose: - print('TrapezoidNeckHalf::WARNING::Parameters are invalid and create overlap: ' - f'{bottom_x} > {width / 2}. ' - 'The collar is reverted to VNeck') - - return VNeckHalf(depth, width) - - edges = pyg.EdgeSeqFactory.from_verts([0, 0], [bottom_x, -depth], [width / 2, -depth]) - return edges - -def CurvyNeckHalf(depth, width, flip=False, **kwargs): - """Testing Curvy Collar design""" - - sign = -1 if flip else 1 - edges = pyg.EdgeSequence(pyg.CurveEdge( - [0, 0], [width / 2,-depth], - [[0.4, sign * 0.3], [0.8, sign * -0.3]])) - - return edges - -def CircleArcNeckHalf(depth, width, angle=90, flip=False, **kwargs): - """Collar with a side represented by a circle arc""" - # 1/4 of a circle - edges = pyg.EdgeSequence(pyg.CircleEdgeFactory.from_points_angle( - [0, 0], [width / 2,-depth], arc_angle=np.deg2rad(angle), - right=(not flip) - )) - - return edges - - -def CircleNeckHalf(depth, width, **kwargs): - """Collar that forms a perfect circle arc when halfs are stitched""" - - # Take a full desired arc and half it! - circle = pyg.CircleEdgeFactory.from_three_points( - [0, 0], - [width, 0], - [width / 2, -depth]) - subdiv = circle.subdivide_len([0.5, 0.5]) - return pyg.EdgeSequence(subdiv[0]) - -def Bezier2NeckHalf(depth, width, flip=False, x=0.5, y=0.3, **kwargs): - """2d degree Bezier curve as neckline""" - - sign = 1 if flip else -1 - edges = pyg.EdgeSequence(pyg.CurveEdge( - [0, 0], [width / 2,-depth], - [[x, sign*y]])) - - return edges - -# # ------ Collars with panels ------ - -class NoPanelsCollar(pyg.Component): - """Face collar class that only forms the projected shapes """ - - def __init__(self, name, body, design) -> None: - super().__init__(name) - - # Front - collar_type = globals()[design['collar']['f_collar']['v']] - f_collar = collar_type( - design['collar']['fc_depth']['v'], - design['collar']['width']['v'], - angle=design['collar']['fc_angle']['v'], - flip=design['collar']['f_flip_curve']['v'], - x=design['collar']['f_bezier_x']['v'], - y=design['collar']['f_bezier_y']['v'], - verbose=self.verbose - ) - - # Back - collar_type = globals()[design['collar']['b_collar']['v']] - b_collar = collar_type( - design['collar']['bc_depth']['v'], - design['collar']['width']['v'], - angle=design['collar']['bc_angle']['v'], - flip=design['collar']['b_flip_curve']['v'], - x=design['collar']['b_bezier_x']['v'], - y=design['collar']['b_bezier_y']['v'], - verbose=self.verbose - ) - - self.interfaces = { - 'front_proj': pyg.Interface(self, f_collar), - 'back_proj': pyg.Interface(self, b_collar) - } - - def length(self): - return 0 - - -class Turtle(pyg.Component): - - def __init__(self, tag, body, design) -> None: - super().__init__(f'Turtle_{tag}') - - depth = design['collar']['component']['depth']['v'] - - # --Projecting shapes-- - f_collar = CircleNeckHalf( - design['collar']['fc_depth']['v'], - design['collar']['width']['v']) - b_collar = CircleNeckHalf( - design['collar']['bc_depth']['v'], - design['collar']['width']['v']) - - self.interfaces = { - 'front_proj': pyg.Interface(self, f_collar), - 'back_proj': pyg.Interface(self, b_collar) - } - - # -- Panels -- - length_f, length_b = f_collar.length(), b_collar.length() - height_p = body['height'] - body['head_l'] + depth - - self.front = StraightBandPanel( - f'{tag}_collar_front', length_f, depth).translate_by( - [-length_f / 2, height_p, 10]) - self.back = StraightBandPanel( - f'{tag}_collar_back', length_b, depth).translate_by( - [-length_b / 2, height_p, -10]) - - self.stitching_rules.append(( - self.front.interfaces['right'], - self.back.interfaces['right'] - )) - - self.interfaces.update({ - 'front': self.front.interfaces['left'], - 'back': self.back.interfaces['left'], - 'bottom': pyg.Interface.from_multiple( - self.front.interfaces['bottom'], - self.back.interfaces['bottom'] - ) - }) - - def length(self): - return self.interfaces['back'].edges.length() - - -class SimpleLapelPanel(pyg.Panel): - """A panel for the front part of simple Lapel""" - def __init__(self, name, length, max_depth) -> None: - super().__init__(name) - - self.edges = pyg.EdgeSeqFactory.from_verts( - [0, 0], [max_depth, 0], [max_depth, -length] - ) - - self.edges.append( - pyg.CurveEdge( - self.edges[-1].end, - self.edges[0].start, - [[0.7, 0.2]] - ) - ) - - self.interfaces = { - 'to_collar': pyg.Interface(self, self.edges[0]), - 'to_bodice': pyg.Interface(self, self.edges[1]) - } - - -class SimpleLapel(pyg.Component): - - def __init__(self, tag, body, design) -> None: - super().__init__(f'Turtle_{tag}') - - depth = design['collar']['component']['depth']['v'] - standing = design['collar']['component']['lapel_standing']['v'] - - # --Projecting shapes-- - # Any front one! - collar_type = globals()[design['collar']['f_collar']['v']] - f_collar = collar_type( - design['collar']['fc_depth']['v'], - design['collar']['width']['v'], - angle=design['collar']['fc_angle']['v'], - flip=design['collar']['f_flip_curve']['v']) - - b_collar = CircleNeckHalf( - design['collar']['bc_depth']['v'], - design['collar']['width']['v']) - - self.interfaces = { - 'front_proj': pyg.Interface(self, f_collar), - 'back_proj': pyg.Interface(self, b_collar) - } - - # -- Panels -- - length_f, length_b = f_collar.length(), b_collar.length() - height_p = body['height'] - body['head_l'] + depth * 2 - - self.front = SimpleLapelPanel( - f'{tag}_collar_front', length_f, depth).translate_by( - [-depth * 2, height_p, 35]) # TODOLOW This should be related with the bodice panels' placement - - if standing: - self.back = StraightBandPanel( - f'{tag}_collar_back', length_b, depth).translate_by( - [-length_b / 2, height_p, -10]) - else: - # A curved back panel that follows the collar opening - rad, angle, _ = b_collar[0].as_radius_angle() - self.back = CircleArcPanel( - f'{tag}_collar_back', rad, depth, angle - ).translate_by([-length_b, height_p, -10]) - self.back.rotate_by(R.from_euler('XYZ', [90, 45, 0], degrees=True)) - - if standing: - self.back.interfaces['right'].set_right_wrong(True) - - self.stitching_rules.append(( - self.front.interfaces['to_collar'], - self.back.interfaces['right'] - )) - - self.interfaces.update({ - #'front': NOTE: no front interface here - 'back': self.back.interfaces['left'], - 'bottom': pyg.Interface.from_multiple( - self.front.interfaces['to_bodice'].set_right_wrong(True), - self.back.interfaces['bottom'] if standing else self.back.interfaces['top'].set_right_wrong(True), - ) - }) - - def length(self): - return self.interfaces['back'].edges.length() - -class HoodPanel(pyg.Panel): - """A panel for the side of the hood""" - def __init__(self, name, f_depth, b_depth, f_length, b_length, width, in_length, depth) -> None: - super().__init__(name) - - width = width / 2 # Panel covers one half only - length = in_length + width / 2 - - # Bottom-back - bottom_back_in = pyg.CurveEdge( - [-width, -b_depth], - [0, 0], - [[0.3, -0.2], [0.6, 0.2]] - ) - bottom_back = pyg.ops.curve_match_tangents( - bottom_back_in.as_curve(), - [1, 0], # Full opening is vertically aligned - [1, 0], - target_len=b_length, - return_as_edge=True, - verbose=self.verbose - ) - self.edges.append(bottom_back) - - # Bottom front - bottom_front_in = pyg.CurveEdge( - self.edges[-1].end, - [width, -f_depth], - [[0.3, 0.2], [0.6, -0.2]] - ) - bottom_front = pyg.ops.curve_match_tangents( - bottom_front_in.as_curve(), - [1, 0], # Full opening is vertically aligned - [1, 0], - target_len=f_length, - return_as_edge=True, - verbose=self.verbose - ) - self.edges.append(bottom_front) - - # Front-top straight section - self.edges.append(pyg.EdgeSeqFactory.from_verts( - self.edges[-1].end, - [width * 1.2, length], [width * 1.2 - depth, length] - )) - # Back of the hood - self.edges.append( - pyg.CurveEdge( - self.edges[-1].end, - self.edges[0].start, - [[0.2, -0.5]] - ) - ) - - self.interfaces = { - 'to_other_side': pyg.Interface(self, self.edges[-2:]), - 'to_bodice': pyg.Interface(self, self.edges[0:2]).reverse() - } - - self.rotate_by(R.from_euler('XYZ', [0, -90, 0], degrees=True)) - self.translate_by([-width, 0, 0]) - -class Hood2Panels(pyg.Component): - - def __init__(self, tag, body, design) -> None: - super().__init__(f'Hood_{tag}') - - # --Projecting shapes-- - width = design['collar']['width']['v'] - f_collar = CircleNeckHalf( - design['collar']['fc_depth']['v'], - design['collar']['width']['v']) - b_collar = CircleNeckHalf( - design['collar']['bc_depth']['v'], - design['collar']['width']['v']) - - self.interfaces = { - 'front_proj': pyg.Interface(self, f_collar), - 'back_proj': pyg.Interface(self, b_collar) - } - - # -- Panel -- - self.panel = HoodPanel( - f'{tag}_hood', - design['collar']['fc_depth']['v'], - design['collar']['bc_depth']['v'], - f_length=f_collar.length(), - b_length=b_collar.length(), - width=width, - in_length=body['head_l'] * design['collar']['component']['hood_length']['v'], - depth=width / 2 * design['collar']['component']['hood_depth']['v'] - ).translate_by( - [0, body['height'] - body['head_l'] + 10, 0]) - - self.interfaces.update({ - #'front': NOTE: no front interface here - 'back': self.panel.interfaces['to_other_side'], - 'bottom': self.panel.interfaces['to_bodice'] - }) - - def length(self): - return self.panel.length() - diff --git a/assets/garment_programs/collars/__init__.py b/assets/garment_programs/collars/__init__.py new file mode 100644 index 00000000..cf468044 --- /dev/null +++ b/assets/garment_programs/collars/__init__.py @@ -0,0 +1,4 @@ +from .no_panels_collar import NoPanelsCollar +from .panel_collars import Turtle, SimpleLapel, Hood2Panels + +__all__ = ["NoPanelsCollar", "Turtle", "SimpleLapel", "Hood2Panels"] diff --git a/assets/garment_programs/collars/collar_halves/__init__.py b/assets/garment_programs/collars/collar_halves/__init__.py new file mode 100644 index 00000000..bc4d8ab7 --- /dev/null +++ b/assets/garment_programs/collars/collar_halves/__init__.py @@ -0,0 +1,19 @@ +from .base import ( + VNeckHalf, + SquareNeckHalf, + TrapezoidNeckHalf, + CurvyNeckHalf, + CircleArcNeckHalf, + CircleNeckHalf, + Bezier2NeckHalf, +) + +__all__ = [ + "VNeckHalf", + "SquareNeckHalf", + "TrapezoidNeckHalf", + "CurvyNeckHalf", + "CircleArcNeckHalf", + "CircleNeckHalf", + "Bezier2NeckHalf", +] diff --git a/assets/garment_programs/collars/collar_halves/base.py b/assets/garment_programs/collars/collar_halves/base.py new file mode 100644 index 00000000..c065c5d2 --- /dev/null +++ b/assets/garment_programs/collars/collar_halves/base.py @@ -0,0 +1,116 @@ +import numpy as np + +import pygarment as pyg +from assets.garment_programs.collars.collar_halves import factory + +# # ------ Collar shapes withough extra panels ------ + + +@factory.register_builder("VNeckHalf") +def VNeckHalf(depth: float, width: float, **kwargs): + """Simple VNeck design""" + + edges = pyg.EdgeSequence(pyg.Edge([0, 0], [width / 2, -depth])) + return edges + + +@factory.register_builder("SquareNeckHalf") +def SquareNeckHalf(depth: float, width: float, **kwargs): + """Square design""" + + edges = pyg.EdgeSeqFactory.from_verts([0, 0], [0, -depth], [width / 2, -depth]) + return edges + + +@factory.register_builder("TrapezoidNeckHalf") +def TrapezoidNeckHalf( + depth: float, width: float, angle: float = 90.0, verbose: bool = True, **kwargs +): + """Trapesoid neck design""" + + # Special case when angle = 180 (sin = 0) + if pyg.utils.close_enough(angle, 180, tol=1) or pyg.utils.close_enough( + angle, 0, tol=1 + ): + # degrades into VNeck + return VNeckHalf(depth, width) + + rad_angle = np.deg2rad(angle) + + bottom_x = -depth * np.cos(rad_angle) / np.sin(rad_angle) + if ( + bottom_x > width / 2 + ): # Invalid angle/depth/width combination resulted in invalid shape + if verbose: + print( + "TrapezoidNeckHalf::WARNING::Parameters are invalid and create overlap: " + f"{bottom_x} > {width / 2}. " + "The collar is reverted to VNeck" + ) + + return VNeckHalf(depth, width) + + edges = pyg.EdgeSeqFactory.from_verts( + [0, 0], [bottom_x, -depth], [width / 2, -depth] + ) + return edges + + +@factory.register_builder("CurvyNeckHalf") +def CurvyNeckHalf(depth: float, width: float, flip: float = False, **kwargs): + """Testing Curvy Collar design""" + + sign = -1 if flip else 1 + edges = pyg.EdgeSequence( + pyg.CurveEdge( + [0, 0], [width / 2, -depth], [[0.4, sign * 0.3], [0.8, sign * -0.3]] + ) + ) + + return edges + + +@factory.register_builder("CircleArcNeckHalf") +def CircleArcNeckHalf( + depth: float, width: float, angle: float = 90.0, flip: bool = False, **kwargs +): + """Collar with a side represented by a circle arc""" + # 1/4 of a circle + edges = pyg.EdgeSequence( + pyg.CircleEdgeFactory.from_points_angle( + [0, 0], [width / 2, -depth], arc_angle=np.deg2rad(angle), right=(not flip) + ) + ) + + return edges + + +@factory.register_builder("CircleNeckHalf") +def CircleNeckHalf(depth: float, width: float, **kwargs): + """Collar that forms a perfect circle arc when halfs are stitched""" + + # Take a full desired arc and half it! + circle = pyg.CircleEdgeFactory.from_three_points( + [0, 0], [width, 0], [width / 2, -depth] + ) + subdiv = circle.subdivide_len([0.5, 0.5]) + return pyg.EdgeSequence(subdiv[0]) + + +@factory.register_builder("Bezier2NeckHalf") +def Bezier2NeckHalf( + depth: float, + width: float, + flip: bool = False, + x: float = 0.5, + y: float = 0.3, + **kwargs, +): + """2d degree Bezier curve as neckline""" + + sign = 1 if flip else -1 + edges = pyg.EdgeSequence( + pyg.CurveEdge([0, 0], [width / 2, -depth], [[x, sign * y]]) + ) + + return edges diff --git a/assets/garment_programs/collars/collar_halves/factory.py b/assets/garment_programs/collars/collar_halves/factory.py new file mode 100644 index 00000000..5d22d3b8 --- /dev/null +++ b/assets/garment_programs/collars/collar_halves/factory.py @@ -0,0 +1,33 @@ +from pygarment import registry + +_REGISTERED_COLLAR_HALVES_CLS = {} +_REGISTERED_COLLAR_HALVES_CFG = {} + + +def register_builder(key: str): + """Decorates a builder. + + The builder should be a Callable (a class or a function). + Args: + key: A `str` of key to look up the builder. + + Returns: + A callable for using as class decorator that registers the decorated class + for creation from an instance of task_config_cls. + """ + return registry.register(_REGISTERED_COLLAR_HALVES_CLS, key) + + +def build(config: dict | None = None, name: str | None = None, **kwargs): + builder = registry.lookup(_REGISTERED_COLLAR_HALVES_CLS, name) + return builder(config=config, **kwargs) + + +def get_config(name: str): + """Looks up the `Config` according to the `name`.""" + cfg_creater = registry.lookup(_REGISTERED_COLLAR_HALVES_CFG, name) + return cfg_creater() + + +def register_config(key: str): + return registry.register(_REGISTERED_COLLAR_HALVES_CFG, key) diff --git a/assets/garment_programs/collars/factory.py b/assets/garment_programs/collars/factory.py new file mode 100644 index 00000000..64552e09 --- /dev/null +++ b/assets/garment_programs/collars/factory.py @@ -0,0 +1,33 @@ +from pygarment import registry + +_REGISTERED_COLLAR_CLS = {} +_REGISTERED_COLLAR_CFG = {} + + +def register_builder(key: str): + """Decorates a builder. + + The builder should be a Callable (a class or a function). + Args: + key: A `str` of key to look up the builder. + + Returns: + A callable for using as class decorator that registers the decorated class + for creation from an instance of task_config_cls. + """ + return registry.register(_REGISTERED_COLLAR_CLS, key) + + +def build(config: dict | None = None, name: str | None = None, **kwargs): + builder = registry.lookup(_REGISTERED_COLLAR_CLS, name) + return builder(config=config, **kwargs) + + +def get_config(name: str): + """Looks up the `Config` according to the `name`.""" + cfg_creater = registry.lookup(_REGISTERED_COLLAR_CFG, name) + return cfg_creater() + + +def register_config(key: str): + return registry.register(_REGISTERED_COLLAR_CFG, key) diff --git a/assets/garment_programs/collars/no_panels_collar.py b/assets/garment_programs/collars/no_panels_collar.py new file mode 100644 index 00000000..bc5ee52d --- /dev/null +++ b/assets/garment_programs/collars/no_panels_collar.py @@ -0,0 +1,44 @@ +import pygarment as pyg +from assets.garment_programs.collars import factory +from assets.garment_programs.collars.collar_halves import \ + factory as collar_curve_factory + + +@factory.register_builder("NoPanelsCollar") +class NoPanelsCollar(pyg.Component): + """Face collar class that only forms the projected shapes""" + + def __init__(self, string_name: str, body: dict, design: dict) -> None: + super().__init__(string_name) + + # Front + f_collar = collar_curve_factory.build( + name=design["collar"]["f_collar"]["v"], + depth=design["collar"]["fc_depth"]["v"], + width=design["collar"]["width"]["v"], + angle=design["collar"]["fc_angle"]["v"], + flip=design["collar"]["f_flip_curve"]["v"], + x=design["collar"]["f_bezier_x"]["v"], + y=design["collar"]["f_bezier_y"]["v"], + verbose=self.verbose, + ) + + # Back + b_collar = collar_curve_factory.build( + name=design["collar"]["b_collar"]["v"], + depth=design["collar"]["bc_depth"]["v"], + width=design["collar"]["width"]["v"], + angle=design["collar"]["bc_angle"]["v"], + flip=design["collar"]["b_flip_curve"]["v"], + x=design["collar"]["b_bezier_x"]["v"], + y=design["collar"]["b_bezier_y"]["v"], + verbose=self.verbose, + ) + + self.interfaces = { + "front_proj": pyg.Interface(self, f_collar), + "back_proj": pyg.Interface(self, b_collar), + } + + def length(self): + return 0 diff --git a/assets/garment_programs/collars/panel_collars.py b/assets/garment_programs/collars/panel_collars.py new file mode 100644 index 00000000..47ebd827 --- /dev/null +++ b/assets/garment_programs/collars/panel_collars.py @@ -0,0 +1,314 @@ +from scipy.spatial.transform import Rotation as R + +import pygarment as pyg +from assets.garment_programs.bands import factory as band_factory +from assets.garment_programs.collars import factory +from assets.garment_programs.collars.collar_halves import \ + factory as collar_curve_factory + +# # ------ Collars with panels ------ + + +@factory.register_builder("Turtle") +class Turtle(pyg.Component): + + def __init__(self, tag: str, body: dict, design: dict) -> None: + super().__init__(f"Turtle_{tag}") + + depth = design["collar"]["component"]["depth"]["v"] + + # --Projecting shapes-- + # f_collar = CircleNeckHalf( + # design["collar"]["fc_depth"]["v"], design["collar"]["width"]["v"] + # ) + # b_collar = CircleNeckHalf( + # design["collar"]["bc_depth"]["v"], design["collar"]["width"]["v"] + # ) + f_collar = collar_curve_factory.build( + name="CircleNeckHalf", + depth=design["collar"]["fc_depth"]["v"], + width=design["collar"]["width"]["v"], + ) + b_collar = collar_curve_factory.build( + name="CircleNeckHalf", + depth=design["collar"]["bc_depth"]["v"], + width=design["collar"]["width"]["v"], + ) + + self.interfaces = { + "front_proj": pyg.Interface(self, f_collar), + "back_proj": pyg.Interface(self, b_collar), + } + + # -- Panels -- + length_f, length_b = f_collar.length(), b_collar.length() + height_p = body["height"] - body["head_l"] + depth + + # self.front = StraightBandPanel( + # f"{tag}_collar_front", length_f, depth + # ).translate_by([-length_f / 2, height_p, 10]) + # self.back = StraightBandPanel( + # f"{tag}_collar_back", length_b, depth + # ).translate_by([-length_b / 2, height_p, -10]) + self.front = band_factory.build( + name="StraightBandPanel", + string_name=f"{tag}_collar_front", + width=length_f, + depth=depth, + ).translate_by([-length_f / 2, height_p, 10]) + self.back = band_factory.build( + name="StraightBandPanel", + string_name=f"{tag}_collar_back", + width=length_b, + depth=depth, + ).translate_by([-length_b / 2, height_p, -10]) + + self.stitching_rules.append( + (self.front.interfaces["right"], self.back.interfaces["right"]) + ) + + self.interfaces.update( + { + "front": self.front.interfaces["left"], + "back": self.back.interfaces["left"], + "bottom": pyg.Interface.from_multiple( + self.front.interfaces["bottom"], self.back.interfaces["bottom"] + ), + } + ) + + def length(self): + return self.interfaces["back"].edges.length() + + +class SimpleLapelPanel(pyg.Panel): + """A panel for the front part of simple Lapel""" + + def __init__(self, name, length, max_depth) -> None: + super().__init__(name) + + self.edges = pyg.EdgeSeqFactory.from_verts( + [0, 0], [max_depth, 0], [max_depth, -length] + ) + + self.edges.append( + pyg.CurveEdge(self.edges[-1].end, self.edges[0].start, [[0.7, 0.2]]) + ) + + self.interfaces = { + "to_collar": pyg.Interface(self, self.edges[0]), + "to_bodice": pyg.Interface(self, self.edges[1]), + } + + +@factory.register_builder("SimpleLapel") +class SimpleLapel(pyg.Component): + + def __init__(self, tag, body, design) -> None: + super().__init__(f"Turtle_{tag}") + + depth = design["collar"]["component"]["depth"]["v"] + standing = design["collar"]["component"]["lapel_standing"]["v"] + + # --Projecting shapes-- + # Any front one! + # collar_type = globals()[design["collar"]["f_collar"]["v"]] + # f_collar = collar_type( + # design["collar"]["fc_depth"]["v"], + # design["collar"]["width"]["v"], + # angle=design["collar"]["fc_angle"]["v"], + # flip=design["collar"]["f_flip_curve"]["v"], + # ) + # b_collar = CircleNeckHalf( + # design["collar"]["bc_depth"]["v"], design["collar"]["width"]["v"] + # ) + f_collar = collar_curve_factory.build( + name=design["collar"]["f_collar"]["v"], + depth=design["collar"]["fc_depth"]["v"], + width=design["collar"]["width"]["v"], + angle=design["collar"]["fc_angle"]["v"], + flip=design["collar"]["f_flip_curve"]["v"], + ) + b_collar = collar_curve_factory.build( + name="CircleNeckHalf", + depth=design["collar"]["bc_depth"]["v"], + width=design["collar"]["width"]["v"], + ) + + self.interfaces = { + "front_proj": pyg.Interface(self, f_collar), + "back_proj": pyg.Interface(self, b_collar), + } + + # -- Panels -- + length_f, length_b = f_collar.length(), b_collar.length() + height_p = body["height"] - body["head_l"] + depth * 2 + + self.front = SimpleLapelPanel( + f"{tag}_collar_front", length_f, depth + ).translate_by([-depth * 2, height_p, 35]) + # TODOLOW This should be related with the bodice panels' placement + + if standing: + # self.back = StraightBandPanel( + # f"{tag}_collar_back", length_b, depth + # ).translate_by([-length_b / 2, height_p, -10]) + self.back = band_factory.build( + name="StraightBandPanel", + string_name=f"{tag}_collar_back", + width=length_b, + depth=depth, + ).translate_by([-length_b / 2, height_p, -10]) + else: + # A curved back panel that follows the collar opening + rad, angle, _ = b_collar[0].as_radius_angle() + # self.back = CircleArcPanel( + # f"{tag}_collar_back", rad, depth, angle + # ).translate_by([-length_b, height_p, -10]) + self.back = band_factory.build( + name="CircleArcPanel", + string_name=f"{tag}_collar_back", + top_rad=rad, + length=depth, + angle=angle, + ).translate_by([-length_b, height_p, -10]) + self.back.rotate_by(R.from_euler("XYZ", [90, 45, 0], degrees=True)) + + if standing: + self.back.interfaces["right"].set_right_wrong(True) + + self.stitching_rules.append( + (self.front.interfaces["to_collar"], self.back.interfaces["right"]) + ) + + self.interfaces.update( + { + #'front': NOTE: no front interface here + "back": self.back.interfaces["left"], + "bottom": pyg.Interface.from_multiple( + self.front.interfaces["to_bodice"].set_right_wrong(True), + ( + self.back.interfaces["bottom"] + if standing + else self.back.interfaces["top"].set_right_wrong(True) + ), + ), + } + ) + + def length(self): + return self.interfaces["back"].edges.length() + + +class HoodPanel(pyg.Panel): + """A panel for the side of the hood""" + + def __init__( + self, name, f_depth, b_depth, f_length, b_length, width, in_length, depth + ) -> None: + super().__init__(name) + + width = width / 2 # Panel covers one half only + length = in_length + width / 2 + + # Bottom-back + bottom_back_in = pyg.CurveEdge( + [-width, -b_depth], [0, 0], [[0.3, -0.2], [0.6, 0.2]] + ) + bottom_back = pyg.ops.curve_match_tangents( + bottom_back_in.as_curve(), + [1, 0], # Full opening is vertically aligned + [1, 0], + target_len=b_length, + return_as_edge=True, + verbose=self.verbose, + ) + self.edges.append(bottom_back) + + # Bottom front + bottom_front_in = pyg.CurveEdge( + self.edges[-1].end, [width, -f_depth], [[0.3, 0.2], [0.6, -0.2]] + ) + bottom_front = pyg.ops.curve_match_tangents( + bottom_front_in.as_curve(), + [1, 0], # Full opening is vertically aligned + [1, 0], + target_len=f_length, + return_as_edge=True, + verbose=self.verbose, + ) + self.edges.append(bottom_front) + + # Front-top straight section + self.edges.append( + pyg.EdgeSeqFactory.from_verts( + self.edges[-1].end, [width * 1.2, length], [width * 1.2 - depth, length] + ) + ) + # Back of the hood + self.edges.append( + pyg.CurveEdge(self.edges[-1].end, self.edges[0].start, [[0.2, -0.5]]) + ) + + self.interfaces = { + "to_other_side": pyg.Interface(self, self.edges[-2:]), + "to_bodice": pyg.Interface(self, self.edges[0:2]).reverse(), + } + + self.rotate_by(R.from_euler("XYZ", [0, -90, 0], degrees=True)) + self.translate_by([-width, 0, 0]) + + +@factory.register_builder("Hood2Panels") +class Hood2Panels(pyg.Component): + + def __init__(self, tag, body, design) -> None: + super().__init__(f"Hood_{tag}") + + # --Projecting shapes-- + width = design["collar"]["width"]["v"] + # f_collar = CircleNeckHalf( + # design["collar"]["fc_depth"]["v"], design["collar"]["width"]["v"] + # ) + # b_collar = CircleNeckHalf( + # design["collar"]["bc_depth"]["v"], design["collar"]["width"]["v"] + # ) + f_collar = collar_curve_factory.build( + name="CircleNeckHalf", + depth=design["collar"]["fc_depth"]["v"], + width=design["collar"]["width"]["v"], + ) + b_collar = collar_curve_factory.build( + name="CircleNeckHalf", + depth=design["collar"]["bc_depth"]["v"], + width=design["collar"]["width"]["v"], + ) + + self.interfaces = { + "front_proj": pyg.Interface(self, f_collar), + "back_proj": pyg.Interface(self, b_collar), + } + + # -- Panel -- + self.panel = HoodPanel( + f"{tag}_hood", + design["collar"]["fc_depth"]["v"], + design["collar"]["bc_depth"]["v"], + f_length=f_collar.length(), + b_length=b_collar.length(), + width=width, + in_length=body["head_l"] + * design["collar"]["component"]["hood_length"]["v"], + depth=width / 2 * design["collar"]["component"]["hood_depth"]["v"], + ).translate_by([0, body["height"] - body["head_l"] + 10, 0]) + + self.interfaces.update( + { + #'front': NOTE: no front interface here + "back": self.panel.interfaces["to_other_side"], + "bottom": self.panel.interfaces["to_bodice"], + } + ) + + def length(self): + return self.panel.length() diff --git a/assets/garment_programs/meta_garment.py b/assets/garment_programs/meta_garment.py index 0b205d06..a4249f2e 100644 --- a/assets/garment_programs/meta_garment.py +++ b/assets/garment_programs/meta_garment.py @@ -1,12 +1,12 @@ -from assets.garment_programs.tee import * -from assets.garment_programs.godet import * -from assets.garment_programs.bodice import * -from assets.garment_programs.pants import * -from assets.garment_programs.bands import * -from assets.garment_programs.skirt_paneled import * -from assets.garment_programs.skirt_levels import * -from assets.garment_programs.circle_skirt import * -from assets.garment_programs.sleeves import * +from assets.garment_programs.bodice.bodice_halves.tee import * +from assets.garment_programs.bottoms.godet import * +from assets.garment_programs.bodice.bodice_halves.bodice_halves import * +from assets.garment_programs.bottoms.pants import * +from assets.garment_programs.bands.base import * +from assets.garment_programs.bottoms.skirt_paneled import * +from assets.garment_programs.bottoms.skirt_levels import * +from assets.garment_programs.bottoms.circle_skirt import * +from assets.garment_programs.sleeves.sleeves import * class TotalLengthError(BaseException): """Error indicating that the total length of a garment goes beyond diff --git a/assets/garment_programs/sleeves/__init__.py b/assets/garment_programs/sleeves/__init__.py new file mode 100644 index 00000000..b084be09 --- /dev/null +++ b/assets/garment_programs/sleeves/__init__.py @@ -0,0 +1,3 @@ +from .sleeves import Sleeve + +__all__ = ["Sleeve"] \ No newline at end of file diff --git a/assets/garment_programs/sleeves/armhole_shapes/__init__.py b/assets/garment_programs/sleeves/armhole_shapes/__init__.py new file mode 100644 index 00000000..97118446 --- /dev/null +++ b/assets/garment_programs/sleeves/armhole_shapes/__init__.py @@ -0,0 +1,3 @@ +from .base import ArmholeSquare, ArmholeAngle, ArmholeCurve + +__all__ = ["ArmholeSquare", "ArmholeAngle", "ArmholeCurve"] diff --git a/assets/garment_programs/sleeves/armhole_shapes/base.py b/assets/garment_programs/sleeves/armhole_shapes/base.py new file mode 100644 index 00000000..9f56e297 --- /dev/null +++ b/assets/garment_programs/sleeves/armhole_shapes/base.py @@ -0,0 +1,124 @@ +from copy import deepcopy + +import numpy as np +import pygarment as pyg +from assets.garment_programs.sleeves.armhole_shapes import factory + + +# ------ Armhole shapes ------ +@factory.register_builder("ArmholeSquare") +def ArmholeSquare( + incl: float, width: float, angle: float, invert: bool = True, **kwargs +): + """Simple square armhole cut-out + Not recommended to use for sleeves, stitching in 3D might be hard + + if angle is provided, it also calculated the shape of the sleeve interface to attach + + returns edge sequence and part to be preserved inverted + """ + + edges = pyg.EdgeSeqFactory.from_verts([0, 0], [incl, 0], [incl, width]) + if not invert: + return edges, None + + sina, cosa = np.sin(angle), np.cos(angle) + l = edges[0].length() + sleeve_edges = pyg.EdgeSeqFactory.from_verts( + [incl + l * sina, -l * cosa], [incl, 0], [incl, width] + ) + + # TODOLOW Bend instead of rotating to avoid sharp connection + sleeve_edges.rotate(angle=-angle) + + return edges, sleeve_edges + + +@factory.register_builder("ArmholeAngle") +def ArmholeAngle( + incl: float, + width: float, + angle: float, + incl_coeff: float = 0.2, + w_coeff: float = 0.2, + invert: bool = True, + **kwargs +): + """Piece-wise smooth armhole shape""" + diff_incl = incl * (1 - incl_coeff) + edges = pyg.EdgeSeqFactory.from_verts( + [0, 0], [diff_incl, w_coeff * width], [incl, width] + ) + if not invert: + return edges, None + + sina, cosa = np.sin(angle), np.cos(angle) + l = edges[0].length() + sleeve_edges = pyg.EdgeSeqFactory.from_verts( + [diff_incl + l * sina, w_coeff * width - l * cosa], + [diff_incl, w_coeff * width], + [incl, width], + ) + # TODOLOW Bend instead of rotating to avoid sharp connection + sleeve_edges.rotate(angle=-angle) + + return edges, sleeve_edges + + +@factory.register_builder("ArmholeCurve") +def ArmholeCurve( + incl: float, + width: float, + angle: float, + bottom_angle_mix: float = 0, + invert: bool = True, + verbose: bool = False, + **kwargs +): + """Classic sleeve opening on Cubic Bezier curves""" + # Curvature as parameters? + cps = [[0.5, 0.2], [0.8, 0.35]] + edge = pyg.CurveEdge([incl, width], [0, 0], cps) + edge_as_seq = pyg.EdgeSequence(edge.reverse()) + + if not invert: + return edge_as_seq, None + + # Initialize inverse (initial guess) + # Agle == 0 + down_direction = np.array([0, -1]) # Full opening is vertically aligned + inv_cps = deepcopy(cps) + inv_cps[-1][1] *= -1 # Invert the last + inv_edge = pyg.CurveEdge( + start=[incl, width], + end=(np.array([incl, width]) + down_direction * edge._straight_len()).tolist(), + control_points=inv_cps, + ) + + # Rotate by desired angle (usually desired sleeve rest angle) + inv_edge.rotate(angle=-angle) + + # Optimize the inverse shape to be nice + shortcut = inv_edge.shortcut() + rotated_direction = shortcut[-1] - shortcut[0] + rotated_direction /= np.linalg.norm(rotated_direction) + left_direction = np.array([-1, 0]) + mix_factor = bottom_angle_mix + + dir = (1 - mix_factor) * rotated_direction + ( + mix_factor * down_direction + if mix_factor > 0 + else (-mix_factor * left_direction) + ) + + # TODOLOW Remember relative curvature results and reuse them? (speed) + fin_inv_edge = pyg.ops.curve_match_tangents( + inv_edge.as_curve(), + down_direction, # Full opening is vertically aligned + dir, + target_len=edge.length(), + return_as_edge=True, + verbose=verbose, + ) + + return edge_as_seq, pyg.EdgeSequence(fin_inv_edge.reverse()) diff --git a/assets/garment_programs/sleeves/armhole_shapes/factory.py b/assets/garment_programs/sleeves/armhole_shapes/factory.py new file mode 100644 index 00000000..089577c0 --- /dev/null +++ b/assets/garment_programs/sleeves/armhole_shapes/factory.py @@ -0,0 +1,33 @@ +from pygarment import registry + +_REGISTERED_ARMHOLE_CLS = {} +_REGISTERED_ARMHOLE_CFG = {} + + +def register_builder(key: str): + """Decorates a builder. + + The builder should be a Callable (a class or a function). + Args: + key: A `str` of key to look up the builder. + + Returns: + A callable for using as class decorator that registers the decorated class + for creation from an instance of task_config_cls. + """ + return registry.register(_REGISTERED_ARMHOLE_CLS, key) + + +def build(config: dict | None = None, name: str | None = None, **kwargs): + builder = registry.lookup(_REGISTERED_ARMHOLE_CLS, name) + return builder(config=config, **kwargs) + + +def get_config(name: str): + """Looks up the `Config` according to the `name`.""" + cfg_creater = registry.lookup(_REGISTERED_ARMHOLE_CFG, name) + return cfg_creater() + + +def register_config(key: str): + return registry.register(_REGISTERED_ARMHOLE_CFG, key) diff --git a/assets/garment_programs/sleeves/factory.py b/assets/garment_programs/sleeves/factory.py new file mode 100644 index 00000000..54308d62 --- /dev/null +++ b/assets/garment_programs/sleeves/factory.py @@ -0,0 +1,33 @@ +from pygarment import registry + +_REGISTERED_SLEEVE_CLS = {} +_REGISTERED_SLEEVE_CFG = {} + + +def register_builder(key: str): + """Decorates a builder. + + The builder should be a Callable (a class or a function). + Args: + key: A `str` of key to look up the builder. + + Returns: + A callable for using as class decorator that registers the decorated class + for creation from an instance of task_config_cls. + """ + return registry.register(_REGISTERED_SLEEVE_CLS, key) + + +def build(config: dict | None = None, name: str | None = None, **kwargs): + builder = registry.lookup(_REGISTERED_SLEEVE_CLS, name) + return builder(config=config, **kwargs) + + +def get_config(name: str): + """Looks up the `Config` according to the `name`.""" + cfg_creater = registry.lookup(_REGISTERED_SLEEVE_CFG, name) + return cfg_creater() + + +def register_config(key: str): + return registry.register(_REGISTERED_SLEEVE_CFG, key) diff --git a/assets/garment_programs/sleeves.py b/assets/garment_programs/sleeves/sleeves.py similarity index 68% rename from assets/garment_programs/sleeves.py rename to assets/garment_programs/sleeves/sleeves.py index 775c8690..211e38f5 100644 --- a/assets/garment_programs/sleeves.py +++ b/assets/garment_programs/sleeves/sleeves.py @@ -3,106 +3,12 @@ import numpy as np from scipy.spatial.transform import Rotation as R -from assets.garment_programs import bands +from assets.garment_programs.bands import base as bands +from assets.garment_programs.sleeves.armhole_shapes import factory as armhole_factory +from assets.garment_programs.sleeves import factory import pygarment as pyg -# ------ Armhole shapes ------ -def ArmholeSquare(incl, width, angle, invert=True, **kwargs): - """Simple square armhole cut-out - Not recommended to use for sleeves, stitching in 3D might be hard - - if angle is provided, it also calculated the shape of the sleeve interface to attach - - returns edge sequence and part to be preserved inverted - """ - - edges = pyg.EdgeSeqFactory.from_verts([0, 0], [incl, 0], [incl, width]) - if not invert: - return edges, None - - sina, cosa = np.sin(angle), np.cos(angle) - l = edges[0].length() - sleeve_edges = pyg.EdgeSeqFactory.from_verts( - [incl + l*sina, - l*cosa], - [incl, 0], [incl, width]) - - # TODOLOW Bend instead of rotating to avoid sharp connection - sleeve_edges.rotate(angle=-angle) - - return edges, sleeve_edges - - -def ArmholeAngle(incl, width, angle, incl_coeff=0.2, w_coeff=0.2, - invert=True, **kwargs): - """Piece-wise smooth armhole shape""" - diff_incl = incl * (1 - incl_coeff) - edges = pyg.EdgeSeqFactory.from_verts( - [0, 0], [diff_incl, w_coeff * width], [incl, width]) - if not invert: - return edges, None - - sina, cosa = np.sin(angle), np.cos(angle) - l = edges[0].length() - sleeve_edges = pyg.EdgeSeqFactory.from_verts( - [diff_incl + l*sina, w_coeff * width - l*cosa], - [diff_incl, w_coeff * width], [incl, width]) - # TODOLOW Bend instead of rotating to avoid sharp connection - sleeve_edges.rotate(angle=-angle) - - return edges, sleeve_edges - - -def ArmholeCurve(incl, width, angle, bottom_angle_mix=0, invert=True, verbose=False, **kwargs): - """ Classic sleeve opening on Cubic Bezier curves - """ - # Curvature as parameters? - cps = [[0.5, 0.2], [0.8, 0.35]] - edge = pyg.CurveEdge([incl, width], [0, 0], cps) - edge_as_seq = pyg.EdgeSequence(edge.reverse()) - - if not invert: - return edge_as_seq, None - - # Initialize inverse (initial guess) - # Agle == 0 - down_direction = np.array([0, -1]) # Full opening is vertically aligned - inv_cps = deepcopy(cps) - inv_cps[-1][1] *= -1 # Invert the last - inv_edge = pyg.CurveEdge( - start=[incl, width], - end=(np.array([incl, width]) + down_direction * edge._straight_len()).tolist(), - control_points=inv_cps - ) - - # Rotate by desired angle (usually desired sleeve rest angle) - inv_edge.rotate(angle=-angle) - - # Optimize the inverse shape to be nice - shortcut = inv_edge.shortcut() - rotated_direction = shortcut[-1] - shortcut[0] - rotated_direction /= np.linalg.norm(rotated_direction) - left_direction = np.array([-1, 0]) - mix_factor = bottom_angle_mix - - dir = (1 - mix_factor) * rotated_direction + ( - mix_factor * down_direction if mix_factor > 0 else (- mix_factor * left_direction)) - - # TODOLOW Remember relative curvature results and reuse them? (speed) - fin_inv_edge = pyg.ops.curve_match_tangents( - inv_edge.as_curve(), - down_direction, # Full opening is vertically aligned - dir, - target_len=edge.length(), - return_as_edge=True, - verbose=verbose - ) - - return edge_as_seq, pyg.EdgeSequence(fin_inv_edge.reverse()) - - -# -------- New sleeve definitions ------- - class SleevePanel(pyg.Panel): """Trying proper sleeve panel""" @@ -120,7 +26,7 @@ def __init__(self, name, body, design, open_shape, length_shift=0, _standing_mar standing = design['standing_shoulder']['v'] # Calculating extension size & end size before applying ruffles - # Since ruffles add to pattern length & width, but not to de-facto + # Since ruffles add to pattern length & width, but not to de-facto # sleeve length in 3D end_width = design['end_width']['v'] * abs(open_shape[0].start[1] - open_shape[-1].end[1]) # Ensure it fits regardless of parameters @@ -190,11 +96,12 @@ def __init__(self, name, body, design, open_shape, length_shift=0, _standing_mar body['height'] - body['head_l'], 0, ]) - - def length(self, longest_dim=False): + + def length(self, longest_dim: bool = False): return self.interfaces['bottom'].edges.length() +@factory.register_builder("Sleeve") class Sleeve(pyg.Component): """Trying to do a proper sleeve""" def __init__(self, tag, body, design, front_w, back_w): @@ -211,7 +118,7 @@ def __init__(self, tag, body, design, front_w, back_w): design = design['sleeve'] self.design = design self.body = body - + sleeve_balance = body['_base_sleeve_balance'] / 2 rest_angle = max(np.deg2rad(design['sleeve_angle']['v']), @@ -224,29 +131,51 @@ def __init__(self, tag, body, design, front_w, back_w): back_w = back_w(connecting_width) if callable(back_w) else back_w # --- Define sleeve opening shapes ---- - # NOTE: Non-trad armholes only for sleeveless styles due to + # NOTE: Non-trad armholes only for sleeveless styles due to # unclear inversion and stitching errors (see below) - armhole = globals()[design['armhole_shape']['v']] if design['sleeveless']['v'] else ArmholeCurve - front_project, front_opening = armhole( - front_w - sleeve_balance, - connecting_width, - angle=rest_angle, - incl_coeff=smoothing_coeff, - w_coeff=smoothing_coeff, - invert=not design['sleeveless']['v'], - bottom_angle_mix=design['opening_dir_mix']['v'], - verbose=self.verbose + # armhole = globals()[design['armhole_shape']['v']] if design['sleeveless']['v'] else ArmholeCurve + # front_project, front_opening = armhole( + # front_w - sleeve_balance, + # connecting_width, + # angle=rest_angle, + # incl_coeff=smoothing_coeff, + # w_coeff=smoothing_coeff, + # invert=not design['sleeveless']['v'], + # bottom_angle_mix=design['opening_dir_mix']['v'], + # verbose=self.verbose + # ) + armhole_str_name = "ArmholeCurve" if design['sleeveless']['v'] is None else design['sleeveless']['v'] + front_project, front_opening = armhole_factory.build( + name=armhole_str_name, + incl=front_w - sleeve_balance, + width=connecting_width, + angle=rest_angle, + incl_coeff=smoothing_coeff, + w_coeff=smoothing_coeff, + invert=not design["sleeveless"]["v"], + bottom_angle_mix=design["opening_dir_mix"]["v"], + verbose=self.verbose, ) - back_project, back_opening = armhole( - back_w - sleeve_balance, - connecting_width, - angle=rest_angle, - incl_coeff=smoothing_coeff, + # back_project, back_opening = armhole( + # back_w - sleeve_balance, + # connecting_width, + # angle=rest_angle, + # incl_coeff=smoothing_coeff, + # w_coeff=smoothing_coeff, + # invert=not design['sleeveless']['v'], + # bottom_angle_mix=design['opening_dir_mix']['v'] + # ) + back_project, back_opening = armhole_factory.build( + name=armhole_str_name, + incl=back_w - sleeve_balance, + width=connecting_width, + angle=rest_angle, + incl_coeff=smoothing_coeff, w_coeff=smoothing_coeff, - invert=not design['sleeveless']['v'], - bottom_angle_mix=design['opening_dir_mix']['v'] + invert=not design["sleeveless"]["v"], + bottom_angle_mix=design["opening_dir_mix"]["v"], ) - + self.interfaces = { 'in_front_shape': pyg.Interface(self, front_project), 'in_back_shape': pyg.Interface(self, back_project) @@ -255,14 +184,14 @@ def __init__(self, tag, body, design, front_w, back_w): if design['sleeveless']['v']: # The rest is not needed! return - + if front_w != back_w: front_opening, back_opening = pyg.ops.even_armhole_openings( front_opening, back_opening, tol=0.2 / front_opening.length(), # ~2mm tolerance as a fraction of length verbose=self.verbose ) - + # --- Eval length adjustment for cuffs (if any) ---- cuff_len_adj = self._cuff_len_adj() @@ -331,7 +260,7 @@ def __init__(self, tag, body, design, front_w, back_w): self.interfaces['out'] ) ) - + # UPD out interface! self.interfaces['out'] = self.cuff.interfaces['bottom'] @@ -339,26 +268,26 @@ def __init__(self, tag, body, design, front_w, back_w): self.rotate_by(R.from_euler( 'XYZ', [0, 0, body['arm_pose_angle']], degrees=True)) - # Set label + # Set label self.set_panel_label('arm') def _cuff_len_adj(self): """Eval sleeve length adjustment due to cuffs (if any)""" if not self.design['cuff']['type']['v']: return 0 - + cuff_len_adj = self.design['cuff']['cuff_len']['v'] * self.body['arm_length'] max_len = self.design['length']['v'] * self.body['arm_length'] if cuff_len_adj > max_len * 0.7: cuff_len_adj = max_len * 0.7 - + return cuff_len_adj def length(self): if self.design['sleeveless']['v']: return 0 - + if self.design['cuff']['type']['v']: return self.f_sleeve.length() + self.cuff.length() - - return self.f_sleeve.length() \ No newline at end of file + + return self.f_sleeve.length() From a53b21702ec6202bbc82b908a39d26cdb6cf5fc0 Mon Sep 17 00:00:00 2001 From: quoccuongLE Date: Sun, 16 Feb 2025 09:54:12 +0100 Subject: [PATCH 5/8] feat: restore pygarment structure --- pygarment/__init__.py | 16 ++++----- .../garmentcode/{edge => }/circle_edge.py | 2 +- .../garmentcode/{component => }/component.py | 6 ---- pygarment/garmentcode/component/factory.py | 33 ------------------- pygarment/garmentcode/{edge => }/edge.py | 3 +- pygarment/garmentcode/edge/factory.py | 33 ------------------- .../garmentcode/{edge => }/edge_factory.py | 8 +---- .../garmentcode/{operators => }/operators.py | 2 +- pygarment/garmentcode/{panel => }/panel.py | 10 ++---- pygarment/garmentcode/panel/factory.py | 33 ------------------- 10 files changed, 14 insertions(+), 132 deletions(-) rename pygarment/garmentcode/{edge => }/circle_edge.py (99%) rename pygarment/garmentcode/{component => }/component.py (97%) delete mode 100644 pygarment/garmentcode/component/factory.py rename pygarment/garmentcode/{edge => }/edge.py (99%) delete mode 100644 pygarment/garmentcode/edge/factory.py rename pygarment/garmentcode/{edge => }/edge_factory.py (98%) rename pygarment/garmentcode/{operators => }/operators.py (99%) rename pygarment/garmentcode/{panel => }/panel.py (98%) delete mode 100644 pygarment/garmentcode/panel/factory.py diff --git a/pygarment/__init__.py b/pygarment/__init__.py index 92c4c5c1..be011c71 100644 --- a/pygarment/__init__.py +++ b/pygarment/__init__.py @@ -3,19 +3,19 @@ """ # Building blocks -from pygarment.garmentcode.component.component import Component -from pygarment.garmentcode.panel.panel import Panel -from pygarment.garmentcode.edge.edge import Edge, CircleEdge, CurveEdge, EdgeSequence +from pygarment.garmentcode.component import Component +from pygarment.garmentcode.panel import Panel +from pygarment.garmentcode.edge import Edge, CircleEdge, CurveEdge, EdgeSequence from pygarment.garmentcode.connector import Stitches from pygarment.garmentcode.interface import Interface -from pygarment.garmentcode.edge.edge_factory import EdgeSeqFactory -from pygarment.garmentcode.edge.edge_factory import CircleEdgeFactory -from pygarment.garmentcode.edge.edge_factory import EdgeFactory -from pygarment.garmentcode.edge.edge_factory import CurveEdgeFactory +from pygarment.garmentcode.edge_factory import EdgeSeqFactory +from pygarment.garmentcode.edge_factory import CircleEdgeFactory +from pygarment.garmentcode.edge_factory import EdgeFactory +from pygarment.garmentcode.edge_factory import CurveEdgeFactory # Operations -import pygarment.garmentcode.operators.operators as ops +import pygarment.garmentcode.operators as ops import pygarment.garmentcode.utils as utils # Parameter support diff --git a/pygarment/garmentcode/edge/circle_edge.py b/pygarment/garmentcode/circle_edge.py similarity index 99% rename from pygarment/garmentcode/edge/circle_edge.py rename to pygarment/garmentcode/circle_edge.py index 61b6e069..56584125 100644 --- a/pygarment/garmentcode/edge/circle_edge.py +++ b/pygarment/garmentcode/circle_edge.py @@ -83,7 +83,7 @@ def _subdivide(self, fractions: list, by_length=False): # So parent implementation is ok # TODOLOW Implementation is very similar to CurveEdge param-based subdivision - from pygarment.garmentcode.edge.edge_factory import \ + from pygarment.garmentcode.edge_factory import \ EdgeFactory # TODOLOW: ami - better solution? frac = [abs(f) for f in fractions] diff --git a/pygarment/garmentcode/component/component.py b/pygarment/garmentcode/component.py similarity index 97% rename from pygarment/garmentcode/component/component.py rename to pygarment/garmentcode/component.py index 2405a4ef..28b50381 100644 --- a/pygarment/garmentcode/component/component.py +++ b/pygarment/garmentcode/component.py @@ -3,7 +3,6 @@ from pygarment.garmentcode.base import BaseComponent from pygarment.pattern.wrappers import VisPattern -from pygarment.garmentcode.component import factory class Component(BaseComponent): @@ -145,8 +144,3 @@ def _get_subcomponents(self): return list(set([att for att in all_attrs if isinstance(att, BaseComponent)] + self.subs)) - - -@factory.register_builder("component") -def build_component(name: str): - return Component(name=name) diff --git a/pygarment/garmentcode/component/factory.py b/pygarment/garmentcode/component/factory.py deleted file mode 100644 index 5250cc07..00000000 --- a/pygarment/garmentcode/component/factory.py +++ /dev/null @@ -1,33 +0,0 @@ -from pygarment import registry - -_REGISTERED_COMPONENT_CLS = {} -_REGISTERED_COMPONENT_CFG = {} - - -def register_builder(key: str): - """Decorates a builder. - - The builder should be a Callable (a class or a function). - Args: - key: A `str` of key to look up the builder. - - Returns: - A callable for using as class decorator that registers the decorated class - for creation from an instance of task_config_cls. - """ - return registry.register(_REGISTERED_COMPONENT_CLS, key) - - -def build(config: dict, name: str | None = None, **kwargs): - builder = registry.lookup(_REGISTERED_COMPONENT_CLS, name) - return builder(config=config, **kwargs) - - -def get_config(name: str): - """Looks up the `Config` according to the `name`.""" - cfg_creater = registry.lookup(_REGISTERED_COMPONENT_CFG, name) - return cfg_creater() - - -def register_config(key: str): - return registry.register(_REGISTERED_COMPONENT_CFG, key) diff --git a/pygarment/garmentcode/edge/edge.py b/pygarment/garmentcode/edge.py similarity index 99% rename from pygarment/garmentcode/edge/edge.py rename to pygarment/garmentcode/edge.py index df152f56..406dd499 100644 --- a/pygarment/garmentcode/edge/edge.py +++ b/pygarment/garmentcode/edge.py @@ -6,7 +6,6 @@ from pygarment.garmentcode.utils import R2D, close_enough, c_to_list from pygarment.pattern.utils import rel_to_abs_2d, abs_to_rel_2d -# from .edge_sequence import EdgeSequence # TODO: Circular import error ILENGTH_S_TOL = 1e-10 # NOTE: tolerance value for evaluating curve parameter (t) from acr length @@ -322,7 +321,7 @@ def _subdivide(self, fractions: list, by_length=False): splitting its curve parametrization or overall length according to fractions while preserving the overall shape """ - from pygarment.garmentcode.edge.edge_factory import EdgeFactory # TODOLOW: ami - better solution? + from pygarment.garmentcode.edge_factory import EdgeFactory # TODOLOW: ami - better solution? curve = self.as_curve() # Sub-curves diff --git a/pygarment/garmentcode/edge/factory.py b/pygarment/garmentcode/edge/factory.py deleted file mode 100644 index 48c76fa2..00000000 --- a/pygarment/garmentcode/edge/factory.py +++ /dev/null @@ -1,33 +0,0 @@ -from pygarment import registry - -_REGISTERED_EDGE_CLS = {} -_REGISTERED_EDGE_CFG = {} - - -def register_builder(key: str): - """Decorates a builder. - - The builder should be a Callable (a class or a function). - Args: - key: A `str` of key to look up the builder. - - Returns: - A callable for using as class decorator that registers the decorated class - for creation from an instance of task_config_cls. - """ - return registry.register(_REGISTERED_EDGE_CLS, key) - - -def build(config: dict, name: str | None = None, **kwargs): - builder = registry.lookup(_REGISTERED_EDGE_CLS, name) - return builder(config=config, **kwargs) - - -def get_config(name: str): - """Looks up the `Config` according to the `name`.""" - cfg_creater = registry.lookup(_REGISTERED_EDGE_CFG, name) - return cfg_creater() - - -def register_config(key: str): - return registry.register(_REGISTERED_EDGE_CFG, key) diff --git a/pygarment/garmentcode/edge/edge_factory.py b/pygarment/garmentcode/edge_factory.py similarity index 98% rename from pygarment/garmentcode/edge/edge_factory.py rename to pygarment/garmentcode/edge_factory.py index 20fb6acd..5221afde 100644 --- a/pygarment/garmentcode/edge/edge_factory.py +++ b/pygarment/garmentcode/edge_factory.py @@ -3,14 +3,12 @@ from numpy.linalg import norm from scipy.optimize import minimize -from pygarment.garmentcode.edge.edge import (CircleEdge, CurveEdge, Edge, +from pygarment.garmentcode.edge import (CircleEdge, CurveEdge, Edge, EdgeSequence) from pygarment.garmentcode.utils import (bbox_paths, c_to_list, close_enough, list_to_c, vector_angle) from pygarment.pattern.utils import abs_to_rel_2d, rel_to_abs_2d -from pygarment.garmentcode.edge import factory - class EdgeFactory: @staticmethod @@ -40,7 +38,6 @@ def from_svg_curve(seg): return CurveEdge(start, end, cp, relative=False) -@factory.register_builder("edge") def build_edge(seg, type: str = "from_svg_curve"): return EdgeFactory.from_svg_curve(seg) @@ -158,7 +155,6 @@ def from_three_points(start, end, point_on_arc, relative=False): large_arc=mid_dist > rad, right=angle > 0) -@factory.register_builder("circle arc") def build_circle_arc(method: str, **kwargs): _build = getattr(CircleEdgeFactory, method) if callable(_build): @@ -233,7 +229,6 @@ def curve_from_tangents(start, end, target_tan0=None, target_tan1=None, return CurveEdge(start, end, control_points=[cp], relative=True) -@factory.register_builder("curve edge") def build_curve_edge(method: str, **kwargs): _build = getattr(CurveEdgeFactory, method) if callable(_build): @@ -407,7 +402,6 @@ def halfs_from_svg(svg_filepath, target_height=None): return left_seqs, right_seqs -@factory.register_builder("edge sequence") def build_edge_seq(method: str, **kwargs): _build = getattr(EdgeSeqFactory, method) if callable(_build): diff --git a/pygarment/garmentcode/operators/operators.py b/pygarment/garmentcode/operators.py similarity index 99% rename from pygarment/garmentcode/operators/operators.py rename to pygarment/garmentcode/operators.py index b98c8484..feb81d43 100644 --- a/pygarment/garmentcode/operators/operators.py +++ b/pygarment/garmentcode/operators.py @@ -7,7 +7,7 @@ from scipy.optimize import minimize import svgpathtools as svgpath -from pygarment.garmentcode.edge.edge import Edge, CurveEdge, EdgeSequence, ILENGTH_S_TOL +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 diff --git a/pygarment/garmentcode/panel/panel.py b/pygarment/garmentcode/panel.py similarity index 98% rename from pygarment/garmentcode/panel/panel.py rename to pygarment/garmentcode/panel.py index ad81a4e3..09d3d49f 100644 --- a/pygarment/garmentcode/panel/panel.py +++ b/pygarment/garmentcode/panel.py @@ -5,11 +5,10 @@ from pygarment.pattern.core import BasicPattern from pygarment.garmentcode.base import BaseComponent -from pygarment.garmentcode.edge.edge import Edge, EdgeSequence, CircleEdge +from pygarment.garmentcode.edge import Edge, EdgeSequence, CircleEdge from pygarment.garmentcode.utils import close_enough, vector_align_3D -from pygarment.garmentcode.operators.operators import cut_into_edge +from pygarment.garmentcode.operators import cut_into_edge from pygarment.garmentcode.interface import Interface -from pygarment.garmentcode.panel import factory class Panel(BaseComponent): @@ -407,8 +406,3 @@ def bbox3D(self): 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) - - -@factory.register_builder("panel") -def create_panel(name: str, label: str=""): - return Panel(name=name, label=label) diff --git a/pygarment/garmentcode/panel/factory.py b/pygarment/garmentcode/panel/factory.py deleted file mode 100644 index b7daa353..00000000 --- a/pygarment/garmentcode/panel/factory.py +++ /dev/null @@ -1,33 +0,0 @@ -from pygarment import registry - -_REGISTERED_PANEL_CLS = {} -_REGISTERED_PANEL_CFG = {} - - -def register_builder(key: str): - """Decorates a builder. - - The builder should be a Callable (a class or a function). - Args: - key: A `str` of key to look up the builder. - - Returns: - A callable for using as class decorator that registers the decorated class - for creation from an instance of task_config_cls. - """ - return registry.register(_REGISTERED_PANEL_CLS, key) - - -def build(config: dict, name: str | None = None, **kwargs): - builder = registry.lookup(_REGISTERED_PANEL_CLS, name) - return builder(config=config, **kwargs) - - -def get_config(name: str): - """Looks up the `Config` according to the `name`.""" - cfg_creater = registry.lookup(_REGISTERED_PANEL_CFG, name) - return cfg_creater() - - -def register_config(key: str): - return registry.register(_REGISTERED_PANEL_CFG, key) From 672dee0ce23fb08b400f3c911bad6edd2ee62699 Mon Sep 17 00:00:00 2001 From: quoccuongLE Date: Sun, 16 Feb 2025 10:12:02 +0100 Subject: [PATCH 6/8] fix: typing, import, reverse split edge files --- assets/garment_programs/bands/factory.py | 2 +- assets/garment_programs/bodice/factory.py | 2 +- assets/garment_programs/bottoms/factory.py | 2 +- assets/garment_programs/bottoms/godet.py | 2 +- .../garment_programs/bottoms/skirt_levels.py | 2 +- .../collars/collar_halves/factory.py | 2 +- assets/garment_programs/collars/factory.py | 2 +- .../sleeves/armhole_shapes/factory.py | 2 +- assets/garment_programs/sleeves/factory.py | 2 +- pygarment/garmentcode/edge.py | 424 ++++++++++++++---- pygarment/garmentcode/interface.py | 2 +- 11 files changed, 339 insertions(+), 105 deletions(-) diff --git a/assets/garment_programs/bands/factory.py b/assets/garment_programs/bands/factory.py index 290703f4..9a8a170c 100644 --- a/assets/garment_programs/bands/factory.py +++ b/assets/garment_programs/bands/factory.py @@ -18,7 +18,7 @@ def register_builder(key: str): return registry.register(_REGISTERED_BAND_CLS, key) -def build(config: dict | None = None, name: str | None = None, **kwargs): +def build(config: dict = None, name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_BAND_CLS, name) return builder(config=config, **kwargs) diff --git a/assets/garment_programs/bodice/factory.py b/assets/garment_programs/bodice/factory.py index 8ca51912..b8055107 100644 --- a/assets/garment_programs/bodice/factory.py +++ b/assets/garment_programs/bodice/factory.py @@ -18,7 +18,7 @@ def register_builder(key: str): return registry.register(_REGISTERED_BODICE_CLS, key) -def build(config: dict, name: str | None = None, **kwargs): +def build(config: dict, name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_BODICE_CLS, name) return builder(config=config, **kwargs) diff --git a/assets/garment_programs/bottoms/factory.py b/assets/garment_programs/bottoms/factory.py index 6288c4d7..50430532 100644 --- a/assets/garment_programs/bottoms/factory.py +++ b/assets/garment_programs/bottoms/factory.py @@ -18,7 +18,7 @@ def register_builder(key: str): return registry.register(_REGISTERED_BOTTOM_CLS, key) -def build(config: dict | None = None, name: str | None = None, **kwargs): +def build(config: dict = None, name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_BOTTOM_CLS, name) return builder(config=config, **kwargs) diff --git a/assets/garment_programs/bottoms/godet.py b/assets/garment_programs/bottoms/godet.py index a5e4cbc2..a700002e 100644 --- a/assets/garment_programs/bottoms/godet.py +++ b/assets/garment_programs/bottoms/godet.py @@ -27,7 +27,7 @@ def __init__(self, id: int, width: float = 30, depth: float = 30) -> None: @factory.register_builder("GodetSkirt") class GodetSkirt(BaseBottoms): - def __init__(self, body: dict, design: dict, rise: float | None = None) -> None: + def __init__(self, body: dict, design: dict, rise: float = None) -> None: super().__init__(body, design, rise=rise) gdesign = design['godet-skirt'] diff --git a/assets/garment_programs/bottoms/skirt_levels.py b/assets/garment_programs/bottoms/skirt_levels.py index 213466d1..e80ed0d6 100644 --- a/assets/garment_programs/bottoms/skirt_levels.py +++ b/assets/garment_programs/bottoms/skirt_levels.py @@ -9,7 +9,7 @@ class SkirtLevels(BaseBottoms): """Skirt constiting of multuple stitched skirts""" - def __init__(self, body: dict, design: dict, rise: float | None = None) -> None: + def __init__(self, body: dict, design: dict, rise: float = None) -> None: super().__init__(body, design, rise=rise) ldesign = design['levels-skirt'] diff --git a/assets/garment_programs/collars/collar_halves/factory.py b/assets/garment_programs/collars/collar_halves/factory.py index 5d22d3b8..07a63182 100644 --- a/assets/garment_programs/collars/collar_halves/factory.py +++ b/assets/garment_programs/collars/collar_halves/factory.py @@ -18,7 +18,7 @@ def register_builder(key: str): return registry.register(_REGISTERED_COLLAR_HALVES_CLS, key) -def build(config: dict | None = None, name: str | None = None, **kwargs): +def build(config: dict = None, name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_COLLAR_HALVES_CLS, name) return builder(config=config, **kwargs) diff --git a/assets/garment_programs/collars/factory.py b/assets/garment_programs/collars/factory.py index 64552e09..e5268ac1 100644 --- a/assets/garment_programs/collars/factory.py +++ b/assets/garment_programs/collars/factory.py @@ -18,7 +18,7 @@ def register_builder(key: str): return registry.register(_REGISTERED_COLLAR_CLS, key) -def build(config: dict | None = None, name: str | None = None, **kwargs): +def build(config: dict = None, name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_COLLAR_CLS, name) return builder(config=config, **kwargs) diff --git a/assets/garment_programs/sleeves/armhole_shapes/factory.py b/assets/garment_programs/sleeves/armhole_shapes/factory.py index 089577c0..cdda9d22 100644 --- a/assets/garment_programs/sleeves/armhole_shapes/factory.py +++ b/assets/garment_programs/sleeves/armhole_shapes/factory.py @@ -18,7 +18,7 @@ def register_builder(key: str): return registry.register(_REGISTERED_ARMHOLE_CLS, key) -def build(config: dict | None = None, name: str | None = None, **kwargs): +def build(config: dict = None, name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_ARMHOLE_CLS, name) return builder(config=config, **kwargs) diff --git a/assets/garment_programs/sleeves/factory.py b/assets/garment_programs/sleeves/factory.py index 54308d62..fbd19706 100644 --- a/assets/garment_programs/sleeves/factory.py +++ b/assets/garment_programs/sleeves/factory.py @@ -18,7 +18,7 @@ def register_builder(key: str): return registry.register(_REGISTERED_SLEEVE_CLS, key) -def build(config: dict | None = None, name: str | None = None, **kwargs): +def build(config: dict = None, name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_SLEEVE_CLS, name) return builder(config=config, **kwargs) diff --git a/pygarment/garmentcode/edge.py b/pygarment/garmentcode/edge.py index 406dd499..93c8e4b3 100644 --- a/pygarment/garmentcode/edge.py +++ b/pygarment/garmentcode/edge.py @@ -4,11 +4,16 @@ from numpy.linalg import norm import svgpathtools as svgpath # https://github.com/mathandy/svgpathtools -from pygarment.garmentcode.utils import R2D, close_enough, c_to_list +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 +) -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 @@ -18,9 +23,9 @@ class Edge: and (End-Start) as Ox axis """ - def __init__(self, start=None, end=None, label='') -> None: - """ Simple edge initialization. - Parameters: + 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 @@ -32,7 +37,9 @@ def __init__(self, start=None, end=None, label='') -> 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' + 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 @@ -46,8 +53,8 @@ def __init__(self, start=None, end=None, label='') -> None: def length(self): """Return current length of an edge. - Since vertices may change their locations externally, the length - is dynamically evaluated + Since vertices may change their locations externally, the length + is dynamically evaluated """ return self._straight_len() @@ -76,11 +83,11 @@ def __eq__(self, __o: object, tol=1e-2) -> bool: 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}]' + 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 - + """'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__() @@ -90,10 +97,10 @@ def midpoint(self): 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 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]) @@ -104,16 +111,16 @@ def as_curve(self): # Get the nodes correcly nodes = np.vstack((self.start, self.end)) - params = nodes[:, 0] + 1j*nodes[:, 1] + params = nodes[:, 0] + 1j * nodes[:, 1] return svgpath.Line(*params) - def linearize(self, n_verts_inside = 0): + 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 + + # 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: @@ -146,15 +153,14 @@ def reverse(self): 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 - """ + """Translate the edge vertices s.t. the start is at new_start""" if new_start is None: new_start = [0, 0] @@ -166,113 +172,319 @@ def snap_to(self, new_start=None): def rotate(self, angle): """Rotate edge by angle in place, using first point as a reference - Parameters: + 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 + """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 + """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 + 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 + 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') + 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]] + [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 + 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) + """Returns the dict-based representation of edges, + compatible with core -> BasePattern JSON (dict) """ properties = {"endpoints": [0, 1]} if self.label: - properties['label'] = 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.0), 1.0)) + + 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: + 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 + * 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) + * 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: @@ -286,13 +498,17 @@ def __init__(self, start=None, end=None, control_points=None, 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') + 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] + self.control_points = [ + abs_to_rel_2d(self.start, self.end, c).tolist() + for c in self.control_points + ] def length(self): """Length of Bezier curve edge""" @@ -304,24 +520,27 @@ 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}]'] + 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) + 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) + 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 + """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? + from pygarment.garmentcode.edge_factory import ( + EdgeFactory, + ) # TODOLOW: ami - better solution? + curve = self.as_curve() # Sub-curves @@ -355,7 +574,10 @@ def reverse(self): # 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] + self.control_points[0], self.control_points[1] = ( + self.control_points[1], + self.control_points[0], + ) # Update coordinates for p in self.control_points: @@ -374,8 +596,8 @@ def reflect_features(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 + Converting on the fly as exact vertex location might have been updated since + the creation of the edge """ # Get the nodes correcly if absolute: @@ -385,16 +607,20 @@ def as_curve(self, absolute=True): cp = self.control_points nodes = np.vstack(([0, 0], cp, [1, 0])) - params = nodes[:, 0] + 1j*nodes[:, 1] + params = nodes[:, 0] + 1j * nodes[:, 1] - return svgpath.QuadraticBezier(*params) if len(cp) < 2 else svgpath.CubicBezier(*params) + 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 + 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:] @@ -402,43 +628,51 @@ def linearize(self, n_verts_inside=9): 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] + 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 + 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 + 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) + 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] + [ + 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) + """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 - } + props["curvature"] = { + "type": "quadratic" if len(self.control_points) == 1 else "cubic", + "params": self.control_points, + } return ends, props diff --git a/pygarment/garmentcode/interface.py b/pygarment/garmentcode/interface.py index 23a67468..4745a27a 100644 --- a/pygarment/garmentcode/interface.py +++ b/pygarment/garmentcode/interface.py @@ -3,7 +3,7 @@ from numpy.linalg import norm import numpy as np -from pygarment.garmentcode.edge.edge import EdgeSequence, Edge +from pygarment.garmentcode.edge import EdgeSequence, Edge from pygarment.garmentcode.utils import close_enough From ad10724d4640153a3494255a96ebf1a47ef4f76f Mon Sep 17 00:00:00 2001 From: quoccuongLE Date: Sun, 16 Feb 2025 10:49:05 +0100 Subject: [PATCH 7/8] feat: remove config args in factories --- assets/garment_programs/bands/factory.py | 4 +- assets/garment_programs/bodice/factory.py | 4 +- assets/garment_programs/bodice/shirts.py | 12 ++--- assets/garment_programs/bottoms/factory.py | 4 +- .../collars/collar_halves/factory.py | 4 +- assets/garment_programs/collars/factory.py | 4 +- assets/garment_programs/meta_garment.py | 54 ++++++++++--------- .../sleeves/armhole_shapes/factory.py | 4 +- assets/garment_programs/sleeves/factory.py | 4 +- assets/garment_programs/sleeves/sleeves.py | 17 +++--- 10 files changed, 61 insertions(+), 50 deletions(-) diff --git a/assets/garment_programs/bands/factory.py b/assets/garment_programs/bands/factory.py index 9a8a170c..d95a2cce 100644 --- a/assets/garment_programs/bands/factory.py +++ b/assets/garment_programs/bands/factory.py @@ -18,9 +18,9 @@ def register_builder(key: str): return registry.register(_REGISTERED_BAND_CLS, key) -def build(config: dict = None, name: str = None, **kwargs): +def build(name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_BAND_CLS, name) - return builder(config=config, **kwargs) + return builder(**kwargs) def get_config(name: str): diff --git a/assets/garment_programs/bodice/factory.py b/assets/garment_programs/bodice/factory.py index b8055107..3ac92abb 100644 --- a/assets/garment_programs/bodice/factory.py +++ b/assets/garment_programs/bodice/factory.py @@ -18,9 +18,9 @@ def register_builder(key: str): return registry.register(_REGISTERED_BODICE_CLS, key) -def build(config: dict, name: str = None, **kwargs): +def build(config: dict = None, name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_BODICE_CLS, name) - return builder(config=config, **kwargs) + return builder(**kwargs) def get_config(name: str): diff --git a/assets/garment_programs/bodice/shirts.py b/assets/garment_programs/bodice/shirts.py index 8404cc22..8e4c8d17 100644 --- a/assets/garment_programs/bodice/shirts.py +++ b/assets/garment_programs/bodice/shirts.py @@ -83,11 +83,11 @@ def __init__(self, body, design) -> None: super().__init__(body, design, fitted=True) -@factory.register_builder("shirt") -def build_shirt(body_config: dict, design_config: dict): - return Shirt(body=body_config, design=design_config, fitted=False) +@factory.register_builder("Shirt") +def build_shirt(body: dict, design: dict): + return Shirt(body=body, design=design, fitted=False) -@factory.register_builder("fitted shirt") -def build_shirt(body_config: dict, design_config: dict): - return Shirt(body=body_config, design=design_config, fitted=True) +@factory.register_builder("FittedShirt") +def build_shirt(body: dict, design: dict): + return Shirt(body=body, design=design, fitted=True) diff --git a/assets/garment_programs/bottoms/factory.py b/assets/garment_programs/bottoms/factory.py index 50430532..781af1e6 100644 --- a/assets/garment_programs/bottoms/factory.py +++ b/assets/garment_programs/bottoms/factory.py @@ -18,9 +18,9 @@ def register_builder(key: str): return registry.register(_REGISTERED_BOTTOM_CLS, key) -def build(config: dict = None, name: str = None, **kwargs): +def build(name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_BOTTOM_CLS, name) - return builder(config=config, **kwargs) + return builder(**kwargs) def get_config(name: str): diff --git a/assets/garment_programs/collars/collar_halves/factory.py b/assets/garment_programs/collars/collar_halves/factory.py index 07a63182..2f1221b2 100644 --- a/assets/garment_programs/collars/collar_halves/factory.py +++ b/assets/garment_programs/collars/collar_halves/factory.py @@ -18,9 +18,9 @@ def register_builder(key: str): return registry.register(_REGISTERED_COLLAR_HALVES_CLS, key) -def build(config: dict = None, name: str = None, **kwargs): +def build(name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_COLLAR_HALVES_CLS, name) - return builder(config=config, **kwargs) + return builder(**kwargs) def get_config(name: str): diff --git a/assets/garment_programs/collars/factory.py b/assets/garment_programs/collars/factory.py index e5268ac1..00c0f87c 100644 --- a/assets/garment_programs/collars/factory.py +++ b/assets/garment_programs/collars/factory.py @@ -18,9 +18,9 @@ def register_builder(key: str): return registry.register(_REGISTERED_COLLAR_CLS, key) -def build(config: dict = None, name: str = None, **kwargs): +def build(name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_COLLAR_CLS, name) - return builder(config=config, **kwargs) + return builder(**kwargs) def get_config(name: str): diff --git a/assets/garment_programs/meta_garment.py b/assets/garment_programs/meta_garment.py index a4249f2e..768c2d80 100644 --- a/assets/garment_programs/meta_garment.py +++ b/assets/garment_programs/meta_garment.py @@ -1,12 +1,13 @@ -from assets.garment_programs.bodice.bodice_halves.tee import * -from assets.garment_programs.bottoms.godet import * -from assets.garment_programs.bodice.bodice_halves.bodice_halves import * -from assets.garment_programs.bottoms.pants import * -from assets.garment_programs.bands.base import * -from assets.garment_programs.bottoms.skirt_paneled import * -from assets.garment_programs.bottoms.skirt_levels import * -from assets.garment_programs.bottoms.circle_skirt import * -from assets.garment_programs.sleeves.sleeves import * +import pygarment as pyg +from assets.garment_programs.bands import * +from assets.garment_programs.bands import factory as band_factory +from assets.garment_programs.bodice import * +from assets.garment_programs.bodice import factory as bodice_factory +from assets.garment_programs.bottoms import * +from assets.garment_programs.bottoms import factory as bottom_factory +from assets.garment_programs.collars import * +from assets.garment_programs.sleeves import * + class TotalLengthError(BaseException): """Error indicating that the total length of a garment goes beyond @@ -23,7 +24,7 @@ class MetaGarment(pyg.Component): for various dresses and jumpsuit styles and fit them to the body measurements """ - def __init__(self, name, body, design) -> None: + def __init__(self, name: str, body: dict, design: dict) -> None: super().__init__(name) self.body = body self.design = design @@ -35,31 +36,37 @@ def __init__(self, name, body, design) -> None: # Upper garment if self.upper_name: - upper = globals()[self.upper_name] - self.subs = [upper(body, design)] + self.subs = [bodice_factory.build(name=self.upper_name, body=body, design=design)] # Set a label self.subs[-1].set_panel_label('body', overwrite=False) # Define Lower garment if self.lower_name: - Lower_class = globals()[self.lower_name] # NOTE: full rise for fitted tops - Lower = Lower_class(body, design, rise=1. if self.upper_name and 'Fitted' in self.upper_name else None) + Lower = bottom_factory.build( + name=self.lower_name, + body=body, + design=design, + rise=1.0 if self.upper_name and "Fitted" in self.upper_name else None, + ) else: Lower = None # Belt (or not) # TODO Adapt the rise of the lower garment to the width of the belt for correct matching if self.belt_name: - Belt_class = globals()[self.belt_name] - # Adjust rise to match the Lower garment if needed - Belt = Belt_class(body, design, Lower.get_rise() if Lower else 1.) + Belt = band_factory.build( + name=self.belt_name, + body=body, + design=design, + rise=Lower.get_rise() if Lower else 1.0, + ) self.subs.append(Belt) - # Place below the upper garment + # Place below the upper garment if len(self.subs) > 1: self.subs[-1].place_by_interface( self.subs[-1].interfaces['top'], @@ -70,7 +77,7 @@ def __init__(self, name, body, design) -> None: self.stitching_rules.append( (self.subs[-2].interfaces['bottom'], self.subs[-1].interfaces['top'])) - + # Add waist label self.subs[-1].interfaces['top'].edges.propagate_label('lower_interface') # Set panel segmentation labels @@ -89,14 +96,13 @@ def __init__(self, name, body, design) -> None: self.stitching_rules.append( (self.subs[-2].interfaces['bottom'], self.subs[-1].interfaces['top'])) - + # Add waist label if not self.belt_name: self.subs[-1].interfaces['top'].edges.propagate_label('lower_interface') # Set panel segmentation labels self.subs[-1].set_panel_label('leg', overwrite=False) - def assert_total_length(self, tol=1): """Check the total length of components""" # Check that the total length of the components are less that body height @@ -105,7 +111,7 @@ def assert_total_length(self, tol=1): if length > floor + tol: raise TotalLengthError(f'{self.__class__.__name__}::{self.name}::ERROR:' f':Total length {length} exceeds the floor length {floor}') - + # TODO these checks don't require initialization of the pattern! def assert_non_empty(self, filter_belts=True): """Check that the garment is non-empty @@ -114,10 +120,10 @@ def assert_non_empty(self, filter_belts=True): if not self.upper_name and not self.lower_name: if filter_belts or not self.belt_name: raise IncorrectElementConfiguration() - + def assert_skirt_waistband(self): """Check if a generated heavy skirt is created with a waistband""" if self.lower_name and self.lower_name in ['SkirtCircle', 'AsymmSkirtCircle', 'SkirtManyPanels']: if not (self.belt_name or self.upper_name): - raise IncorrectElementConfiguration() \ No newline at end of file + raise IncorrectElementConfiguration() diff --git a/assets/garment_programs/sleeves/armhole_shapes/factory.py b/assets/garment_programs/sleeves/armhole_shapes/factory.py index cdda9d22..7c9a0d46 100644 --- a/assets/garment_programs/sleeves/armhole_shapes/factory.py +++ b/assets/garment_programs/sleeves/armhole_shapes/factory.py @@ -18,9 +18,9 @@ def register_builder(key: str): return registry.register(_REGISTERED_ARMHOLE_CLS, key) -def build(config: dict = None, name: str = None, **kwargs): +def build(name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_ARMHOLE_CLS, name) - return builder(config=config, **kwargs) + return builder(**kwargs) def get_config(name: str): diff --git a/assets/garment_programs/sleeves/factory.py b/assets/garment_programs/sleeves/factory.py index fbd19706..13e4cf8f 100644 --- a/assets/garment_programs/sleeves/factory.py +++ b/assets/garment_programs/sleeves/factory.py @@ -18,9 +18,9 @@ def register_builder(key: str): return registry.register(_REGISTERED_SLEEVE_CLS, key) -def build(config: dict = None, name: str = None, **kwargs): +def build(name: str = None, **kwargs): builder = registry.lookup(_REGISTERED_SLEEVE_CLS, name) - return builder(config=config, **kwargs) + return builder(**kwargs) def get_config(name: str): diff --git a/assets/garment_programs/sleeves/sleeves.py b/assets/garment_programs/sleeves/sleeves.py index 211e38f5..bb68c1c5 100644 --- a/assets/garment_programs/sleeves/sleeves.py +++ b/assets/garment_programs/sleeves/sleeves.py @@ -3,10 +3,11 @@ import numpy as np from scipy.spatial.transform import Rotation as R +import pygarment as pyg from assets.garment_programs.bands import base as bands -from assets.garment_programs.sleeves.armhole_shapes import factory as armhole_factory from assets.garment_programs.sleeves import factory -import pygarment as pyg +from assets.garment_programs.sleeves.armhole_shapes import \ + factory as armhole_factory class SleevePanel(pyg.Panel): @@ -144,7 +145,11 @@ def __init__(self, tag, body, design, front_w, back_w): # bottom_angle_mix=design['opening_dir_mix']['v'], # verbose=self.verbose # ) - armhole_str_name = "ArmholeCurve" if design['sleeveless']['v'] is None else design['sleeveless']['v'] + armhole_str_name = ( + "ArmholeCurve" + if design["armhole_shape"]["v"] is None + else design["armhole_shape"]["v"] + ) front_project, front_opening = armhole_factory.build( name=armhole_str_name, incl=front_w - sleeve_balance, @@ -158,9 +163,9 @@ def __init__(self, tag, body, design, front_w, back_w): ) # back_project, back_opening = armhole( # back_w - sleeve_balance, - # connecting_width, - # angle=rest_angle, - # incl_coeff=smoothing_coeff, + # connecting_width, + # angle=rest_angle, + # incl_coeff=smoothing_coeff, # w_coeff=smoothing_coeff, # invert=not design['sleeveless']['v'], # bottom_angle_mix=design['opening_dir_mix']['v'] From e86ad298c20211260e3cbc867cf317f37a777c6b Mon Sep 17 00:00:00 2001 From: quoccuongLE Date: Sun, 16 Feb 2025 16:33:46 +0100 Subject: [PATCH 8/8] feat: Update gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 83116491..ea2f2f0f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,3 @@ output* # Nicegui .nicegui .venv/ -assets/bodies/drippy/ \ No newline at end of file