mirror of
https://github.com/Yuki-Kokomi/OpenECAD_Project.git
synced 2026-02-04 00:33:22 -05:00
423 lines
17 KiB
Python
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
|