init_code
This commit is contained in:
499
pygarment/garmentcode/edge_factory.py
Normal file
499
pygarment/garmentcode/edge_factory.py
Normal file
@@ -0,0 +1,499 @@
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
import svgpathtools as svgpath
|
||||
from scipy.optimize import minimize
|
||||
|
||||
from pygarment.garmentcode.edge import EdgeSequence, Edge, CurveEdge
|
||||
from pygarment.garmentcode.edge import CircleEdge
|
||||
from pygarment.garmentcode.utils import vector_angle
|
||||
from pygarment.garmentcode.utils import bbox_paths
|
||||
from pygarment.garmentcode.utils import close_enough
|
||||
from pygarment.garmentcode.utils import c_to_list
|
||||
from pygarment.garmentcode.utils import list_to_c
|
||||
from pygarment.pattern.utils import rel_to_abs_2d, abs_to_rel_2d
|
||||
|
||||
|
||||
class EdgeFactory:
|
||||
@staticmethod
|
||||
def from_svg_curve(seg):
|
||||
"""Create Edge/CurveEdge/CircleEdge object from svgpath object
|
||||
Type is determined by svgpath type
|
||||
"""
|
||||
|
||||
start, end = c_to_list(seg.start), c_to_list(seg.end)
|
||||
if isinstance(seg, svgpath.Line):
|
||||
return Edge(start, end)
|
||||
if isinstance(seg, svgpath.Arc):
|
||||
# NOTE: assuming circular arc (same radius in both directoins)
|
||||
radius = seg.radius.real
|
||||
return CircleEdgeFactory.from_points_radius(
|
||||
start, end, radius, seg.large_arc, seg.sweep
|
||||
)
|
||||
|
||||
# Only Bezier left
|
||||
if isinstance(seg, svgpath.QuadraticBezier):
|
||||
cp = [c_to_list(seg.control)]
|
||||
elif isinstance(seg, svgpath.CubicBezier):
|
||||
cp = [c_to_list(seg.control1), c_to_list(seg.control2)]
|
||||
else:
|
||||
raise NotImplementedError(f'CurveEdge::ERROR::Incorrect curve type supplied {seg.type}')
|
||||
|
||||
return CurveEdge(start, end, cp, relative=False)
|
||||
|
||||
class CircleEdgeFactory:
|
||||
@staticmethod
|
||||
def from_points_angle(start, end, arc_angle, right=True):
|
||||
"""Construct circle arc from two fixed points and an angle
|
||||
|
||||
arc_angle:
|
||||
|
||||
NOTE: Might fail on angles close to 2pi
|
||||
"""
|
||||
# Big or small arc
|
||||
if arc_angle > np.pi:
|
||||
arc_angle = 2 * np.pi - arc_angle
|
||||
to_sum = True
|
||||
else:
|
||||
to_sum = False
|
||||
|
||||
radius = 1 / np.sin(arc_angle / 2) / 2
|
||||
h = 1 / np.tan(arc_angle / 2) / 2
|
||||
|
||||
control_y = radius + h if to_sum else radius - h # relative control point
|
||||
control_y *= -1 if right else 1
|
||||
|
||||
return CircleEdge(start, end, cy=control_y)
|
||||
|
||||
@staticmethod
|
||||
def from_points_radius(start, end, radius, large_arc=False, right=True):
|
||||
"""Construct circle arc relative representation
|
||||
from two fixed points and an (absolute) radius
|
||||
"""
|
||||
# Find circle center
|
||||
str_dist = norm(np.asarray(end) - np.asarray(start))
|
||||
|
||||
# NOTE: close enough values may give negative
|
||||
# value under sqrt due to numerical errors
|
||||
if close_enough(radius ** 2, str_dist ** 2 / 4, 1e-3):
|
||||
center_r = 0.
|
||||
else:
|
||||
center_r = np.sqrt(radius ** 2 - str_dist ** 2 / 4)
|
||||
|
||||
# Find the absolute value of Y
|
||||
control_y = radius + center_r if large_arc else radius - center_r
|
||||
|
||||
# Convert to relative
|
||||
control_y = control_y / str_dist
|
||||
|
||||
# Flip sight according to "right" parameter
|
||||
control_y *= -1 if right else 1
|
||||
|
||||
return CircleEdge(start, end, cy=control_y)
|
||||
|
||||
@staticmethod
|
||||
def from_rad_length(rad, length, right=True, start=None):
|
||||
"""NOTE: if start vertex is not provided, both vertices will be created
|
||||
to match desired radius and length
|
||||
"""
|
||||
max_len = 2 * np.pi * rad
|
||||
|
||||
if length > max_len:
|
||||
raise ValueError(
|
||||
f'CircleEdge::ERROR::Incorrect length for specified radius')
|
||||
|
||||
large_arc = length > max_len / 2
|
||||
if large_arc:
|
||||
length = max_len - length
|
||||
|
||||
w_half = rad * np.sin(length / rad / 2)
|
||||
|
||||
edge = CircleEdgeFactory.from_points_radius(
|
||||
[-w_half, 0], [w_half, 0],
|
||||
radius=rad,
|
||||
large_arc=large_arc,
|
||||
right=right
|
||||
)
|
||||
|
||||
if start:
|
||||
edge.snap_to(start)
|
||||
edge.start = start
|
||||
|
||||
return edge
|
||||
|
||||
@staticmethod
|
||||
def from_three_points(start, end, point_on_arc, relative=False):
|
||||
"""Create a circle arc from 3 points (start, end and any point on an arc)
|
||||
|
||||
NOTE: Control point specified in the same coord system as start and end
|
||||
NOTE: points should not be on the same line
|
||||
"""
|
||||
if relative:
|
||||
point_on_arc = rel_to_abs_2d(start, end, point_on_arc)
|
||||
|
||||
nstart, nend, npoint_on_arc = np.asarray(start), np.asarray(
|
||||
end), np.asarray(point_on_arc)
|
||||
|
||||
# https://stackoverflow.com/a/28910804
|
||||
# Using complex numbers to calculate the center & radius
|
||||
x, y, z = list_to_c([start, point_on_arc, end])
|
||||
w = z - x
|
||||
w /= y - x
|
||||
c = (x - y) * (w - abs(w) ** 2) / 2j / w.imag - x
|
||||
# NOTE center = [c.real, c.imag]
|
||||
rad = abs(c + x)
|
||||
|
||||
# Large/small arc
|
||||
mid_dist = norm(npoint_on_arc - ((nstart + nend) / 2))
|
||||
|
||||
# Orientation
|
||||
angle = vector_angle(npoint_on_arc - nstart, nend - nstart) # +/-
|
||||
|
||||
return CircleEdgeFactory.from_points_radius(
|
||||
start, end, radius=rad,
|
||||
large_arc=mid_dist > rad, right=angle > 0)
|
||||
|
||||
class CurveEdgeFactory:
|
||||
@staticmethod
|
||||
def curve_3_points(start, end, target, verbose=False):
|
||||
"""Create (Quadratic) curve edge between start and end that
|
||||
passes through the target point
|
||||
"""
|
||||
rel_target = abs_to_rel_2d(start, end, target)
|
||||
|
||||
if rel_target[0] > 1 or rel_target[0] < 0:
|
||||
raise NotImplementedError(
|
||||
"CurveEdgeFactory::Curve_by_3_points::ERROR::requested target point's projection "
|
||||
"is outside of the base edge, which is not yet supported"
|
||||
)
|
||||
|
||||
# Initialization with a target point as control point
|
||||
# Ensures very smooth, minimal solution
|
||||
out = minimize(
|
||||
_fit_pass_point,
|
||||
rel_target,
|
||||
args=(rel_target)
|
||||
)
|
||||
|
||||
if not out.success:
|
||||
if verbose:
|
||||
print('Curve From Extreme::WARNING::Optimization not successful')
|
||||
print(out)
|
||||
|
||||
cp = out.x.tolist()
|
||||
|
||||
return CurveEdge(start, end, control_points=[cp], relative=True)
|
||||
|
||||
@staticmethod
|
||||
def curve_from_tangents(start, end, target_tan0=None, target_tan1=None,
|
||||
initial_guess=None, verbose=False):
|
||||
"""Create Quadratic Bezier curve connecting given points with the target tangents
|
||||
(both or any of the two can be specified)
|
||||
|
||||
NOTE: Target tangent vectors are automatically normalized
|
||||
"""
|
||||
|
||||
if target_tan0 is not None:
|
||||
target_tan0 = abs_to_rel_2d(start, end, target_tan0, as_vector=True)
|
||||
target_tan0 /= norm(target_tan0)
|
||||
|
||||
if target_tan1 is not None:
|
||||
target_tan1 = abs_to_rel_2d(start, end, target_tan1, as_vector=True)
|
||||
target_tan1 /= norm(target_tan1)
|
||||
|
||||
# Initialization with a target point as control point
|
||||
# Ensures very smooth, minimal solution
|
||||
out = minimize(
|
||||
_fit_tangents,
|
||||
[0.5, 0] if initial_guess is None else initial_guess,
|
||||
args=(target_tan0, target_tan1)
|
||||
)
|
||||
|
||||
if not out.success:
|
||||
print('CurveEdgeFactory::Curve From Tangents::WARNING::Optimization not successful')
|
||||
if verbose:
|
||||
print(out)
|
||||
|
||||
cp = out.x.tolist()
|
||||
|
||||
return CurveEdge(start, end, control_points=[cp], relative=True)
|
||||
|
||||
class EdgeSeqFactory:
|
||||
"""Create EdgeSequence objects for some common edge sequence patterns
|
||||
"""
|
||||
@staticmethod
|
||||
def from_svg_path(path: svgpath.Path, dist_tol=0.05, verbose=False):
|
||||
"""Convert SVG path given as svgpathtool Path object to an EdgeSequence
|
||||
|
||||
* dist_tol: tolerance for vertex closeness to be considered the same
|
||||
vertex
|
||||
NOTE: Assumes that the path can be chained
|
||||
"""
|
||||
# Convert as is
|
||||
edges = []
|
||||
for seg in path._segments:
|
||||
# skip segments of length zero
|
||||
if close_enough(seg.length(), tol=dist_tol):
|
||||
if verbose:
|
||||
print('Skipped: ', seg)
|
||||
continue
|
||||
edges.append(EdgeFactory.from_svg_curve(seg))
|
||||
|
||||
# Chain the edges
|
||||
if len(edges) > 1:
|
||||
for i in range(1, len(edges)):
|
||||
|
||||
if not all(close_enough(s, e, tol=dist_tol)
|
||||
for s, e in zip(edges[i].start, edges[i - 1].end)):
|
||||
raise ValueError(
|
||||
'EdgeSequence::from_svg_path::input path is not chained')
|
||||
|
||||
edges[i].start = edges[i - 1].end
|
||||
return EdgeSequence(*edges, verbose=verbose)
|
||||
|
||||
@staticmethod
|
||||
def from_verts(*verts, loop=False):
|
||||
"""Generate sequence of straight edges from given vertices. If loop==True,
|
||||
the method also closes the edge sequence as a loop
|
||||
"""
|
||||
seq = EdgeSequence(Edge(verts[0], verts[1]))
|
||||
for i in range(2, len(verts)):
|
||||
seq.append(Edge(seq[-1].end, verts[i]))
|
||||
|
||||
if loop:
|
||||
seq.append(Edge(seq[-1].end, seq[0].start))
|
||||
|
||||
seq.isChained() # print warning if smth is wrong
|
||||
return seq
|
||||
|
||||
@staticmethod
|
||||
def from_fractions(start, end, frac=None):
|
||||
"""A sequence of edges between start and end wich lengths are distributed
|
||||
as specified in frac list
|
||||
Parameters:
|
||||
* frac -- list of legth fractions. Every entry is in (0, 1],
|
||||
all entries sums up to 1
|
||||
"""
|
||||
frac = [abs(f) for f in frac]
|
||||
if not close_enough(fsum := sum(frac), 1, 1e-4):
|
||||
raise RuntimeError(f'EdgeSequence::ERROR::fraction is incorrect. The sum {fsum} is not 1')
|
||||
|
||||
vec = np.asarray(end) - np.asarray(start)
|
||||
verts = [start]
|
||||
for i in range(len(frac) - 1):
|
||||
verts.append(
|
||||
[verts[-1][0] + frac[i]*vec[0],
|
||||
verts[-1][1] + frac[i]*vec[1]]
|
||||
)
|
||||
verts.append(end)
|
||||
|
||||
return EdgeSeqFactory.from_verts(*verts)
|
||||
|
||||
@staticmethod
|
||||
def side_with_cut(start=(0, 0), end=(1, 0), start_cut=0, end_cut=0):
|
||||
""" Edge with internal vertices that allows to stitch only part of the border represented
|
||||
by the long side edge
|
||||
|
||||
start_cut and end_cut specify the fraction of the edge to to add extra vertices at
|
||||
"""
|
||||
nstart, nend = np.array(start), np.array(end)
|
||||
verts = [start]
|
||||
|
||||
if start_cut > 0:
|
||||
verts.append((start + start_cut * (nend-nstart)).tolist())
|
||||
if end_cut > 0:
|
||||
verts.append((end - end_cut * (nend-nstart)).tolist())
|
||||
verts.append(end)
|
||||
|
||||
edges = EdgeSeqFactory.from_verts(*verts)
|
||||
|
||||
return edges
|
||||
|
||||
# ------ Darts ------
|
||||
@staticmethod
|
||||
def dart_shape(width, side_len=None, depth=None):
|
||||
"""Shape of simple triangular dart:
|
||||
specified by desired width and either the dart side length or depth
|
||||
"""
|
||||
|
||||
if side_len is None and depth is None:
|
||||
raise ValueError(
|
||||
'EdgeFactory::ERROR::dart shape is not fully specified.'
|
||||
' Add dart side length or dart perpendicular'
|
||||
)
|
||||
|
||||
if depth is None:
|
||||
if width / 2 > side_len:
|
||||
raise ValueError(
|
||||
f'EdgeFactory::ERROR::Requested dart shape (w={width}, side={side_len}) '
|
||||
'does not form a valid triangle')
|
||||
depth = np.sqrt((side_len**2 - (width / 2)**2))
|
||||
|
||||
return EdgeSeqFactory.from_verts([0, 0], [width / 2, -depth], [width, 0])
|
||||
|
||||
# --- SVG ----
|
||||
@staticmethod
|
||||
def halfs_from_svg(svg_filepath, target_height=None):
|
||||
"""Load a shape from an SVG and split it in half (vertically)
|
||||
|
||||
* target_height -- scales the shape s.t. it's height matches the given
|
||||
number
|
||||
|
||||
Shapes restrictions:
|
||||
1) every path in the provided SVG is assumed to form a closed loop
|
||||
that has exactly 2 intersection points with a vertical line
|
||||
passing though the middle of the shape
|
||||
2) The paths should not be nested (inside each other) or intersect
|
||||
as to not create disconnected pieces of the edge when used in
|
||||
shape projection
|
||||
"""
|
||||
paths, _ = svgpath.svg2paths(svg_filepath)
|
||||
|
||||
# Scaling
|
||||
if target_height is not None:
|
||||
bbox = bbox_paths(paths)
|
||||
scale = target_height / (bbox[-1] - bbox[-2])
|
||||
paths = [p.scaled(scale) for p in paths]
|
||||
|
||||
# Get the half-shapes
|
||||
left, right = split_half_svg_paths(paths)
|
||||
|
||||
# Turn into Edge Sequences
|
||||
left_seqs = [EdgeSeqFactory.from_svg_path(p) for p in left]
|
||||
right_seqs = [EdgeSeqFactory.from_svg_path(p) for p in right]
|
||||
|
||||
# In SVG OY is looking downward, we are using OY looking upward
|
||||
# Flip the shape to align
|
||||
bbox = bbox_paths(paths)
|
||||
center_y = (bbox[2] + bbox[3]) / 2
|
||||
left_seqs = [p.reflect([bbox[0], center_y],
|
||||
[bbox[1], center_y]) for p in left_seqs]
|
||||
right_seqs = [p.reflect([bbox[0], center_y],
|
||||
[bbox[1], center_y]) for p in right_seqs]
|
||||
|
||||
# Edge orientation s.t. the shortcut directions align with OY
|
||||
# It preserves the correct relative placement of the shapes later
|
||||
for p in left_seqs:
|
||||
if (p.shortcut()[1][1] - p.shortcut()[0][1]) < 0:
|
||||
p.reverse()
|
||||
for p in right_seqs:
|
||||
if (p.shortcut()[1][1] - p.shortcut()[0][1]) < 0:
|
||||
p.reverse()
|
||||
|
||||
return left_seqs, right_seqs
|
||||
|
||||
# --- For Curves ---
|
||||
def _fit_pass_point(cp, target_location):
|
||||
""" Fit the control point of basic [[0, 0] -> [1, 0]] Quadratic Bezier s.t.
|
||||
it passes through the target location.
|
||||
|
||||
* cp - initial guess for Quadratic Bezier control point coordinates
|
||||
(relative to the edge)
|
||||
* target_location -- target to fit extremum to --
|
||||
expressed in RELATIVE coordinates to your desired edge
|
||||
"""
|
||||
control_bezier = np.array([
|
||||
[0, 0],
|
||||
cp,
|
||||
[1, 0]
|
||||
])
|
||||
params = list_to_c(control_bezier)
|
||||
curve = svgpath.QuadraticBezier(*params)
|
||||
|
||||
inter_segment = svgpath.Line(
|
||||
target_location[0] + 1j * target_location[1] * 2,
|
||||
target_location[0] + 1j * (- target_location[1] * 2)
|
||||
)
|
||||
|
||||
intersect_t = curve.intersect(inter_segment)
|
||||
point = curve.point(intersect_t[0][0])
|
||||
|
||||
diff = abs(point - list_to_c(target_location))
|
||||
|
||||
return diff**2
|
||||
|
||||
|
||||
def _fit_tangents(cp, target_tangent_start, target_tangent_end, reg_strength=0.01):
|
||||
""" Fit the control point of basic [[0, 0] -> [1, 0]] Quadratic Bezier s.t.
|
||||
it's expremum is close to target location.
|
||||
|
||||
* cp - initial guess for Quadratic Bezier control point coordinates
|
||||
(relative to the edge)
|
||||
* target_location -- target to fit extremum to --
|
||||
expressed in RELATIVE coordinates to your desired edge
|
||||
"""
|
||||
control_bezier = np.array([
|
||||
[0, 0],
|
||||
cp,
|
||||
[1, 0]
|
||||
])
|
||||
params = list_to_c(control_bezier)
|
||||
curve = svgpath.QuadraticBezier(*params)
|
||||
|
||||
fin = 0
|
||||
if target_tangent_start is not None:
|
||||
# NOTE: tangents seems to use opposite left/right convention
|
||||
target0 = target_tangent_start[0] + 1j*target_tangent_start[1]
|
||||
fin += (abs(curve.unit_tangent(0) - target0))**2
|
||||
|
||||
if target_tangent_end is not None:
|
||||
target1 = target_tangent_end[0] + 1j*target_tangent_end[1]
|
||||
fin += (abs(curve.unit_tangent(1) - target1))**2
|
||||
|
||||
# NOTE: Tried _max_curvature() and Y value regularizaton,
|
||||
# but it seems like they are not needed
|
||||
return fin
|
||||
|
||||
|
||||
# ---- For SVG Loading ----
|
||||
|
||||
def split_half_svg_paths(paths):
|
||||
"""Sepate SVG paths in half over the vertical line -- for insertion into an
|
||||
edge side
|
||||
|
||||
Paths shapes restrictions:
|
||||
1) every path in the provided list is assumed to form a closed loop
|
||||
that has
|
||||
exactly 2 intersection points with a vetrical line passing though the
|
||||
middle of the shape
|
||||
2) The paths geometry should not be nested
|
||||
as to not create disconnected pieces of the edge when used in
|
||||
shape projection
|
||||
|
||||
"""
|
||||
# Shape Bbox
|
||||
bbox = bbox_paths(paths)
|
||||
center_x = (bbox[0] + bbox[1]) / 2
|
||||
|
||||
# Mid-Intersection
|
||||
inter_segment = svgpath.Line(
|
||||
center_x + 1j * bbox[2],
|
||||
center_x + 1j * bbox[3]
|
||||
)
|
||||
|
||||
right, left = [], []
|
||||
for p in paths:
|
||||
# Intersect points
|
||||
intersect_t = p.intersect(inter_segment)
|
||||
|
||||
if len(intersect_t) != 2:
|
||||
raise ValueError(f'SplitSVGHole::ERROR::Each Provided Svg path should cross vertical like exactly 2 times')
|
||||
|
||||
# Split
|
||||
from_T, to_T = intersect_t[0][0][0], intersect_t[1][0][0]
|
||||
if to_T < from_T:
|
||||
from_T, to_T = to_T, from_T
|
||||
|
||||
side_1 = p.cropped(from_T, to_T)
|
||||
# This order should preserve continuity
|
||||
side_2 = svgpath.Path(
|
||||
*p.cropped(to_T, 1)._segments,
|
||||
*p.cropped(0, from_T)._segments)
|
||||
|
||||
# Collect correctly
|
||||
if side_1.bbox()[2] > center_x:
|
||||
side_1, side_2 = side_2, side_1
|
||||
|
||||
right.append(side_2)
|
||||
left.append(side_1)
|
||||
|
||||
return left, right
|
||||
Reference in New Issue
Block a user