init_code
This commit is contained in:
408
pygarment/garmentcode/panel.py
Normal file
408
pygarment/garmentcode/panel.py
Normal file
@@ -0,0 +1,408 @@
|
||||
import numpy as np
|
||||
from copy import copy
|
||||
from argparse import Namespace
|
||||
from scipy.spatial.transform import Rotation as R
|
||||
|
||||
from pygarment.pattern.core import BasicPattern
|
||||
from pygarment.garmentcode.base import BaseComponent
|
||||
from pygarment.garmentcode.edge import Edge, EdgeSequence, CircleEdge
|
||||
from pygarment.garmentcode.utils import close_enough, vector_align_3D
|
||||
from pygarment.garmentcode.operators import cut_into_edge
|
||||
from pygarment.garmentcode.interface import Interface
|
||||
|
||||
|
||||
class Panel(BaseComponent):
|
||||
""" A Base class for defining a Garment component corresponding to a single
|
||||
flat fiece of fabric
|
||||
|
||||
Defined as a collection of edges on a 2D grid with specified 3D placement
|
||||
(world coordinates)
|
||||
|
||||
NOTE: All operations methods return 'self' object to allow sequential
|
||||
applications
|
||||
|
||||
"""
|
||||
def __init__(self, name, label='') -> None:
|
||||
"""Base class for panel creations
|
||||
* Name: panel name. Expected to be a unique identifier of a panel object
|
||||
* label: additional panel label (non-unique)
|
||||
"""
|
||||
super().__init__(name)
|
||||
|
||||
self.label = label
|
||||
self.translation = np.zeros(3)
|
||||
self.rotation = R.from_euler('XYZ', [0, 0, 0]) # zero rotation
|
||||
# NOTE: initiating with empty sequence allows .append() to it safely
|
||||
self.edges = EdgeSequence()
|
||||
|
||||
# Info
|
||||
def pivot_3D(self):
|
||||
"""Pivot point of a panel in 3D"""
|
||||
return self.point_to_3D([0, 0])
|
||||
|
||||
def length(self, longest_dim=False):
|
||||
"""Length of a panel element in cm
|
||||
|
||||
Defaults the to the vertical length of a 2D bounding box
|
||||
* longest_dim -- if set, returns the longest dimention out of the bounding box dimentions
|
||||
"""
|
||||
bbox = self.bbox()
|
||||
|
||||
x = abs(bbox[1][0] - bbox[0][0])
|
||||
y = abs(bbox[1][1] - bbox[0][1])
|
||||
|
||||
return max(x, y) if longest_dim else y
|
||||
|
||||
def is_self_intersecting(self):
|
||||
"""Check whether the panel has self-intersection"""
|
||||
edge_curves = []
|
||||
for e in self.edges:
|
||||
if isinstance(e, CircleEdge):
|
||||
# NOTE: Intersections for Arcs (Circle edge) fails in svgpathtools:
|
||||
# They are not well implemented in svgpathtools, see
|
||||
# https://github.com/mathandy/svgpathtools/issues/121
|
||||
# https://github.com/mathandy/svgpathtools/blob/fcb648b9bb9591d925876d3b51649fa175b40524/svgpathtools/path.py#L1960
|
||||
# Hence using linear approximation for robustness:
|
||||
edge_curves += [eseg.as_curve() for eseg in e.linearize(n_verts_inside=10)]
|
||||
else:
|
||||
edge_curves.append(e.as_curve())
|
||||
|
||||
# NOTE: simple pairwise checks of edges
|
||||
for i1 in range(0, len(edge_curves)):
|
||||
for i2 in range(i1 + 1, len(edge_curves)):
|
||||
intersect_t = edge_curves[i1].intersect(edge_curves[i2])
|
||||
|
||||
# Check exceptions -- intersection at the vertex
|
||||
for i in range(len(intersect_t)):
|
||||
t1, t2 = intersect_t[i]
|
||||
if t2 < t1:
|
||||
t1, t2 = t2, t1
|
||||
if close_enough(t1, 0) and close_enough(t2, 1):
|
||||
intersect_t[i] = None
|
||||
intersect_t = [el for el in intersect_t if el is not None]
|
||||
|
||||
if intersect_t: # Any other case of intersections
|
||||
return True
|
||||
return False
|
||||
|
||||
# ANCHOR - Operations -- update object in-place
|
||||
def set_panel_label(self, label: str, overwrite=True):
|
||||
"""If overwrite is not enabled, only updates the label if it's empty."""
|
||||
if not self.label or overwrite:
|
||||
self.label = label
|
||||
|
||||
def set_pivot(self, point_2d, replicate_placement=False):
|
||||
"""Specify 2D point w.r.t. panel local space
|
||||
to be used as pivot for translation and rotation
|
||||
|
||||
Parameters:
|
||||
* point_2d -- desired point 2D point w.r.t current pivot (origin)
|
||||
of panel local space
|
||||
* replicate_placement -- will replicate the location of the panel
|
||||
as it was before pivot change
|
||||
default - False (no adjustment, the panel may "jump" in 3D)
|
||||
"""
|
||||
point_2d = copy(point_2d) # Remove unwanted object reference
|
||||
# In case an actual vertex was used as a target point
|
||||
|
||||
if replicate_placement:
|
||||
self.translation = self.point_to_3D(point_2d)
|
||||
# FIXME Replicate rotation
|
||||
|
||||
# UPD vertex locations relative to new pivot
|
||||
for v in self.edges.verts():
|
||||
v[0] -= int(point_2d[0])
|
||||
v[1] -= int(point_2d[1])
|
||||
|
||||
def top_center_pivot(self):
|
||||
"""One of the most useful pivots
|
||||
is the one in the middle of the top edge of the panel
|
||||
"""
|
||||
vertices = np.asarray(self.edges.verts())
|
||||
|
||||
# out of 2D bounding box sides' midpoints choose the one that is
|
||||
# highest in 3D
|
||||
top_right = vertices.max(axis=0)
|
||||
low_left = vertices.min(axis=0)
|
||||
mid_x = (top_right[0] + low_left[0]) / 2
|
||||
mid_y = (top_right[1] + low_left[1]) / 2
|
||||
mid_points_2D = [
|
||||
[mid_x, top_right[1]],
|
||||
[mid_x, low_left[1]],
|
||||
[top_right[0], mid_y],
|
||||
[low_left[0], mid_y]
|
||||
]
|
||||
mid_points_3D = np.vstack(tuple(
|
||||
[self.point_to_3D(coords) for coords in mid_points_2D]
|
||||
))
|
||||
top_mid_point = mid_points_3D[:, 1].argmax()
|
||||
|
||||
self.set_pivot(mid_points_2D[top_mid_point])
|
||||
return self
|
||||
|
||||
def translate_by(self, delta_vector):
|
||||
"""Translate panel by a vector"""
|
||||
self.translation = self.translation + np.array(delta_vector)
|
||||
# NOTE: One may also want to have autonorm only on the assembly?
|
||||
self.autonorm()
|
||||
return self
|
||||
|
||||
def translate_to(self, new_translation):
|
||||
"""Set panel translation to be exactly that vector"""
|
||||
self.translation = np.asarray(new_translation)
|
||||
self.autonorm()
|
||||
return self
|
||||
|
||||
def rotate_by(self, delta_rotation: R):
|
||||
"""Rotate panel by a given rotation
|
||||
* delta_rotation: scipy rotation object
|
||||
"""
|
||||
self.rotation = delta_rotation * self.rotation
|
||||
self.autonorm()
|
||||
return self
|
||||
|
||||
def rotate_to(self, new_rot: R):
|
||||
"""Set panel rotation to be exactly the given rotation
|
||||
* new_rot: scipy rotation object
|
||||
"""
|
||||
if not isinstance(new_rot, R):
|
||||
raise ValueError(f'{self.__class__.__name__}::ERROR::Only accepting rotations in scipy format')
|
||||
self.rotation = new_rot
|
||||
self.autonorm()
|
||||
return self
|
||||
|
||||
def rotate_align(self, vector):
|
||||
"""Set panel rotation s.t. it's norm is aligned with a given 3D
|
||||
vector"""
|
||||
|
||||
vector = np.asarray(vector)
|
||||
vector = vector / np.linalg.norm(vector)
|
||||
n = self.norm()
|
||||
self.rotate_by(vector_align_3D(n, vector))
|
||||
return self
|
||||
|
||||
def center_x(self):
|
||||
"""Adjust translation over x s.t. the center of the panel is aligned
|
||||
with the Y axis (center of the body)"""
|
||||
|
||||
center_3d = self.point_to_3D(self._center_2D())
|
||||
self.translation[0] += -center_3d[0]
|
||||
return self
|
||||
|
||||
def autonorm(self):
|
||||
"""Update right/wrong side orientation, s.t. the normal of the surface
|
||||
looks outside he world origin,
|
||||
taking into account the shape and the global position.
|
||||
|
||||
This should provide correct panel orientation in most cases.
|
||||
|
||||
NOTE: for best results, call autonorm after translation
|
||||
specification
|
||||
"""
|
||||
norm_dr = self.norm()
|
||||
|
||||
# NOTE: Nothing happens if self.translation is zero
|
||||
if np.dot(norm_dr, self.translation) < 0:
|
||||
# Swap if wrong
|
||||
self.edges.reverse()
|
||||
|
||||
def mirror(self, axis=None):
|
||||
"""Swap this panel with its mirror image
|
||||
|
||||
Axis specifies 2D axis to swap around: Y axis by default
|
||||
"""
|
||||
if axis is None:
|
||||
axis = [0, 1]
|
||||
# Case Around Y
|
||||
if close_enough(axis[0], tol=1e-4): # reflection around Y
|
||||
|
||||
# Vertices
|
||||
self.edges.reflect([0, 0], [0, 1])
|
||||
|
||||
# Position
|
||||
self.translation[0] *= -1
|
||||
|
||||
# Rotations
|
||||
curr_euler = self.rotation.as_euler('XYZ')
|
||||
curr_euler[1] *= -1
|
||||
curr_euler[2] *= -1
|
||||
self.rotate_to(R.from_euler('XYZ', curr_euler))
|
||||
|
||||
# Fix right/wrong side
|
||||
self.autonorm()
|
||||
else:
|
||||
# TODO Any other axis
|
||||
raise NotImplementedError(f'{self.name}::ERROR::Mirrowing over arbitrary axis is not implemented')
|
||||
return self
|
||||
|
||||
def add_dart(self, dart_shape, edge, offset, right=True, edge_seq=None, int_edge_seq=None, ):
|
||||
""" Shortcut for adding a dart to a panel:
|
||||
* Performs insertion of the dart_shape in the given edge (parameters are the same
|
||||
as in pyp.ops.cut_into_edge)
|
||||
* Creates stitch to connect the dart sides
|
||||
* Modifies edge_sequnces with full set (edge_seq) or only the interface part (int_edge_seq)
|
||||
of the created edges, if those are provided
|
||||
|
||||
Returns new edges after insertion, and the interface part (excludes dart edges)
|
||||
"""
|
||||
edges_new, dart_edges, int_new = cut_into_edge(
|
||||
dart_shape,
|
||||
edge,
|
||||
offset=offset,
|
||||
right=right)
|
||||
|
||||
self.stitching_rules.append(
|
||||
(Interface(self, dart_edges[0]), Interface(self, dart_edges[1])))
|
||||
|
||||
# Update the edges if given
|
||||
if edge_seq is not None:
|
||||
edge_seq.substitute(edge, edges_new)
|
||||
edges_new = edge_seq
|
||||
if int_edge_seq is not None:
|
||||
int_edge_seq.substitute(edge, int_new)
|
||||
int_new = int_edge_seq
|
||||
|
||||
return edges_new, int_new
|
||||
|
||||
# ANCHOR - Build the panel -- get serializable representation
|
||||
def assembly(self):
|
||||
"""Convert panel into serialazable representation
|
||||
|
||||
NOTE: panel EdgeSequence is assumed to be a single loop of edges
|
||||
"""
|
||||
# FIXME Some panels have weird resulting alignemnt when th
|
||||
# is pivot setup is removed -- there is a bug somewhere
|
||||
|
||||
# always start from zero for consistency between panels
|
||||
self.set_pivot(self.edges[0].start, replicate_placement=True)
|
||||
|
||||
# Basics
|
||||
panel = Namespace(
|
||||
translation=self.translation.tolist(),
|
||||
rotation=self.rotation.as_euler('XYZ', degrees=True).tolist(),
|
||||
vertices=[self.edges[0].start],
|
||||
edges=[])
|
||||
|
||||
for i in range(len(self.edges)):
|
||||
vertices, edge = self.edges[i].assembly()
|
||||
|
||||
# add new vertices
|
||||
if panel.vertices[-1] == vertices[0]: # We care if both point to the same vertex location, not necessarily the same vertex object
|
||||
vert_shift = len(panel.vertices) - 1 # first edge vertex = last vertex already in the loop
|
||||
panel.vertices += vertices[1:]
|
||||
else:
|
||||
vert_shift = len(panel.vertices)
|
||||
panel.vertices += vertices
|
||||
|
||||
# upd vertex references in edges according to new vertex ids in
|
||||
# the panel vertex loop
|
||||
edge['endpoints'] = [id + vert_shift for id in edge['endpoints']]
|
||||
|
||||
edge_shift = len(panel.edges) # before adding new ones
|
||||
self.edges[i].geometric_id = edge_shift # remember the mapping of logical edge to geometric id in panel loop
|
||||
panel.edges.append(edge)
|
||||
|
||||
# Check closing of the loop and upd vertex reference for the last edge
|
||||
if panel.vertices[-1] == panel.vertices[0]:
|
||||
panel.vertices.pop()
|
||||
panel.edges[-1]['endpoints'][-1] = 0
|
||||
|
||||
# Add panel label, if known
|
||||
if self.label:
|
||||
panel.label = self.label
|
||||
|
||||
spattern = BasicPattern()
|
||||
spattern.name = self.name
|
||||
spattern.pattern['panels'] = {self.name: vars(panel)}
|
||||
|
||||
# Assembly stitching info (panel might have inner stitches)
|
||||
spattern.pattern['stitches'] = self.stitching_rules.assembly()
|
||||
|
||||
return spattern
|
||||
|
||||
# ANCHOR utils
|
||||
def _center_2D(self, n_verts_inside = 3):
|
||||
"""Approximate Location of the panel center.
|
||||
|
||||
NOTE: uses crude linear approximation for curved edges,
|
||||
n_verts_inside = number of vertices (excluding the start
|
||||
and end vertices) used to create a linearization of an edge
|
||||
"""
|
||||
# NOTE: assuming that edges are organized in a loop and share vertices
|
||||
lin_edges = EdgeSequence([e.linearize(n_verts_inside)
|
||||
for e in self.edges])
|
||||
verts = lin_edges.verts()
|
||||
|
||||
return np.mean(verts, axis=0)
|
||||
|
||||
def point_to_3D(self, point_2d):
|
||||
"""Calculate 3D location of a point given in the local 2D plane """
|
||||
point_2d = np.asarray(point_2d)
|
||||
if len(point_2d) == 2:
|
||||
point_2d = np.append(point_2d, 0)
|
||||
|
||||
point_3d = self.rotation.apply(point_2d)
|
||||
point_3d += self.translation
|
||||
return point_3d
|
||||
|
||||
def norm(self):
|
||||
"""Normal direction for the current panel using bounding box"""
|
||||
|
||||
# To make norm evaluation work for non-convex panels
|
||||
# Determine points located on bounding box (b_verts_2d), compute
|
||||
# norm of consecutive b_verts_3d and the b_verts_3d mean (b_center_3d),
|
||||
# then weight the norms.
|
||||
# The dominant norm direction should be the correct one
|
||||
|
||||
_, b_verts_2d = self.edges.bbox()
|
||||
b_verts_3d = [self.point_to_3D(bv_2d) for bv_2d in b_verts_2d]
|
||||
b_center_3d = np.mean((b_verts_3d), axis=0)
|
||||
|
||||
norms = []
|
||||
num_b_verts_3d = len(b_verts_3d)
|
||||
for i in range(num_b_verts_3d):
|
||||
vert_0 = b_verts_3d[i]
|
||||
vert_1 = b_verts_3d[(i + 1) % num_b_verts_3d]
|
||||
# Pylance + NP error for unreachanble code -- see https://github.com/numpy/numpy/issues/22146
|
||||
# Works ok for numpy 1.23.4+
|
||||
norm = np.cross(vert_0 - b_center_3d, vert_1 - b_center_3d)
|
||||
norm /= np.linalg.norm(norm)
|
||||
norms.append(norm)
|
||||
|
||||
# Current norm direction
|
||||
avg_norm = sum(norms) / len(norms)
|
||||
|
||||
if close_enough(np.linalg.norm(avg_norm), 0):
|
||||
# Indecisive averaging, so using just one of the norms
|
||||
# NOTE: sometimes happens on thin arcs
|
||||
avg_norm = norms[0]
|
||||
if self.verbose:
|
||||
print(f'{self.__class__.__name__}::{self.name}::WARNING::Norm evaluation failed, assigning norm based on the first edge')
|
||||
|
||||
final_norm = avg_norm / np.linalg.norm(avg_norm)
|
||||
|
||||
# solve float errors
|
||||
for i, ni in enumerate(final_norm):
|
||||
if np.isclose([ni], [0.0]):
|
||||
final_norm[i] = 0.0
|
||||
|
||||
return final_norm
|
||||
|
||||
def bbox(self):
|
||||
"""Evaluate 2D bounding box"""
|
||||
# Using curve linearization for more accurate approximation of bbox
|
||||
lin_edges = EdgeSequence([e.linearize() for e in self.edges])
|
||||
verts_2d = np.asarray(lin_edges.verts())
|
||||
|
||||
return verts_2d.min(axis=0), verts_2d.max(axis=0)
|
||||
|
||||
|
||||
def bbox3D(self):
|
||||
"""Evaluate 3D bounding box of the current panel"""
|
||||
|
||||
# Using curve linearization for more accurate approximation of bbox
|
||||
lin_edges = EdgeSequence([e.linearize() for e in self.edges])
|
||||
verts_2d = lin_edges.verts()
|
||||
verts_3d = np.asarray([self.point_to_3D(v) for v in verts_2d])
|
||||
|
||||
return verts_3d.min(axis=0), verts_3d.max(axis=0)
|
||||
Reference in New Issue
Block a user