init_code

This commit is contained in:
sky
2025-07-03 17:03:00 +08:00
parent a710c87a2b
commit 89766fe3d1
220 changed files with 479903 additions and 77 deletions

View File

@@ -0,0 +1,606 @@
"""Shortcuts for common operations on panels and components"""
from copy import deepcopy, copy
import numpy as np
from numpy.linalg import norm
from scipy.spatial.transform import Rotation as R
from scipy.optimize import minimize
import svgpathtools as svgpath
from pygarment.garmentcode.edge import Edge, CurveEdge, EdgeSequence, ILENGTH_S_TOL
from pygarment.garmentcode.interface import Interface
from pygarment.garmentcode.utils import vector_angle, close_enough, c_to_list, c_to_np
from pygarment.garmentcode.utils import list_to_c
from pygarment.garmentcode.base import BaseComponent
# ANCHOR ----- Edge Sequences Modifiers ----
def cut_corner(target_shape: EdgeSequence, target_interface: Interface,
verbose: bool = False):
""" Cut the corner made of edges 1 and 2 following the shape of target_shape
This routine updated the panel geometry and interfaces appropriately
Parameters:
* 'target_shape' is an EdgeSequence that is expected to contain one
Edge or sequence of chained Edges
(next one starts from the end vertex of the one before)
# NOTE: 'target_shape' might be scaled (along the main direction)
to fit the corner size
* Panel to modify
* target_interface -- the chained pairs of edges that form the corner
to cut, s.t. the end vertex of eid1 is at the corner
# NOTE: Onto edges are expected to be straight lines for simplicity
# NOTE There might be slight computational errors in the resulting
shape, that are more pronounced on svg visualizations due to
scaling and rasterization
Side-Effects:
* Modified the panel shape to insert new edges
* Adds new interface object corresponding to new edges to the
panel interface list
Returns:
* Newly inserted edges
* New interface object corresponding to new edges
"""
# TODO Support any number of edges in the target corner edges
# ---- Evaluate optimal projection of the target shape onto the corner
corner_shape = target_shape.copy()
panel = target_interface.panel[0] # TODO Support multiple panels???
target_edges = target_interface.edges
# Get rid of directions by working on vertices
if target_edges[0].start is target_edges[-1].end:
# Orginal edges have beed reversed in normalization or smth
target_edges.edges.reverse() # UPD the order
if corner_shape[0].start is corner_shape[-1].end:
# Orginal edges have beed reversed in normalization or smth
corner_shape.edges.reverse() # UPD the order
if corner_shape[0].start[1] > corner_shape[-1].end[1]:
# now corner shape is oriented the same way as vertices
corner_shape.reverse()
corner_shape.snap_to([0,0])
shortcut = corner_shape.shortcut()
# Curves (can be defined outside)
curve1 = target_edges[0].as_curve()
curve2 = target_edges[1].as_curve()
# align order with the projecting shape, s.t.
# curve2 is always the lower one
swaped = False
if target_edges[0].start[1] > target_edges[-1].end[1]:
curve1, curve2 = curve2, curve1
swaped = True
# NOW curve1 is lower then curve2
# ----- FIND OPTIMAL PLACE -----
start = [0.5, 0.5]
out = minimize(
_fit_location_corner, start,
args=(shortcut[1] - shortcut[0], curve1, curve2),
bounds=[(0, 1), (0, 1)])
if verbose and not out.success:
print(f'Cut_corner::ERROR::finding the projection (translation) is unsuccessful. Likely an error in edges choice')
print(out)
if verbose and not close_enough(out.fun):
print(f'Cut_corner::WARNING::projection on {target_interface} finished with fun={out.fun}')
print(out)
loc = out.x
point1 = c_to_list(curve1.point(loc[0]))
# re-align corner_shape with found shifts
corner_shape.snap_to(point1)
# ----- UPD panel ----
# Complete to the full corner -- connect with the initial vertices
if swaped:
# The edges are aligned as v2 -> vc -> v1
corner_shape.reverse()
loc[0], loc[1] = loc[1], loc[0]
# Insert a new shape
cut_edge1, _ = target_edges[0].subdivide_param([loc[0], 1-loc[0]])
_, cut_edge2 = target_edges[1].subdivide_param([loc[1], 1-loc[1]])
cut_edge1.end = corner_shape[0].start # Connect with new insert
cut_edge2.start = corner_shape[-1].end
corner_shape.insert(0, cut_edge1)
corner_shape.append(cut_edge2)
# Substitute edges in the panel definition
panel.edges.pop(target_edges[0])
panel.edges.substitute(target_edges[1], corner_shape)
# Update interface definitions
target_edges = EdgeSequence(target_edges.edges) # keep the same edge references,
# but not the same edge sequence reference
# In case it matches one of the interfaces (we don't want target edges to be overriden)
iter = panel.interfaces if isinstance(panel.interfaces, list) else panel.interfaces.values()
for intr in iter:
# Substitute old edges with what's left from them after cutting
if target_edges[0] in intr.edges:
intr.edges.substitute(target_edges[0], corner_shape[0])
if target_edges[1] in intr.edges:
intr.edges.substitute(target_edges[1], corner_shape[-1])
# Add new interface corresponding to the introduced cut
new_int = Interface(panel, corner_shape[1:-1])
if isinstance(panel.interfaces, list):
panel.interfaces.append(new_int)
else:
panel.interfaces[f'int_{len(panel.interfaces)}'] = new_int
return corner_shape[1:-1], new_int
def cut_into_edge(target_shape, base_edge:Edge, offset=0, right=True,
flip_target=False, tol=1e-2):
""" Insert edges of the target_shape into the given base_edge, starting
from offset edges in target shape are rotated s.t. start -> end
vertex vector is aligned with the edge
NOTE: Supports making multiple cuts in one go maintaining the relative
distances between cuts
provided that
* they are all specified in the same coordinate system
* (for now) the openings (shortcuts) of each cut are aligned with
OY direction
Parameters:
* target_shape -- list of single edge, chained edges, or multiple
chaind EdgeSequences to be inserted in the edge.
* base_edge -- edge object, defining the border
* Offset -- position of the center of the target shape along the edge.
* right -- which direction the cut should be oriented w.r.t. the
direction of base edge
* flip_target -- reflect the shape w.r.t its central perpendicular
(default=False, no action taken)
Returns:
* Newly created edges that accomodate the cut
* Edges corresponding to the target shape
* Edges that lie on the original base edge
"""
# TODO Not only for Y-aligned shapes
# TODOLOW Add a parameter: Align target_shape by center or from the start of the offset
# NOTE: the optimization routine might be different for the two options
if isinstance(target_shape, EdgeSequence):
return cut_into_edge_single(
target_shape, base_edge, offset, right, tol)
# center of the shape
shortcuts = np.asarray([e.shortcut() for e in target_shape])
median_y = (shortcuts[:, :, 1].max() + shortcuts[:, :, 1].min()) / 2
# Flip the shapes if requested
if flip_target:
target_shape = [s.copy() for s in target_shape]
# Flip
target_shape = [s.reflect([0, median_y], [1, median_y])
for s in target_shape]
# Flip the order as well to reflect orientation change
target_shape = [s.reverse() for s in target_shape]
# Calculate relative offsets to place the whole shape at the target offset
shortcuts = np.asarray([e.shortcut() for e in target_shape])
rel_offsets = [(s[0][1] + s[1][1]) / 2 - median_y for s in shortcuts]
per_seq_offsets = [offset + r for r in rel_offsets]
# Project from farthest to closest
sorted_tup = sorted(zip(per_seq_offsets, target_shape), reverse=True)
proj_edge, int_edges = base_edge, EdgeSequence(base_edge)
new_in_edges = EdgeSequence()
all_new_edges = EdgeSequence(base_edge)
for off, shape in sorted_tup:
new_edge, in_edges, new_interface = cut_into_edge(
shape, proj_edge, offset=off, right=right, tol=tol)
all_new_edges.substitute(proj_edge, new_edge)
int_edges.substitute(proj_edge, new_interface)
new_in_edges.append(in_edges)
proj_edge = new_edge[0]
return all_new_edges, new_in_edges, int_edges
def cut_into_edge_single(target_shape, base_edge: Edge, offset=0, right=True,
tol=1e-2, verbose: bool = False):
""" Insert edges of the target_shape into the given base_edge, starting
from offset
edges in target shape are rotated s.t. start -> end vertex vector is
aligned with the edge
Parameters:
* target_shape -- list of single edge or chained edges to be inserted
in the edge.
* base_edge -- edge object, defining the border
* right -- which direction the cut should be oriented w.r.t. the
direction of base edge
* Offset -- position of the center of the target shape along the edge.
Returns:
* Newly created edges that accommodate the cut
* Edges corresponding to the target shape
* Edges that lie on the original base edge
"""
target_shape = EdgeSequence(target_shape)
new_edges = target_shape.copy().snap_to([0, 0]) # copy and normalize translation of vertices
# Simplify to vectors
shortcut = new_edges.shortcut() # "Interface" of the shape to insert
target_shape_w = norm(shortcut)
edge_len = base_edge.length()
if offset < target_shape_w / 2 - tol or offset > (edge_len - target_shape_w / 2) + tol:
# NOTE: This is not a definitive check, and the cut might still not fit, depending on the base_edge curvature
raise ValueError(f'Operators-CutingIntoEdge::ERROR::offset value is not within the base_edge length')
# find starting vertex for insertion & place edges there
curve = base_edge.as_curve()
rel_offset = curve.ilength(offset, s_tol=ILENGTH_S_TOL)
# ----- OPTIMIZATION ---
start = [0.1, 0.1]
out = minimize(
_fit_location_edge, start,
args=(rel_offset, target_shape_w, curve),
bounds=[(0, 1)])
shift = out.x
# Error checks
if verbose and not out.success:
print(f'Cut_edge::ERROR::finding the projection (translation) is unsuccessful. Likely an error in edges choice')
if not close_enough(out.fun, tol=0.01):
if verbose:
print(out)
raise RuntimeError(f'Cut_edge::ERROR::projection on {base_edge} finished with fun={out.fun}')
if rel_offset + shift[0] > 1 + tol or (rel_offset - shift[1]) < 0 - tol:
raise RuntimeError(
f'Cut_edge::ERROR::projection on {base_edge} is out of edge bounds: '
f'[{rel_offset - shift[1], rel_offset + shift[0]}].'
' Check the offset value')
# All good -- integrate the target shape
ins_point = c_to_np(curve.point(rel_offset - shift[1])) if (rel_offset - shift[1]) > tol else base_edge.start
fin_point = c_to_np(curve.point(rel_offset + shift[0])) if (rel_offset + shift[0]) < 1 - tol else base_edge.end
# Align the shape with an edge
# find rotation to apply on target shape
insert_vector = np.asarray(fin_point) - np.asarray(ins_point)
angle = vector_angle(shortcut[1] - shortcut[0], insert_vector)
new_edges.rotate(angle)
# place
new_edges.snap_to(ins_point)
# Check orientation
avg_vertex = np.asarray(new_edges.verts()).mean(0)
right_position = np.sign(np.cross(insert_vector, avg_vertex - np.asarray(new_edges[0].start))) == -1
if not right and right_position or right and not right_position:
# flip shape to match the requested direction
new_edges.reflect(new_edges[0].start, new_edges[-1].end)
# Integrate edges
# NOTE: no need to create extra edges if the the shape is incerted right at the beggining or end of the edge
base_edge_leftovers = EdgeSequence()
start_id, end_id = 0, len(new_edges)
if ins_point is base_edge.start:
new_edges[0].start = base_edge.start # Connect into the original edge
else:
# TODOLOW more elegant subroutine
start_part = base_edge.subdivide_param([rel_offset - shift[1], 1 - (rel_offset - shift[1])])[0]
start_part.end = new_edges[0].start
new_edges.insert(0, start_part)
base_edge_leftovers.append(new_edges[0])
start_id = 1
if fin_point is base_edge.end:
new_edges[-1].end = base_edge.end # Connect into the original edge
else:
end_part = base_edge.subdivide_param([rel_offset + shift[0], 1 - (rel_offset + shift[0])])[-1]
end_part.start = new_edges[-1].end
new_edges.append(end_part)
base_edge_leftovers.append(new_edges[-1])
end_id = -1
return new_edges, new_edges[start_id:end_id], base_edge_leftovers
def _fit_location_corner(l, diff_target, curve1, curve2,
verbose: bool = False):
"""Find the points on two curves s.t. vector between them is the same as
shortcut"""
# Current points on curves
point1 = c_to_np(curve1.point(l[0]))
point2 = c_to_np(curve2.point(l[1]))
diff_curr = point2 - point1
if verbose:
print('Location Progression: ', (diff_curr[0] - diff_target[0])**2,
(diff_curr[1] - diff_target[1])**2)
return ((diff_curr[0] - diff_target[0])**2
+ (diff_curr[1] - diff_target[1])**2)
def _fit_location_edge(shift, location, width_target, curve,
verbose: bool = False):
"""Find the points on two curves s.t. vector between them is the same as
shortcut"""
# Current points on curves
pointc = c_to_np(curve.point(location)) # TODO this is constant
point1 = c_to_np(curve.point(location + shift[0]))
point2 = c_to_np(curve.point(location - shift[1]))
if verbose:
print('Location Progression: ', (_dist(point1, point2) - width_target)**2)
# regularize points to be at the same distance from center
reg_symmetry = (_dist(point1, pointc) - _dist(point2, pointc))**2
return (_dist(point1, point2) - width_target)**2 + reg_symmetry
# ANCHOR ----- Panel operations ------
def distribute_Y(component, n_copies, odd_copy_shift=0, name_tag='panel'):
"""Distribute copies of component over the circle around Oy"""
copies = [ component ]
component.name = f'{name_tag}_0' # Unique
delta_rotation = R.from_euler('XYZ', [0, 360 / n_copies, 0], degrees=True)
for i in range(n_copies - 1):
new_component = deepcopy(copies[-1])
new_component.name = f'{name_tag}_{i + 1}' # Unique
new_component.rotate_by(delta_rotation)
new_component.translate_to(delta_rotation.apply(new_component.translation))
copies.append(new_component)
# shift around to resolve collisions (hopefully)
if odd_copy_shift:
for i in range(n_copies):
if not i % 2:
copies[i].translate_by(copies[i].norm() * odd_copy_shift)
return copies
def distribute_horisontally(component, n_copies, stride=20, name_tag='panel'):
"""Distribute copies of component over the straight horisontal line
perpendicular to the norm"""
copies = [ component ]
component.name = f'{name_tag}_0' # Unique
if isinstance(component, BaseComponent):
translation_dir = component.rotation.apply([0, 0, 1]) # Horisontally along the panel
# FIXME What if it's looking up?
translation_dir = np.cross(translation_dir, [0, 1, 0]) # perpendicular to Y
translation_dir = translation_dir / norm(translation_dir)
delta_translation = translation_dir * stride
else:
translation_dir = [1, 0, 0]
for i in range(n_copies - 1):
new_component = deepcopy(copies[-1]) # TODO proper copy
new_component.name = f'{name_tag}_{i + 1}' # Unique
new_component.translate_by(delta_translation)
copies.append(new_component)
return copies
# ANCHOR ----- Sleeve support -----
def even_armhole_openings(front_opening, back_opening, tol=1e-2, verbose: bool = False):
"""
Rearrange sleeve openings for front and back s.t. their projection
on vertical line is the same, while preserving the overall shape.
Allows for creation of two symmetric sleeve panels from them
!! Important: assumes that the front opening is longer then back opening
"""
# Construct sleeve panel shapes from opening inverses
cfront, cback = front_opening.copy(), back_opening.copy()
cback.reflect([0, 0], [1, 0]).reverse().snap_to(cfront[-1].end)
# Cutout
slope = np.array([cfront[0].start, cback[-1].end])
slope_vec = slope[1] - slope[0]
slope_perp = np.asarray([-slope_vec[1], slope_vec[0]])
slope_midpoint = (slope[0] + slope[1]) / 2
# Intersection with the sleeve itself line
# svgpath tools allow solution regardless of egde types
inter_segment = svgpath.Line(
list_to_c(slope_midpoint - 20 * slope_perp),
list_to_c(slope_midpoint + 20 * slope_perp)
)
target_segment = cfront[-1].as_curve()
intersect_t = target_segment.intersect(inter_segment)
if len(intersect_t) != 1 and verbose:
print(
f'Redistribute Sleeve Openings::WARNING::{len(intersect_t)} intersection points instead of one. '
f'Front and back opening curves might be the same with lengths: {cfront.length()}, {cback.length()}'
)
if (len(intersect_t) >= 1
and not (close_enough(intersect_t[0][0], 0, tol=tol) # checking if they are already ok separated
or close_enough(intersect_t[0][0], 1, tol=tol))):
# The current separation is not satisfactory
# Update the opening shapes
intersect_t = intersect_t[0][0]
subdiv = front_opening.edges[-1].subdivide_param([intersect_t, 1 - intersect_t])
front_opening.substitute(-1, subdiv[0])
# Move this part to the back opening
subdiv[1].start, subdiv[1].end = copy(subdiv[1].start), copy(subdiv[1].end) # Disconnect vertices in subdivided version
subdiv.pop(0) # TODOLOW No reflect in the edge class??
subdiv.reflect([0, 0], [1, 0]).reverse().snap_to(back_opening[-1].end)
subdiv[0].start = back_opening[-1].end
back_opening.append(subdiv[0])
# Align the slope with OY direction
# for correct size of sleeve panels
slope_angle = np.arctan(-slope_vec[0] / slope_vec[1])
front_opening.rotate(-slope_angle)
back_opening.rotate(slope_angle)
return front_opening, back_opening
# ANCHOR ----- Curve tools -----
def _avg_curvature(curve, points_estimates=100):
"""Average curvature in a curve"""
# NOTE: this work slow, but direct evaluation seems
# infeasible
# Some hints here:
# https://math.stackexchange.com/questions/220900/bezier-curvature
t_space = np.linspace(0, 1, points_estimates)
return sum([curve.curvature(t) for t in t_space]) / points_estimates
def _max_curvature(curve, points_estimates=100):
"""Average curvature in a curve"""
# NOTE: this work slow, but direct evaluation seems
# infeasible
# Some hints here: https://math.stackexchange.com/questions/1954845/bezier-curvature-extrema
t_space = np.linspace(0, 1, points_estimates)
return max([curve.curvature(t) for t in t_space])
def _bend_extend_2_tangent(
shift, cp, target_len, direction,
target_tangent_start, target_tangent_end,
point_estimates=50):
"""Evaluate how well curve preserves the length and tangents
NOTE: point_estimates controls average curvature evaluation.
The higher the number, the more stable the optimization,
but higher computational cost
"""
control = np.array([
cp[0],
[cp[1][0] + shift[0], cp[1][1] + shift[1]],
[cp[2][0] + shift[2], cp[2][1] + shift[3]],
cp[-1] + direction * shift[4]
])
params = control[:, 0] + 1j*control[:, 1]
curve_inverse = svgpath.CubicBezier(*params)
length_diff = (curve_inverse.length() - target_len)**2 # preservation
tan_0_diff = (abs(curve_inverse.unit_tangent(0) - target_tangent_start))**2
tan_1_diff = (abs(curve_inverse.unit_tangent(1) - target_tangent_end))**2
# NOTE: tried regularizing based on Y value in relative coordinates (for speed),
# But it doesn't produce good results
curvature_reg = _max_curvature(curve_inverse, points_estimates=point_estimates)**2
end_expantion_reg = 0.001*shift[-1]**2
return length_diff + tan_0_diff + tan_1_diff + curvature_reg + end_expantion_reg
def curve_match_tangents(curve, target_tan0, target_tan1, target_len=None,
return_as_edge=False, verbose: bool = False):
"""Update the curve to have the desired tangent directions at endpoints
while preserving curve length or desired target length ('target_len') and overall direction
Returns
* control points for the final CubicBezier curves
* Or CurveEdge instance, if return_as_edge=True
NOTE: Only Cubic Bezier curves are supported
NOTE: Expects good enough initialization ('curve') that approximated desired solution
"""
if not isinstance(curve, svgpath.CubicBezier):
raise NotImplementedError(
f'Curve_match_tangents::ERROR::Only Cubic Bezier curves are supported ',
f'(got {type(curve)})')
curve_cps = c_to_np(curve.bpoints())
direction = curve_cps[-1] - curve_cps[0]
direction /= np.linalg.norm(direction)
target_tan0 = target_tan0 / np.linalg.norm(target_tan0)
target_tan1 = target_tan1 / np.linalg.norm(target_tan1)
# match tangents with the requested ones while preserving length
out = minimize(
_bend_extend_2_tangent, # with tangent matching
[0, 0, 0, 0, 0],
args=(
curve_cps,
curve.length() if target_len is None else target_len,
direction,
list_to_c(target_tan0),
list_to_c(target_tan1),
70 # NOTE: Low values cause instable resutls
),
method='L-BFGS-B',
)
if not out.success:
if verbose:
print(f'Curve_match_tangents::WARNING::optimization not successfull')
print(out)
shift = out.x
fin_curve_cps = [
curve_cps[0].tolist(),
[curve_cps[1][0] + shift[0], curve_cps[1][1] + shift[1]],
[curve_cps[2][0] + shift[2], curve_cps[2][1] + shift[3]],
(curve_cps[-1] + direction*shift[-1]).tolist(),
]
if return_as_edge:
fin_inv_edge = CurveEdge(
start=fin_curve_cps[0],
end=fin_curve_cps[-1],
control_points=fin_curve_cps[1:3],
relative=False
)
return fin_inv_edge
return fin_curve_cps
# ---- Utils ----
def _dist(v1, v2):
return norm(v2-v1)
def _fit_scale(s, shortcut, v1, v2, vc, d_v1, d_v2):
"""Evaluate how good a shortcut fits the corner if the vertices are shifted
a little along the line"""
# Shortcut can be used as 2D vector, not a set of 2D points, e.g.
shifted = deepcopy(shortcut)
shifted[0] += (shortcut[0] - shortcut[1]) * s[0] # this only changes the end vertex though
shifted[1] += (shortcut[1] - shortcut[0]) * s[1] # this only changes the end vertex though
return ((d_v1 - _dist(shifted[0], v1) - _dist(shifted[0], vc))**2
+ (d_v2 - _dist(shifted[1], v2) - _dist(shifted[1], vc))**2
)