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