Files
OpenECAD_Project/Bethany/lib/curves.py
2024-08-16 17:50:51 +08:00

423 lines
17 KiB
Python

import numpy as np
import matplotlib.lines as lines
import matplotlib.patches as patches
from .math_utils import rads_to_degs, angle_from_vector_to_x
from .macro import *
# FIXME: these two functions can be treated as static method
def construct_curve_from_dict(stat):
if stat['type'] == "Line3D":
return Line.from_dict(stat)
elif stat['type'] == "Circle3D":
return Circle.from_dict(stat)
elif stat['type'] == "Arc3D":
return Arc.from_dict(stat)
else:
raise NotImplementedError("curve type not supported yet: {}".format(stat['type']))
def construct_curve_from_vector(vec, start_point, is_numerical=True):
type = vec[0]
if type == LINE_IDX:
return Line.from_vector(vec, start_point, is_numerical=is_numerical)
elif type == CIRCLE_IDX:
return Circle.from_vector(vec, start_point, is_numerical=is_numerical)
elif type == ARC_IDX:
res = Arc.from_vector(vec, start_point, is_numerical=is_numerical)
if res is None: # for visualization purpose, replace illed arc with line
return Line.from_vector(vec, start_point, is_numerical=is_numerical)
return res
else:
raise NotImplementedError("curve type not supported yet: command idx {}".format(vec[0]))
####################### base #######################
class CurveBase(object):
"""Base class for curve. All types of curves shall inherit from this."""
def __init__(self):
pass
@staticmethod
def from_dict(stat):
"""construct curve from json data"""
raise NotImplementedError
@staticmethod
def from_vector(vec, start_point, is_numerical=True):
"""construct curve from vector representation"""
raise NotImplementedError
@property
def bbox(self):
"""compute bounding box of the curve"""
raise NotImplementedError
def direction(self, from_start=True):
"""return a vector indicating the curve direction"""
raise NotImplementedError
def transform(self, translate, scale):
"""linear transformation"""
raise NotImplementedError
def flip(self, axis):
"""flip the curve about axis"""
raise NotImplementedError
def reverse(self):
"""reverse the curve direction"""
raise NotImplementedError
def numericalize(self, n=256):
"""quantize curve parameters into integers"""
raise NotImplementedError
def to_vector(self):
"""represent curve using a vector. see macro.py"""
raise NotImplementedError
def draw(self, ax, color):
"""draw the curve using matplotlib"""
raise NotImplementedError
def sample_points(self, n=32):
"""uniformly sample points from the curve"""
raise NotImplementedError
####################### curves #######################
class Line(CurveBase):
def __init__(self, start_point, end_point):
super(Line, self).__init__()
self.start_point = start_point
self.end_point = end_point
def __str__(self):
return "Line: start({}), end({})".format(self.start_point.round(4), self.end_point.round(4))
@staticmethod
def from_dict(stat):
assert stat['type'] == "Line3D"
start_point = np.array([stat['start_point']['x'] * 1000,
stat['start_point']['y'] * 1000])
end_point = np.array([stat['end_point']['x'] * 1000,
stat['end_point']['y'] * 1000])
return Line(start_point, end_point)
@staticmethod
def from_vector(vec, start_point, is_numerical=True):
return Line(start_point, vec[1:3])
@property
def bbox(self):
points = np.stack([self.start_point, self.end_point], axis=0)
return np.stack([np.min(points, axis=0), np.max(points, axis=0)], axis=0)
def direction(self, from_start=True):
return self.end_point - self.start_point
def transform(self, translate, scale):
self.start_point = (self.start_point + translate) * scale
self.end_point = (self.end_point + translate) * scale
def flip(self, axis):
if axis == 'x':
self.start_point[1], self.end_point[1] = -self.start_point[1], -self.end_point[1]
elif axis == 'y':
self.start_point[0], self.end_point[0] = -self.start_point[0], -self.end_point[0]
elif axis == 'xy':
self.start_point = self.start_point * -1
self.end_point = self.end_point * -1
else:
raise ValueError("axis = {}".format(axis))
def reverse(self):
self.start_point, self.end_point = self.end_point, self.start_point
def numericalize(self, n=256):
self.start_point = self.start_point.round().clip(min=0, max=n-1).astype(np.int)
self.end_point = self.end_point.round().clip(min=0, max=n-1).astype(np.int)
def to_vector(self):
vec = [LINE_IDX, self.end_point[0], self.end_point[1]]
return np.array(vec + [PAD_VAL] * (1 + N_ARGS - len(vec)))
def draw(self, ax, color):
xdata = [self.start_point[0], self.end_point[0]]
ydata = [self.start_point[1], self.end_point[1]]
l1 = lines.Line2D(xdata, ydata, lw=1, color=color, axes=ax)
ax.add_line(l1)
ax.plot(self.start_point[0], self.start_point[1], 'ok', color=color)
# ax.plot(self.end_point[0], self.end_point[1], 'ok')
def sample_points(self, n=32):
return np.linspace(self.start_point, self.end_point, num=n)
class Arc(CurveBase):
def __init__(self, start_point, end_point, center, radius,
normal=None, start_angle=None, end_angle=None, ref_vec=None, mid_point=None):
super(Arc, self).__init__()
self.start_point = start_point
self.end_point = end_point
self.center = center
self.radius = radius
self.normal = normal
self.start_angle = start_angle
self.end_angle = end_angle
self.ref_vec = ref_vec
if mid_point is None:
self.mid_point = self.get_mid_point()
else:
self.mid_point = mid_point
def __str__(self):
return "Arc: start({}), end({}), mid({})".format(self.start_point.round(4), self.end_point.round(4),
self.mid_point.round(4))
@staticmethod
def from_dict(stat):
assert stat['type'] == "Arc3D"
start_point = np.array([stat['start_point']['x'] * 1000,
stat['start_point']['y'] * 1000])
end_point = np.array([stat['end_point']['x'] * 1000,
stat['end_point']['y'] * 1000])
center = np.array([stat['center_point']['x'] * 1000,
stat['center_point']['y'] * 1000])
radius = stat['radius'] * 1000
normal = np.array([stat['normal']['x'],
stat['normal']['y'],
stat['normal']['z']])
start_angle = stat['start_angle']
end_angle = stat['end_angle']
ref_vec = np.array([stat['reference_vector']['x'],
stat['reference_vector']['y']])
return Arc(start_point, end_point, center, radius, normal, start_angle, end_angle, ref_vec)
@staticmethod
def from_vector(vec, start_point, is_numerical=True):
end_point = vec[1:3]
sweep_angle = vec[3] / 256 * 2 * np.pi if is_numerical else vec[3]
clock_sign = vec[4]
s2e_vec = end_point - start_point
if np.linalg.norm(s2e_vec) == 0:
return None
radius = (np.linalg.norm(s2e_vec) / 2) / np.sin(sweep_angle / 2)
s2e_mid = (start_point + end_point) / 2
vertical = np.cross(s2e_vec, [0, 0, 1])[:2]
vertical = vertical / np.linalg.norm(vertical)
if clock_sign == 0:
vertical = -vertical
center_point = s2e_mid - vertical * (radius * np.cos(sweep_angle / 2))
start_angle = 0
end_angle = sweep_angle
if clock_sign == 0:
ref_vec = end_point - center_point
else:
ref_vec = start_point - center_point
ref_vec = ref_vec / np.linalg.norm(ref_vec)
return Arc(start_point, end_point, center_point, radius,
start_angle=start_angle, end_angle=end_angle, ref_vec=ref_vec)
def get_angles_counterclockwise(self, eps=1e-8):
c2s_vec = (self.start_point - self.center) / (np.linalg.norm(self.start_point - self.center) + eps)
c2m_vec = (self.mid_point - self.center) / (np.linalg.norm(self.mid_point - self.center) + eps)
c2e_vec = (self.end_point - self.center) / (np.linalg.norm(self.end_point - self.center) + eps)
angle_s, angle_m, angle_e = angle_from_vector_to_x(c2s_vec), angle_from_vector_to_x(c2m_vec), \
angle_from_vector_to_x(c2e_vec)
angle_s, angle_e = min(angle_s, angle_e), max(angle_s, angle_e)
if not angle_s < angle_m < angle_e:
angle_s, angle_e = angle_e - np.pi * 2, angle_s
return angle_s, angle_e
@property
def bbox(self):
points = [self.start_point, self.end_point]
angle_s, angle_e = self.get_angles_counterclockwise()
if angle_s < 0 < angle_e:
points.append(np.array([self.center[0] + self.radius, self.center[1]]))
if angle_s < np.pi / 2 < angle_e or angle_s < -np.pi / 2 * 3 < angle_e:
points.append(np.array([self.center[0], self.center[1] + self.radius]))
if angle_s < np.pi < angle_e or angle_s < -np.pi < angle_e:
points.append(np.array([self.center[0] - self.radius, self.center[1]]))
if angle_s < np.pi / 2 * 3 < angle_e or angle_s < -np.pi/2 < angle_e:
points.append(np.array([self.center[0], self.center[1] - self.radius]))
points = np.stack(points, axis=0)
return np.stack([np.min(points, axis=0), np.max(points, axis=0)], axis=0)
def direction(self, from_start=True):
if from_start:
return self.mid_point - self.start_point
else:
return self.end_point - self.mid_point
@property
def clock_sign(self):
"""get a boolean sign indicating whether the arc is on top of s->e """
s2e = self.end_point - self.start_point
s2m = self.mid_point - self.start_point
sign = np.cross(s2m, s2e) >= 0 # counter-clockwise
return sign
def get_mid_point(self):
mid_angle = (self.start_angle + self.end_angle) / 2
rot_mat = np.array([[np.cos(mid_angle), -np.sin(mid_angle)],
[np.sin(mid_angle), np.cos(mid_angle)]])
mid_vec = rot_mat @ self.ref_vec
return self.center + mid_vec * self.radius
def transform(self, translate, scale):
self.start_point = (self.start_point + translate) * scale
self.mid_point = (self.mid_point + translate) * scale
self.end_point = (self.end_point + translate) * scale
self.center = (self.center + translate) * scale
if isinstance(scale * 1.0, float):
self.radius = abs(self.radius * scale)
def flip(self, axis):
if axis == 'x':
self.transform(0, np.array([1, -1]))
new_ref_vec_angle = angle_from_vector_to_x(self.ref_vec) + self.end_angle - self.start_angle
self.ref_vec = np.array([np.cos(new_ref_vec_angle), -np.sin(new_ref_vec_angle)])
elif axis == 'y':
self.transform(0, np.array([-1, 1]))
new_ref_vec_angle = angle_from_vector_to_x(self.ref_vec) + self.end_angle - self.start_angle
self.ref_vec = np.array([-np.cos(new_ref_vec_angle), np.sin(new_ref_vec_angle)])
elif axis == 'xy':
self.transform(0, -1)
self.ref_vec = self.ref_vec * -1
else:
raise ValueError("axis = {}".format(axis))
def reverse(self):
self.start_point, self.end_point = self.end_point, self.start_point
def numericalize(self, n=256):
self.start_point = self.start_point.round().clip(min=0, max=n-1).astype(np.int)
self.mid_point = self.mid_point.round().clip(min=0, max=n-1).astype(np.int)
self.end_point = self.end_point.round().clip(min=0, max=n-1).astype(np.int)
self.center = self.center.round().clip(min=0, max=n-1).astype(np.int)
tmp = np.array([self.start_angle, self.end_angle])
self.start_angle, self.end_angle = (tmp / (2 * np.pi) * n).round().clip(
min=0, max=n-1).astype(np.int)
def to_vector(self):
sweep_angle = max(abs(self.start_angle - self.end_angle), 1)
return np.array([ARC_IDX, self.end_point[0], self.end_point[1], sweep_angle, int(self.clock_sign), PAD_VAL,
*[PAD_VAL] * N_ARGS_EXT])
def draw(self, ax, color):
ref_vec_angle = rads_to_degs(angle_from_vector_to_x(self.ref_vec))
start_angle = rads_to_degs(self.start_angle)
end_angle = rads_to_degs(self.end_angle)
diameter = 2.0 * self.radius
ap = patches.Arc(
(self.center[0], self.center[1]),
diameter,
diameter,
angle=ref_vec_angle,
theta1=start_angle,
theta2=end_angle,
lw=1,
color=color
)
ax.add_patch(ap)
ax.plot(self.start_point[0], self.start_point[1], 'ok', color=color)
# ax.plot(self.center[0], self.center[1], 'ok', color=color)
ax.plot(self.mid_point[0], self.mid_point[1], 'ok', color=color)
# ax.plot(self.end_point[0], self.end_point[1], 'ok')
def sample_points(self, n=32):
c2s_vec = (self.start_point - self.center) / np.linalg.norm(self.start_point - self.center)
c2m_vec = (self.mid_point - self.center) / np.linalg.norm(self.mid_point - self.center)
c2e_vec = (self.end_point - self.center) / np.linalg.norm(self.end_point - self.center)
angle_s, angle_m, angle_e = angle_from_vector_to_x(c2s_vec), angle_from_vector_to_x(c2m_vec), \
angle_from_vector_to_x(c2e_vec)
angle_s, angle_e = min(angle_s, angle_e), max(angle_s, angle_e)
if not angle_s < angle_m < angle_e:
angle_s, angle_e = angle_e - np.pi * 2, angle_s
angles = np.linspace(angle_s, angle_e, num=n)
points = np.stack([np.cos(angles), np.sin(angles)], axis=1) * self.radius + self.center[np.newaxis]
return points
class Circle(CurveBase):
def __init__(self, center, radius, normal=None):
super(Circle, self).__init__()
self.center = center
self.radius = radius
self.normal = normal
def __str__(self):
return "Circle: center({}), radius({})".format(self.center.round(4), round(self.radius, 4))
@staticmethod
def from_dict(stat):
assert stat['type'] == "Circle3D"
center = np.array([stat['center_point']['x'] * 1000,
stat['center_point']['y'] * 1000])
radius = stat['radius'] * 1000
normal = np.array([stat['normal']['x'],
stat['normal']['y'],
stat['normal']['z']])
return Circle(center, radius, normal)
@staticmethod
def from_vector(vec, start_point=None, is_numerical=True):
return Circle(vec[1:3], vec[5])
@property
def bbox(self):
return np.stack([self.center - self.radius, self.center + self.radius], axis=0)
def direction(self, from_start=True):
return self.center - self.start_point
@property
def start_point(self):
return np.array([self.center[0] - self.radius, self.center[1]])
@property
def end_point(self):
return np.array([self.center[0] + self.radius, self.center[1]])
def transform(self, translate, scale):
self.center = (self.center + translate) * scale
self.radius = self.radius * scale
def flip(self, axis):
if axis == 'x':
self.center[1] = -self.center[1]
elif axis == 'y':
self.center[0] = -self.center[0]
elif axis == 'xy':
self.center = self.center * -1
else:
raise ValueError("axis = {}".format(axis))
def reverse(self):
pass
def numericalize(self, n=256):
self.center = self.center.round().clip(min=0, max=n-1).astype(np.int)
self.radius = np.round(self.radius).clip(min=1, max=n-1).astype(np.int)
def to_vector(self):
vec = [CIRCLE_IDX, self.center[0], self.center[1], PAD_VAL, PAD_VAL, self.radius]
return np.array(vec + [PAD_VAL] * (1 + N_ARGS - len(vec)))
def draw(self, ax, color):
ap = patches.Circle((self.center[0], self.center[1]), self.radius,
lw=1, fill=None, color=color)
ax.add_patch(ap)
ax.plot(self.center[0], self.center[1], 'ok')
def sample_points(self, n=32):
angles = np.linspace(0, np.pi * 2, num=n, endpoint=False)
points = np.stack([np.cos(angles), np.sin(angles)], axis=1) * self.radius + self.center[np.newaxis]
return points