974 lines
43 KiB
Python
974 lines
43 KiB
Python
"""
|
|
Module for basic operations on patterns
|
|
"""
|
|
# Basic
|
|
import copy
|
|
import errno
|
|
import json
|
|
import numpy as np
|
|
import os
|
|
import random
|
|
import svgpathtools as svgpath
|
|
|
|
# My
|
|
|
|
from . import rotation as rotation_tools
|
|
from . import utils
|
|
|
|
standard_filenames = [
|
|
'specification', # e.g. used by dataset generation
|
|
'template',
|
|
'prediction'
|
|
]
|
|
|
|
pattern_spec_template = {
|
|
'pattern': {
|
|
'panels': {},
|
|
'stitches': []
|
|
},
|
|
'parameters': {},
|
|
'parameter_order': [],
|
|
'properties': { # these are to be ensured when pattern content is updated directly
|
|
'curvature_coords': 'relative',
|
|
'normalize_panel_translation': False,
|
|
'normalized_edge_loops': True, # will trigger edge loop normalization on reload
|
|
'units_in_meter': 100 # cm
|
|
}
|
|
}
|
|
|
|
panel_spec_template = {
|
|
'translation': [ 0, 0, 0 ],
|
|
'rotation': [ 0, 0, 0 ],
|
|
'vertices': [],
|
|
'edges': []
|
|
}
|
|
|
|
class EmptyPatternError(BaseException):
|
|
def __init__(self, *args: object) -> None:
|
|
super().__init__(*args)
|
|
|
|
# ------------ Patterns --------
|
|
class BasicPattern(object):
|
|
"""Loading & serializing of a pattern specification in custom JSON format.
|
|
Input:
|
|
* Pattern template in custom JSON format
|
|
Output representations:
|
|
* Pattern instance in custom JSON format
|
|
* In the current state
|
|
|
|
Not implemented:
|
|
* Convertion to NN-friendly format
|
|
* Support for patterns with darts
|
|
"""
|
|
|
|
# ------------ Interface -------------
|
|
|
|
def __init__(self, pattern_file=None):
|
|
|
|
self.spec_file = pattern_file
|
|
|
|
if pattern_file is not None: # load pattern from file
|
|
self.path = os.path.dirname(pattern_file)
|
|
self.name = BasicPattern.name_from_path(pattern_file)
|
|
self.reloadJSON()
|
|
else: # create empty pattern
|
|
self.path = None
|
|
self.name = self.__class__.__name__
|
|
self.spec = copy.deepcopy(pattern_spec_template)
|
|
self.pattern = self.spec['pattern']
|
|
self.properties = self.spec['properties'] # mandatory part
|
|
|
|
def reloadJSON(self):
|
|
"""(Re)loads pattern info from spec file.
|
|
Useful when spec is updated from outside"""
|
|
if self.spec_file is None:
|
|
print('BasicPattern::WARNING::{}::Pattern is not connected to any file. Reloadig from file request ignored.'.format(
|
|
self.name
|
|
))
|
|
return
|
|
|
|
with open(self.spec_file, 'r') as f_json:
|
|
self.spec = json.load(f_json)
|
|
self.pattern = self.spec['pattern']
|
|
self.properties = self.spec['properties'] # mandatory part
|
|
|
|
# template normalization - panel translations and curvature to relative coords
|
|
self._normalize_template()
|
|
|
|
def serialize(self, path, to_subfolder=True, tag='', empty_ok=False):
|
|
|
|
if not empty_ok and len(self.panel_order()) == 0:
|
|
raise RuntimeError(f'{self.__class__.__name__}::ERROR::Asked to save an empty pattern')
|
|
|
|
# log context
|
|
if tag:
|
|
tag = '_' + tag
|
|
if to_subfolder:
|
|
log_dir = os.path.join(path, self.name + tag) # NOTE Added change
|
|
try:
|
|
os.makedirs(log_dir)
|
|
except OSError as e:
|
|
if e.errno != errno.EEXIST:
|
|
raise
|
|
else:
|
|
log_dir = path
|
|
|
|
spec_file = os.path.join(log_dir, (self.name + tag + '_specification.json'))
|
|
|
|
# Save specification
|
|
with open(spec_file, 'w') as f_json:
|
|
json.dump(self.spec, f_json, indent=2)
|
|
|
|
|
|
|
|
# print('{}::{}::Pattern saved to {}'.format(self.__class__.__name__, self.name, spec_file))
|
|
|
|
return log_dir
|
|
|
|
@staticmethod
|
|
def name_from_path(pattern_file):
|
|
name = os.path.splitext(os.path.basename(pattern_file))[0]
|
|
if name.endswith('_specification'):
|
|
name = name.split('_specification')[0]
|
|
if name in standard_filenames: # use name of directory instead
|
|
path = os.path.dirname(pattern_file)
|
|
name = os.path.basename(os.path.normpath(path))
|
|
return name
|
|
|
|
# --------- Info ------------------------
|
|
def panel_order(self, force_update=False):
|
|
"""
|
|
Return current agreed-upon order of panels
|
|
* if not defined in the pattern or if 'force_update' is enabled, re-evaluate it based on curent panel translation and save
|
|
"""
|
|
if 'panel_order' not in self.pattern or force_update:
|
|
self.pattern['panel_order'] = self.define_panel_order()
|
|
return self.pattern['panel_order']
|
|
|
|
def define_panel_order(self, name_list=None, location_dict=None, dim=0, tolerance=10):
|
|
""" (Recursive) Ordering of the panels based on their 3D translation values.
|
|
* Using cm as units for tolerance (when the two coordinates are considered equal)
|
|
* Sorting by all dims as keys X -> Y -> Z (left-right (looking from Z) then down-up then back-front)
|
|
* based on the fuzzysort suggestion here https://stackoverflow.com/a/24024801/11206726"""
|
|
|
|
if name_list is None: # start from beginning
|
|
name_list = self.pattern['panels'].keys()
|
|
if not name_list:
|
|
return []
|
|
if location_dict is None: # obtain location for all panels to use in sorting further
|
|
location_dict = {}
|
|
for name in name_list:
|
|
location_dict[name], _ = self._panel_universal_transtation(name)
|
|
|
|
# consider only translations of the requested panel names
|
|
reference = [location_dict[panel_n][dim] for panel_n in name_list]
|
|
sorted_couple = sorted(zip(reference, name_list)) # sorts according to the first list
|
|
sorted_reference, sorted_names = zip(*sorted_couple)
|
|
sorted_names = list(sorted_names)
|
|
|
|
if (dim + 1) < 3: # 3D is max
|
|
# re-sort values by next dimention if they have similar values in current dimention
|
|
fuzzy_start, fuzzy_end = 0, 0 # init both in case we start from 1 panel to sort
|
|
for fuzzy_end in range(1, len(sorted_reference)):
|
|
if sorted_reference[fuzzy_end] - sorted_reference[fuzzy_start] >= tolerance:
|
|
# the range of similar values is completed
|
|
if fuzzy_end - fuzzy_start > 1:
|
|
sorted_names[fuzzy_start:fuzzy_end] = self.define_panel_order(
|
|
sorted_names[fuzzy_start:fuzzy_end], location_dict, dim + 1, tolerance)
|
|
fuzzy_start = fuzzy_end # start counting similar values anew
|
|
|
|
# take care of the tail
|
|
if fuzzy_start != fuzzy_end:
|
|
sorted_names[fuzzy_start:] = self.define_panel_order(
|
|
sorted_names[fuzzy_start:], location_dict, dim + 1, tolerance)
|
|
|
|
return sorted_names
|
|
|
|
# -- sub-utils --
|
|
def _edge_as_vector(self, vertices, edge_dict):
|
|
"""Represent edge as vector of fixed length:
|
|
* First 2 elements: Vector endpoint.
|
|
Original edge endvertex positions can be restored if edge vector is added to the start point,
|
|
which in turn could be obtained from previous edges in the panel loop
|
|
* Next 2 elements: Curvature values
|
|
Given in relative coordinates. With zeros if edge is not curved
|
|
|
|
"""
|
|
edge_verts = vertices[edge_dict['endpoints']]
|
|
edge_vector = edge_verts[1] - edge_verts[0]
|
|
curvature = np.array(edge_dict['curvature']) if 'curvature' in edge_dict else [0, 0]
|
|
|
|
return np.concatenate([edge_vector, curvature])
|
|
|
|
def _edge_as_curve(self, vertices, edge):
|
|
start = vertices[edge['endpoints'][0]]
|
|
end = vertices[edge['endpoints'][1]]
|
|
if ('curvature' in edge):
|
|
# NOTE: supports old curves
|
|
if isinstance(edge['curvature'], list) or edge['curvature']['type'] == 'quadratic':
|
|
control_scale = self._flip_y(edge['curvature'] if isinstance(edge['curvature'], list) else edge['curvature']['params'][0])
|
|
control_point = utils.rel_to_abs_2d(start, end, control_scale)
|
|
return svgpath.QuadraticBezier(*utils.list_to_c([start, control_point, end]))
|
|
elif edge['curvature']['type'] == 'circle': # Assuming circle
|
|
# https://svgwrite.readthedocs.io/en/latest/classes/path.html#svgwrite.path.Path.push_arc
|
|
|
|
radius, large_arc, right = edge['curvature']['params']
|
|
|
|
return svgpath.Arc(
|
|
utils.list_to_c(start), radius + 1j*radius,
|
|
rotation=0,
|
|
large_arc=large_arc,
|
|
sweep=not right,
|
|
end=utils.list_to_c(end)
|
|
)
|
|
|
|
elif edge['curvature']['type'] == 'cubic':
|
|
cps = []
|
|
for p in edge['curvature']['params']:
|
|
control_scale = self._flip_y(p)
|
|
control_point = utils.rel_to_abs_2d(start, end, control_scale)
|
|
cps.append(control_point)
|
|
|
|
return svgpath.CubicBezier(*utils.list_to_c([start, *cps, end]))
|
|
|
|
else:
|
|
raise NotImplementedError(f'{self.__class__.__name__}::Unknown curvature type {edge["curvature"]["type"]}')
|
|
|
|
else:
|
|
return svgpath.Line(*utils.list_to_c([start, end]))
|
|
|
|
@staticmethod
|
|
def _point_in_3D(local_coord, rotation, translation):
|
|
"""Apply 3D transformation to the point given in 2D local coordinated, e.g. on the panel
|
|
* rotation is expected to be given in 'xyz' Euler anges (as in Autodesk Maya) or as 3x3 matrix"""
|
|
|
|
# 2D->3D local
|
|
local_coord = np.append(local_coord, 0)
|
|
|
|
# Rotate
|
|
rotation = np.array(rotation)
|
|
if rotation.size == 3: # transform Euler angles to matrix
|
|
rotation = rotation_tools.euler_xyz_to_R(rotation)
|
|
# otherwise we already have the matrix
|
|
elif rotation.size != 9:
|
|
raise ValueError('BasicPattern::ERROR::You need to provide Euler angles or Rotation matrix for _point_in_3D(..)')
|
|
rotated_point = rotation.dot(local_coord)
|
|
|
|
# translate
|
|
return rotated_point + translation
|
|
|
|
def _panel_universal_transtation(self, panel_name):
|
|
"""Return a universal 3D translation of the panel (e.g. to be used in judging the panel order).
|
|
Universal translation it defined as world 3D location of mid-point of the top (in 3D) of the panel (2D) bounding box.
|
|
* Assumptions:
|
|
* In most cases, top-mid-point of a panel corresponds to body landmarks (e.g. neck, middle of an arm, waist)
|
|
and thus is mostly stable across garment designs.
|
|
* 3D location of a panel is placing this panel around the body in T-pose
|
|
* Function result is independent from the current choice of the local coordinate system of the panel
|
|
"""
|
|
panel = self.pattern['panels'][panel_name]
|
|
vertices = np.array(panel['vertices'])
|
|
|
|
# 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]
|
|
]
|
|
rot_matrix = rotation_tools.euler_xyz_to_R(panel['rotation']) # calculate once for all points
|
|
mid_points_3D = np.vstack(tuple(
|
|
[self._point_in_3D(coords, rot_matrix, panel['translation']) for coords in mid_points_2D]
|
|
))
|
|
top_mid_point = mid_points_3D[:, 1].argmax()
|
|
|
|
return mid_points_3D[top_mid_point], np.array(mid_points_2D[top_mid_point])
|
|
|
|
# --------- Pattern operations (changes inner dicts) ----------
|
|
def _normalize_template(self):
|
|
"""
|
|
Updated template definition for convenient processing:
|
|
* Converts curvature coordinates to realitive ones (in edge frame) -- for easy length scaling
|
|
* snaps each panel center to (0, 0) if requested in props
|
|
* scales everything to cm
|
|
"""
|
|
if self.properties['curvature_coords'] == 'absolute':
|
|
for panel in self.pattern['panels']:
|
|
# convert curvature
|
|
vertices = self.pattern['panels'][panel]['vertices']
|
|
edges = self.pattern['panels'][panel]['edges']
|
|
for edge in edges:
|
|
if 'curvature' in edge:
|
|
edge['curvature'] = utils.abs_to_rel_2d(
|
|
vertices[edge['endpoints'][0]],
|
|
vertices[edge['endpoints'][1]],
|
|
edge['curvature']
|
|
)
|
|
# now we have new property
|
|
self.properties['curvature_coords'] = 'relative'
|
|
|
|
if 'units_in_meter' in self.properties:
|
|
if self.properties['units_in_meter'] != 100:
|
|
for panel in self.pattern['panels']:
|
|
self._normalize_panel_scaling(panel, self.properties['units_in_meter'])
|
|
# now we have cm
|
|
self.properties['original_units_in_meter'] = self.properties['units_in_meter']
|
|
self.properties['units_in_meter'] = 100
|
|
print('WARNING: pattern units converted to cm')
|
|
else:
|
|
print('WARNING: units not specified in the pattern. Scaling normalization was not applied')
|
|
|
|
# after curvature is converted!!
|
|
# Only if requested
|
|
if ('normalize_panel_translation' in self.properties
|
|
and self.properties['normalize_panel_translation']):
|
|
print('Normalizing translation!')
|
|
self.properties['normalize_panel_translation'] = False # one-time use property. Preverts rotation issues on future reads
|
|
for panel in self.pattern['panels']:
|
|
# put origin in the middle of the panel--
|
|
offset = self._normalize_panel_translation(panel)
|
|
# udpate translation vector
|
|
original = self.pattern['panels'][panel]['translation']
|
|
self.pattern['panels'][panel]['translation'] = [
|
|
original[0] + offset[0],
|
|
original[1] + offset[1],
|
|
original[2],
|
|
]
|
|
|
|
# Recalculate origins and traversal order of panel edge loops if not normalized already
|
|
if ('normalized_edge_loops' not in self.properties
|
|
or not self.properties['normalized_edge_loops']):
|
|
print('{}::WARNING::normalizing the order and origin choice for edge loops in panels'.format(self.__class__.__name__))
|
|
self.properties['normalized_edge_loops'] = True
|
|
for panel in self.pattern['panels']:
|
|
self._normalize_edge_loop(panel)
|
|
|
|
# Recalculate panel order if not given already
|
|
self.panel_order()
|
|
|
|
def _normalize_panel_translation(self, panel_name):
|
|
""" Convert panel vertices to local coordinates:
|
|
Shifts all panel vertices s.t. origin is at the center of the panel
|
|
"""
|
|
panel = self.pattern['panels'][panel_name]
|
|
vertices = np.asarray(panel['vertices'])
|
|
offset = np.mean(vertices, axis=0)
|
|
vertices = vertices - offset
|
|
|
|
panel['vertices'] = vertices.tolist()
|
|
|
|
return offset
|
|
|
|
def _normalize_panel_scaling(self, panel_name, units_in_meter):
|
|
"""Convert all panel info to cm. I assume that curvature is alredy converted to relative coords -- scaling does not need update"""
|
|
scaling = 100 / units_in_meter
|
|
# vertices
|
|
vertices = np.array(self.pattern['panels'][panel_name]['vertices'])
|
|
vertices = scaling * vertices
|
|
self.pattern['panels'][panel_name]['vertices'] = vertices.tolist()
|
|
|
|
# translation
|
|
translation = self.pattern['panels'][panel_name]['translation']
|
|
self.pattern['panels'][panel_name]['translation'] = [scaling * coord for coord in translation]
|
|
|
|
def _normalize_edge_loop(self, panel_name):
|
|
"""
|
|
* Re-order edges s.t. the edge loop starts from low-left vertex
|
|
* Make the edge loop follow counter-clockwise direction (uniform traversal)
|
|
"""
|
|
panel = self.pattern['panels'][panel_name]
|
|
vertices = np.array(panel['vertices'])
|
|
|
|
# Loop Origin
|
|
loop_origin_id = self._vert_at_left_corner(vertices)
|
|
print('{}:{}: Origin: {} -> {}'.format(
|
|
self.name, panel_name, panel['edges'][0]['endpoints'][0], loop_origin_id))
|
|
|
|
rotated_edges, rotated_edge_ids = self._rotate_edges(
|
|
panel['edges'], list(range(len(panel['edges']))), loop_origin_id)
|
|
panel['edges'] = rotated_edges
|
|
|
|
# Panel flip for uniform edge loop order (and normal direction)
|
|
first_edge = self._edge_as_vector(vertices, rotated_edges[0])[:2]
|
|
last_edge = self._edge_as_vector(vertices, rotated_edges[-1])[:2]
|
|
flipped = False
|
|
# due to the choice of origin (at the corner), first & last edge cross-product will reliably show panel normal direction
|
|
if np.cross(first_edge, last_edge) > 0: # should be negative -- counterclockwise
|
|
print('{}::{}::panel <{}> flipped'.format(
|
|
self.__class__.__name__, self.name, panel_name
|
|
))
|
|
flipped = True
|
|
|
|
# Vertices
|
|
vertices[:, 0] = - vertices[:, 0] # flip by X coordinate -- we'll rotate around Y
|
|
panel['vertices'] = vertices.tolist()
|
|
|
|
# Edges
|
|
# new loop origin after update
|
|
loop_origin_id = self._vert_at_left_corner(vertices)
|
|
print('{}:{}: Origin: {} -> {}'.format(
|
|
self.name, panel_name, panel['edges'][0]['endpoints'][0], loop_origin_id))
|
|
|
|
rotated_edges, rotated_edge_ids = self._rotate_edges(rotated_edges, rotated_edge_ids, loop_origin_id)
|
|
panel['edges'] = rotated_edges
|
|
# update the curvatures in edges as they changed left\right symmetry in 3D
|
|
for edge_id in range(len(rotated_edges)):
|
|
if 'curvature' in panel['edges'][edge_id]:
|
|
curvature = panel['edges'][edge_id]['curvature']
|
|
# YES!! Only one of the curvature coordinates need update at this point
|
|
panel['edges'][edge_id]['curvature'][1] = -curvature[1]
|
|
|
|
# Panel translation and rotation -- local coord frame changed!
|
|
panel['translation'][0] -= 2 * panel['translation'][0]
|
|
|
|
panel_R = rotation_tools.euler_xyz_to_R(panel['rotation'])
|
|
flip_R = np.eye(3)
|
|
flip_R[0, 0] = flip_R[2, 2] = -1 # by 180 around Y
|
|
|
|
panel['rotation'] = rotation_tools.R_to_euler(panel_R * flip_R)
|
|
|
|
# Stitches -- update the edge references according to the new ids
|
|
if 'stitches' in self.pattern.keys():
|
|
for stitch_id in range(len(self.pattern['stitches'])):
|
|
for side_id in [0, 1]:
|
|
if self.pattern['stitches'][stitch_id][side_id]['panel'] == panel_name:
|
|
old_edge_id = self.pattern['stitches'][stitch_id][side_id]['edge']
|
|
self.pattern['stitches'][stitch_id][side_id]['edge'] = rotated_edge_ids[old_edge_id]
|
|
|
|
return rotated_edge_ids, flipped
|
|
|
|
# -- sub-utils --
|
|
def _edge_length(self, panel, edge):
|
|
panel = self.pattern['panels'][panel]
|
|
v_id_start, v_id_end = tuple(panel['edges'][edge]['endpoints'])
|
|
v_start, v_end = np.array(panel['vertices'][v_id_start]), \
|
|
np.array(panel['vertices'][v_id_end])
|
|
|
|
return np.linalg.norm(v_end - v_start)
|
|
|
|
@staticmethod
|
|
def _vert_at_left_corner(vertices):
|
|
"""
|
|
Find, which vertex is in the left corner
|
|
* Determenistic process
|
|
"""
|
|
left_corner = np.min(vertices, axis=0)
|
|
vertices = vertices - left_corner
|
|
|
|
# choose the one closest to zero (=low-left corner) as new origin
|
|
verts_norms = np.linalg.norm(vertices, axis=1) # numpy 1.9+
|
|
origin_id = np.argmin(verts_norms)
|
|
|
|
return origin_id
|
|
|
|
@staticmethod
|
|
def _rotate_edges(edges, edge_ids, new_origin_id):
|
|
"""
|
|
Rotate provided list of edges s.t. the first edge starts from vertex with id = new_origin_id
|
|
Map old edge_ids to new ones accordingly
|
|
* edges expects list of edges structures
|
|
"""
|
|
|
|
first_edge_orig_id = [idx for idx, edge in enumerate(edges) if edge['endpoints'][0] == new_origin_id]
|
|
|
|
first_edge_orig_id = first_edge_orig_id[0]
|
|
rotated_edges = edges[first_edge_orig_id:] + edges[:first_edge_orig_id]
|
|
|
|
# map from old ids to new ids
|
|
rotated_edge_ids = edge_ids[(len(rotated_edges) - first_edge_orig_id):] + edge_ids[:(len(rotated_edges) - first_edge_orig_id)]
|
|
|
|
return rotated_edges, rotated_edge_ids
|
|
|
|
def _restore(self, backup_copy):
|
|
"""Restores spec structure from given backup copy
|
|
Makes a full copy of backup to avoid accidential corruption of backup
|
|
"""
|
|
self.spec = copy.deepcopy(backup_copy)
|
|
self.pattern = self.spec['pattern']
|
|
self.properties = self.spec['properties'] # mandatory part
|
|
|
|
# -------- Checks ------------
|
|
def is_self_intersecting(self):
|
|
"""returns True if any of the pattern panels are self-intersecting"""
|
|
return any(map(self._is_panel_self_intersecting, self.pattern['panels']))
|
|
|
|
def _is_panel_self_intersecting(self, panel_name, n_vert_approximation=10):
|
|
"""Checks whatever a given panel contains intersecting edges
|
|
"""
|
|
panel = self.pattern['panels'][panel_name]
|
|
vertices = np.array(panel['vertices'])
|
|
|
|
edge_curves = []
|
|
for e in panel['edges']:
|
|
curve = self._edge_as_curve(vertices, e)
|
|
|
|
if isinstance(curve, svgpath.Arc):
|
|
# 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:
|
|
n = n_vert_approximation + 1
|
|
tvals = np.linspace(0, 1, n, endpoint=False)[1:]
|
|
edge_verts = [curve.point(t) for t in tvals]
|
|
edge_curves += [svgpath.Line(edge_verts[i], edge_verts[i + 1]) for i in range(n-2)]
|
|
else:
|
|
edge_curves.append(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 utils.close_enough(t1, 0) and utils.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
|
|
|
|
# NOTE: Deprecated. Preserved for backward compatibility
|
|
# with the first dataset of 3D garments and sewing patterns
|
|
class ParametrizedPattern(BasicPattern):
|
|
"""
|
|
Extention to BasicPattern that can work with parametrized patterns
|
|
Update pattern with new parameter values & randomize those parameters
|
|
"""
|
|
def __init__(self, pattern_file=None):
|
|
super().__init__(pattern_file)
|
|
self.parameters = self.spec['parameters']
|
|
|
|
self.parameter_defaults = {
|
|
'length': 1,
|
|
'additive_length': 0,
|
|
'curve': 1
|
|
}
|
|
self.constraint_types = [
|
|
'length_equality'
|
|
]
|
|
|
|
def param_values_list(self):
|
|
"""Returns current values of all parameters as a list in the pattern defined parameter order"""
|
|
value_list = []
|
|
for parameter in self.spec['parameter_order']:
|
|
value = self.parameters[parameter]['value']
|
|
if isinstance(value, list):
|
|
value_list += value
|
|
else:
|
|
value_list.append(value)
|
|
return value_list
|
|
|
|
def apply_param_list(self, values):
|
|
"""Apply given parameters supplied as a list of param_values_list() form"""
|
|
|
|
self._restore_template(params_to_default=False)
|
|
|
|
# set new values
|
|
value_count = 0
|
|
for parameter in self.spec['parameter_order']:
|
|
last_value = self.parameters[parameter]['value']
|
|
if isinstance(last_value, list):
|
|
self.parameters[parameter]['value'] = [values[value_count + i] for i in range(len(last_value))]
|
|
value_count += len(last_value)
|
|
else:
|
|
self.parameters[parameter]['value'] = values[value_count]
|
|
value_count += 1
|
|
|
|
self._update_pattern_by_param_values()
|
|
|
|
def reloadJSON(self):
|
|
"""(Re)loads pattern info from spec file.
|
|
Useful when spec is updated from outside"""
|
|
super().reloadJSON()
|
|
|
|
self.parameters = self.spec['parameters']
|
|
self._normalize_param_scaling()
|
|
|
|
def _restore(self, backup_copy):
|
|
"""Restores spec structure from given backup copy
|
|
Makes a full copy of backup to avoid accidential corruption of backup
|
|
"""
|
|
super()._restore(backup_copy)
|
|
self.parameters = self.spec['parameters']
|
|
|
|
# ---------- Parameters operations --------
|
|
|
|
def _normalize_param_scaling(self):
|
|
"""Convert additive parameters to cm units"""
|
|
|
|
if 'original_units_in_meter' in self.properties: # pattern was scaled
|
|
scaling = 100 / self.properties['original_units_in_meter']
|
|
for parameter in self.parameters:
|
|
if self.parameters[parameter]['type'] == 'additive_length':
|
|
self.parameters[parameter]['value'] = scaling * self.parameters[parameter]['value']
|
|
self.parameters[parameter]['range'] = [
|
|
scaling * elem for elem in self.parameters[parameter]['range']
|
|
]
|
|
|
|
# now we have cm everywhere -- no need to keep units info
|
|
self.properties.pop('original_units_in_meter', None)
|
|
|
|
print('WARNING: Parameter units were converted to cm')
|
|
|
|
def _normalize_edge_loop(self, panel_name):
|
|
"""Update the edge loops and edge ids references in parameters & constraints after change"""
|
|
rotated_edge_ids, flipped = super()._normalize_edge_loop(panel_name)
|
|
|
|
# Parameters
|
|
for parameter_name in self.spec['parameters']:
|
|
self._influence_after_edge_loop_update(
|
|
self.spec['parameters'][parameter_name]['influence'],
|
|
panel_name, rotated_edge_ids)
|
|
|
|
# Constraints
|
|
if 'constraints' in self.spec:
|
|
for constraint_name in self.spec['constraints']:
|
|
self._influence_after_edge_loop_update(
|
|
self.spec['constraints'][constraint_name]['influence'],
|
|
panel_name, rotated_edge_ids)
|
|
|
|
def _influence_after_edge_loop_update(self, infl_list, panel_name, new_edge_ids):
|
|
"""
|
|
Update the list of parameter\constraint influence with the new edge ids of given panel.
|
|
|
|
flipped -- indicates if in the new edges start & end vertices have been swapped
|
|
"""
|
|
for infl_id in range(len(infl_list)):
|
|
if infl_list[infl_id]['panel'] == panel_name:
|
|
# update
|
|
edge_list = infl_list[infl_id]['edge_list']
|
|
for edge_list_id in range(len(edge_list)):
|
|
if isinstance(edge_list[edge_list_id], int): # Simple edge id lists in curvature params
|
|
old_id = edge_list[edge_list_id]
|
|
edge_list[edge_list_id] = new_edge_ids[old_id]
|
|
elif isinstance(edge_list[edge_list_id]['id'], list): # Meta-edge in length parameters & constraints
|
|
for i in range(len(edge_list[edge_list_id]['id'])):
|
|
old_id = edge_list[edge_list_id]['id'][i]
|
|
edge_list[edge_list_id]['id'][i] = new_edge_ids[old_id]
|
|
else: # edge description in length parameters & constraints
|
|
old_id = edge_list[edge_list_id]['id']
|
|
edge_list[edge_list_id]['id'] = new_edge_ids[old_id]
|
|
|
|
def _update_pattern_by_param_values(self):
|
|
"""
|
|
Recalculates vertex positions and edge curves according to current
|
|
parameter values
|
|
(!) Assumes that the current pattern is a template:
|
|
with all the parameters equal to defaults!
|
|
"""
|
|
for parameter in self.spec['parameter_order']:
|
|
value = self.parameters[parameter]['value']
|
|
param_type = self.parameters[parameter]['type']
|
|
if param_type not in self.parameter_defaults:
|
|
raise ValueError("Incorrect parameter type. Alowed are "
|
|
+ self.parameter_defaults.keys())
|
|
|
|
for panel_influence in self.parameters[parameter]['influence']:
|
|
for edge in panel_influence['edge_list']:
|
|
if param_type == 'length':
|
|
self._extend_edge(panel_influence['panel'], edge, value)
|
|
elif param_type == 'additive_length':
|
|
self._extend_edge(panel_influence['panel'], edge, value, multiplicative=False)
|
|
elif param_type == 'curve':
|
|
self._curve_edge(panel_influence['panel'], edge, value)
|
|
# finally, ensure secified constraints are held
|
|
self._apply_constraints()
|
|
|
|
def _restore_template(self, params_to_default=True):
|
|
"""Restore pattern to it's state with all parameters having default values
|
|
Recalculate vertex positions, edge curvatures & snap values to 1
|
|
"""
|
|
# Follow process backwards
|
|
self._invert_constraints()
|
|
|
|
for parameter in reversed(self.spec['parameter_order']):
|
|
value = self.parameters[parameter]['value']
|
|
param_type = self.parameters[parameter]['type']
|
|
if param_type not in self.parameter_defaults:
|
|
raise ValueError("Incorrect parameter type. Alowed are "
|
|
+ self.parameter_defaults.keys())
|
|
|
|
for panel_influence in reversed(self.parameters[parameter]['influence']):
|
|
for edge in reversed(panel_influence['edge_list']):
|
|
if param_type == 'length':
|
|
self._extend_edge(panel_influence['panel'], edge, self._invert_value(value))
|
|
elif param_type == 'additive_length':
|
|
self._extend_edge(panel_influence['panel'], edge,
|
|
self._invert_value(value, multiplicative=False),
|
|
multiplicative=False)
|
|
elif param_type == 'curve':
|
|
self._curve_edge(panel_influence['panel'], edge, self._invert_value(value))
|
|
|
|
# restore defaults
|
|
if params_to_default:
|
|
if isinstance(value, list):
|
|
self.parameters[parameter]['value'] = [self.parameter_defaults[param_type] for _ in value]
|
|
else:
|
|
self.parameters[parameter]['value'] = self.parameter_defaults[param_type]
|
|
|
|
def _extend_edge(self, panel_name, edge_influence, value, multiplicative=True):
|
|
"""
|
|
Shrinks/elongates a given edge or edge collection of a given panel. Applies equally
|
|
to straight and curvy edges tnks to relative coordinates of curve controls
|
|
Expects
|
|
* each influenced edge to supply the elongatoin direction
|
|
* scalar scaling_factor
|
|
'multiplicative' parameter controls the type of extention:
|
|
* if True, value is treated as a scaling factor of the edge or edge projection -- default
|
|
* if False, value is added to the edge or edge projection
|
|
"""
|
|
if isinstance(value, list):
|
|
raise ValueError("Multiple scaling factors are not supported")
|
|
|
|
verts_ids, verts_coords, target_line, _ = self._meta_edge(panel_name, edge_influence)
|
|
|
|
# calc extention pivot
|
|
if edge_influence['direction'] == 'end':
|
|
fixed = verts_coords[0] # start is fixed
|
|
elif edge_influence['direction'] == 'start':
|
|
fixed = verts_coords[-1] # end is fixed
|
|
elif edge_influence['direction'] == 'both':
|
|
fixed = (verts_coords[0] + verts_coords[-1]) / 2
|
|
else:
|
|
raise RuntimeError('Unknown edge extention direction {}'.format(edge_influence['direction']))
|
|
|
|
# move verts
|
|
# * along target line that sits on fixed point (correct sign & distance along the line)
|
|
verts_projection = np.empty(verts_coords.shape)
|
|
for i in range(verts_coords.shape[0]):
|
|
verts_projection[i] = (verts_coords[i] - fixed).dot(target_line) * target_line
|
|
|
|
if multiplicative:
|
|
# * to match the scaled projection (correct point of application -- initial vertex position)
|
|
new_verts = verts_coords - (1 - value) * verts_projection
|
|
else:
|
|
# * to match the added projection:
|
|
# still need projection to make sure the extention derection is corect relative to fixed point
|
|
|
|
# normalize first
|
|
for i in range(verts_coords.shape[0]):
|
|
norm = np.linalg.norm(verts_projection[i])
|
|
if not np.isclose(norm, 0):
|
|
verts_projection[i] /= norm
|
|
|
|
# zero projections were not normalized -- they will zero-out the effect
|
|
new_verts = verts_coords + value * verts_projection
|
|
|
|
# update in the initial structure
|
|
panel = self.pattern['panels'][panel_name]
|
|
for ni, idx in enumerate(verts_ids):
|
|
panel['vertices'][idx] = new_verts[ni].tolist()
|
|
|
|
def _curve_edge(self, panel_name, edge, scaling_factor):
|
|
"""
|
|
Updated the curvature of an edge accoding to scaling_factor.
|
|
Can only be applied to edges with curvature information
|
|
scaling_factor can be
|
|
* scalar -- only the Y of control point is changed
|
|
* 2-value list -- both coordinated of control are updated
|
|
"""
|
|
panel = self.pattern['panels'][panel_name]
|
|
if 'curvature' not in panel['edges'][edge]:
|
|
raise ValueError('Applying curvature scaling to non-curvy edge '
|
|
+ str(edge) + ' of ' + panel_name)
|
|
control = panel['edges'][edge]['curvature']
|
|
|
|
if isinstance(scaling_factor, list):
|
|
control = [
|
|
control[0] * scaling_factor[0],
|
|
control[1] * scaling_factor[1]
|
|
]
|
|
else:
|
|
control[1] *= scaling_factor
|
|
|
|
panel['edges'][edge]['curvature'] = control
|
|
|
|
def _invert_value(self, value, multiplicative=True):
|
|
"""If value is a list, return a list with each value inverted.
|
|
'multiplicative' parameter controls the type of inversion:
|
|
* if True, returns multiplicative inverse (1/value) == default
|
|
* if False, returns additive inverse (-value)
|
|
"""
|
|
if multiplicative:
|
|
if isinstance(value, list):
|
|
if any(np.isclose(value, 0)):
|
|
raise ZeroDivisionError('Zero value encountered while restoring multiplicative parameter.')
|
|
return map(lambda x: 1 / x, value)
|
|
else:
|
|
if np.isclose(value, 0):
|
|
raise ZeroDivisionError('Zero value encountered while restoring multiplicative parameter.')
|
|
return 1 / value
|
|
else:
|
|
if isinstance(value, list):
|
|
return map(lambda x: -x, value)
|
|
else:
|
|
return -value
|
|
|
|
def _apply_constraints(self):
|
|
"""Change the pattern to adhere to constraints if given in pattern spec
|
|
Assumes no zero-length edges exist"""
|
|
if 'constraints' not in self.spec:
|
|
return
|
|
|
|
for constraint_n in self.spec['constraints']: # order preserved as it's a list
|
|
constraint = self.spec['constraints'][constraint_n]
|
|
constraint_type = constraint['type']
|
|
if constraint_type not in self.constraint_types:
|
|
raise ValueError("Incorrect constraint type. Alowed are "
|
|
+ self.constraint_types)
|
|
|
|
if constraint_type == 'length_equality':
|
|
# get all length of the affected (meta) edges
|
|
target_len = []
|
|
for panel_influence in constraint['influence']:
|
|
for edge in panel_influence['edge_list']:
|
|
# NOTE: constraints along a custom vector are not well tested
|
|
_, _, _, length = self._meta_edge(panel_influence['panel'], edge)
|
|
edge['length'] = length
|
|
target_len.append(length)
|
|
if len(target_len) == 0:
|
|
return
|
|
# target as mean of provided edges
|
|
target_len = sum(target_len) / len(target_len)
|
|
|
|
# calculate scaling factor for every edge to match max length
|
|
# & update edges with it
|
|
for panel_influence in constraint['influence']:
|
|
for edge in panel_influence['edge_list']:
|
|
scaling = target_len / edge['length']
|
|
if not np.isclose(scaling, 1):
|
|
edge['value'] = scaling
|
|
self._extend_edge(panel_influence['panel'], edge, edge['value'])
|
|
|
|
def _invert_constraints(self):
|
|
"""Restore pattern to the state before constraint was applied"""
|
|
if 'constraints' not in self.spec:
|
|
return
|
|
|
|
# follow the process backwards
|
|
for constraint_n in reversed(self.spec['constraint_order']): # order preserved as it's a list
|
|
constraint = self.spec['constraints'][constraint_n]
|
|
constraint_type = constraint['type']
|
|
if constraint_type not in self.constraint_types:
|
|
raise ValueError("Incorrect constraint type. Alowed are "
|
|
+ self.constraint_types)
|
|
|
|
if constraint_type == 'length_equality':
|
|
# update edges with invertes scaling factor
|
|
for panel_influence in constraint['influence']:
|
|
for edge in panel_influence['edge_list']:
|
|
scaling = self._invert_value(edge['value'])
|
|
self._extend_edge(panel_influence['panel'], edge, scaling)
|
|
edge['value'] = 1
|
|
|
|
def _meta_edge(self, panel_name, edge_influence):
|
|
"""Returns info for the given edge or meta-edge in inified form"""
|
|
|
|
panel = self.pattern['panels'][panel_name]
|
|
edge_ids = edge_influence['id']
|
|
if isinstance(edge_ids, list):
|
|
# meta-edge
|
|
# get all vertices in order
|
|
verts_ids = [panel['edges'][edge_ids[0]]['endpoints'][0]] # start
|
|
for edge_id in edge_ids:
|
|
verts_ids.append(panel['edges'][edge_id]['endpoints'][1]) # end vertices
|
|
else:
|
|
# single edge
|
|
verts_ids = panel['edges'][edge_ids]['endpoints']
|
|
|
|
verts_coords = []
|
|
for idx in verts_ids:
|
|
verts_coords.append(panel['vertices'][idx])
|
|
verts_coords = np.array(verts_coords)
|
|
|
|
# extention line
|
|
if 'along' in edge_influence:
|
|
target_line = edge_influence['along']
|
|
else:
|
|
target_line = verts_coords[-1] - verts_coords[0]
|
|
target_line = np.array(target_line, dtype=float) # https://stackoverflow.com/questions/50625975/typeerror-ufunc-true-divide-output-typecode-d-could-not-be-coerced-to-pro
|
|
|
|
if np.isclose(np.linalg.norm(target_line), 0):
|
|
raise ZeroDivisionError('target line is zero ' + str(target_line))
|
|
else:
|
|
target_line /= np.linalg.norm(target_line)
|
|
|
|
return verts_ids, verts_coords, target_line, target_line.dot(verts_coords[-1] - verts_coords[0])
|
|
|
|
def _invalidate_all_values(self):
|
|
"""Sets all values of params & constraints to None if not set already
|
|
Useful in direct updates of pattern panels"""
|
|
|
|
updated_once = False
|
|
for parameter in self.parameters:
|
|
if self.parameters[parameter]['value'] is not None:
|
|
self.parameters[parameter]['value'] = None
|
|
updated_once = True
|
|
|
|
if 'constraints' in self.spec:
|
|
for constraint in self.spec['constraints']:
|
|
for edge_collection in self.spec['constraints'][constraint]['influence']:
|
|
for edge in edge_collection['edge_list']:
|
|
if edge['value'] is not None:
|
|
edge['value'] = None
|
|
updated_once = True
|
|
if updated_once:
|
|
# only display worning if some new invalidation happened
|
|
print('ParametrizedPattern::WARNING::Parameter (& constraints) values are invalidated')
|
|
|
|
# ---------- Randomization -------------
|
|
def _randomize_pattern(self):
|
|
"""Robustly randomize current pattern"""
|
|
# restore template state before making any changes to parameters
|
|
self._restore_template(params_to_default=False)
|
|
|
|
spec_backup = copy.deepcopy(self.spec)
|
|
self._randomize_parameters()
|
|
self._update_pattern_by_param_values()
|
|
for _ in range(100): # upper bound on trials to avoid infinite loop
|
|
if not self.is_self_intersecting():
|
|
break
|
|
|
|
print('WARNING::Randomized pattern is self-intersecting. Re-try..')
|
|
self._restore(spec_backup)
|
|
# Try again
|
|
self._randomize_parameters()
|
|
self._update_pattern_by_param_values()
|
|
|
|
def _new_value(self, param_range):
|
|
"""Random value within range given as an iteratable"""
|
|
value = random.uniform(param_range[0], param_range[1])
|
|
# prevent non-reversible zero values
|
|
if abs(value) < 1e-2:
|
|
value = 1e-2 * (-1 if value < 0 else 1)
|
|
return value
|
|
|
|
def _randomize_parameters(self):
|
|
"""
|
|
Sets new random values for the pattern parameters
|
|
Parameter type agnostic
|
|
"""
|
|
for parameter in self.parameters:
|
|
param_ranges = self.parameters[parameter]['range']
|
|
|
|
# check if parameter has multiple values (=> multiple ranges) like for curves
|
|
if isinstance(self.parameters[parameter]['value'], list):
|
|
values = []
|
|
for param_range in param_ranges:
|
|
values.append(self._new_value(param_range))
|
|
self.parameters[parameter]['value'] = values
|
|
else: # simple 1-value parameter
|
|
self.parameters[parameter]['value'] = self._new_value(param_ranges)
|
|
|
|
|