new ip
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
The geometry sub-module offers a geometric representation of curves.
|
||||
|
||||
specific maintenance notes:
|
||||
- Explicit variable declaration for geometric objects. ** You can't dynamically declare instance variables. **
|
||||
Refer to the docs for more info https://docs.python.org/3/reference/datamodel.html?highlight=__slots__#slots
|
||||
- The origin is at the bottom-left. As such, all svg_curves must be transformed from the top-left coordinate system
|
||||
before generating geometric objects.
|
||||
"""
|
||||
|
||||
from svg_to_gcode.geometry._vector import Vector
|
||||
from svg_to_gcode.geometry._matrix import Matrix, IdentityMatrix, RotationMatrix
|
||||
|
||||
from svg_to_gcode.geometry._abstract_curve import Curve
|
||||
from svg_to_gcode.geometry._line import Line
|
||||
from svg_to_gcode.geometry._circular_arc import CircularArc
|
||||
from svg_to_gcode.geometry._elliptical_arc import EllipticalArc
|
||||
from svg_to_gcode.geometry._quadratic_bazier import QuadraticBezier
|
||||
from svg_to_gcode.geometry._cubic_bazier import CubicBazier
|
||||
|
||||
from svg_to_gcode.geometry._abstract_chain import Chain
|
||||
from svg_to_gcode.geometry._line_segment_chain import LineSegmentChain
|
||||
from svg_to_gcode.geometry._smooth_arc_chain import SmoothArcChain
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,104 @@
|
||||
from collections.abc import Iterable
|
||||
|
||||
from svg_to_gcode.geometry import Curve
|
||||
from svg_to_gcode import formulas
|
||||
|
||||
|
||||
class Chain(Curve):
|
||||
"""
|
||||
The Chain class is used to store a sequence of consecutive curves. When considered as a whole, Chains can also be
|
||||
viewed as a single, continuous curve. They inherit from the Curve class and are equipped with the subsequent point()
|
||||
and derivative() methods.
|
||||
"""
|
||||
|
||||
__slots__ = '_curves'
|
||||
|
||||
def __init__(self, curves=None):
|
||||
self._curves = []
|
||||
|
||||
if curves is not None:
|
||||
self.extend(curves)
|
||||
|
||||
def __iter__(self):
|
||||
yield from self._curves
|
||||
|
||||
def length(self):
|
||||
"""
|
||||
Return the geometric length of the chain.
|
||||
The __len__ magic method wasn't overridden to avoid ambiguity between total length and chain size.
|
||||
"""
|
||||
return sum([curve.length() for curve in self._curves])
|
||||
|
||||
def chain_size(self):
|
||||
"""
|
||||
Return the number of curves in the chain.
|
||||
The __len__ magic method wasn't overridden to avoid ambiguity between total length and chain size.
|
||||
"""
|
||||
return len(self._curves)
|
||||
|
||||
def get(self, index: int) -> Curve:
|
||||
"""Return a curve at a given index"""
|
||||
return self._curves[index]
|
||||
|
||||
def append(self, new_curve: Curve):
|
||||
"""Append a new curve to the chain"""
|
||||
raise NotImplementedError("All chains must implement an append command")
|
||||
|
||||
def extend(self, new_curves: Iterable):
|
||||
"""Extend the Chain with an iterable"""
|
||||
for new_curve in new_curves:
|
||||
self.append(new_curve)
|
||||
|
||||
def merge(self, chain: "Chain"):
|
||||
"""
|
||||
Merge the Chain instance with another Chain.
|
||||
Equivalent to self.extend() but includes additional checks.
|
||||
"""
|
||||
if not chain._curves:
|
||||
return
|
||||
|
||||
if self._curves:
|
||||
assert self.append(chain._curves[0])
|
||||
|
||||
self._curves.extend(chain._curves[1:])
|
||||
|
||||
def remove_from_first(self, number_of_curves: int):
|
||||
"""Remove n curves starting from the first"""
|
||||
for i in range(number_of_curves):
|
||||
self._curves.pop(0)
|
||||
|
||||
def remove_from_last(self, number_of_curves: int):
|
||||
"""Remove n curves starting from the last"""
|
||||
for i in range(len(self._curves) - 1, number_of_curves + 1, -1):
|
||||
self._curves.pop(-1)
|
||||
|
||||
def _get_curve_t(self, t):
|
||||
lengths = [curve.length() for curve in self._curves]
|
||||
t_position = t * sum(lengths)
|
||||
|
||||
position = 0
|
||||
for i, length in enumerate(lengths):
|
||||
position += length
|
||||
if position > t_position:
|
||||
break
|
||||
|
||||
curve_t = formulas.inv_linear_map(position - length, position, t_position)
|
||||
|
||||
return self._curves[i], curve_t
|
||||
|
||||
def point(self, t):
|
||||
if self.chain_size() == 0:
|
||||
raise ValueError("Chain.point was called before adding any curves to the chain.")
|
||||
|
||||
curve, curve_t = self._get_curve_t(t)
|
||||
return curve.point(curve_t)
|
||||
|
||||
def derivative(self, t):
|
||||
if self.chain_size() == 0:
|
||||
raise ValueError("Chain.derivative was called before adding any curves to the chain.")
|
||||
|
||||
curve, curve_t = self._get_curve_t(t)
|
||||
return curve.derivative(curve_t)
|
||||
|
||||
def sanity_check(self):
|
||||
pass
|
||||
@@ -0,0 +1,74 @@
|
||||
from svg_to_gcode import formulas
|
||||
from svg_to_gcode.geometry import Vector
|
||||
|
||||
|
||||
class Curve:
|
||||
"""
|
||||
The Curve abstract class is the cornerstone of the geometry sub-module. Child classes inherit from it to represent
|
||||
different types of curves.
|
||||
|
||||
:param self.start: the first point of the curve
|
||||
:type self.start: Vector
|
||||
|
||||
:param self.end: the last point of the curve
|
||||
:type self.end: Vector
|
||||
"""
|
||||
|
||||
__slots__ = 'start', 'end'
|
||||
|
||||
def point(self, t: float) -> Vector:
|
||||
"""
|
||||
The point method returns a point along the curve.
|
||||
|
||||
:param t: t is a number between 0 and 1.
|
||||
:return: the point at distance t*self.length from self.start. For t=0.5, the point half-way across the curve
|
||||
would be returned.
|
||||
"""
|
||||
raise NotImplementedError("point(self, t) must be implemented")
|
||||
|
||||
def derivative(self, t):
|
||||
"""
|
||||
The derivative method returns a derivative at a point along the curve.
|
||||
|
||||
:param t: t is a number between 0 and 1.
|
||||
:return: the derivative at self.point(t)
|
||||
"""
|
||||
raise NotImplementedError("derivative(self, t) must be implemented")
|
||||
|
||||
def sanity_check(self):
|
||||
"""Verify if that the curve is valid."""
|
||||
raise NotImplementedError("sanity_check(self) must be implemented")
|
||||
|
||||
# A custom print representation is trivial to implement and useful for debugging
|
||||
def __repr__(self):
|
||||
raise NotImplementedError("__repr__(self) must be implemented")
|
||||
|
||||
@staticmethod
|
||||
def max_distance(curve1: "Curve", curve2: "Curve", t_range1=(0, 1), t_range2=(0, 1), samples=9):
|
||||
|
||||
"""
|
||||
Return the approximate maximum distance between two Curves for different values of t.
|
||||
|
||||
WARNING: should only be used when comparing relatively flat curve segments. If one of the two
|
||||
curves has very eccentric humps, the tip of the hump may not be sampled.
|
||||
|
||||
:param curve1: the first of two curves to be compared.
|
||||
:param curve2: the second of two curves to be compared.
|
||||
:param t_range1: (min_t, max_t) the range of t values which should be sampled in the first curve. The default
|
||||
value of (0, 1) samples the whole curve. A value of (0, 5) would only sample from the first half of the curve.
|
||||
:param t_range2: (min_t, max_t) the range of t values which should be sampled in the second curve. The default
|
||||
value of (0, 1) samples the whole curve. A value of (0, 5) would only sample from the first half of the curve.
|
||||
:param samples: the number of samples which should be taken. Higher values are slower but more accurate.
|
||||
:return: the approximate maximum distance
|
||||
"""
|
||||
|
||||
maximum_distance = 0
|
||||
for i in range(samples):
|
||||
t = (i + 1) / (samples + 1)
|
||||
t1 = formulas.linear_map(t_range1[0], t_range1[1], t)
|
||||
t2 = formulas.linear_map(t_range2[0], t_range2[1], t)
|
||||
|
||||
distance = abs(curve1.point(t1) - curve2.point(t2))
|
||||
maximum_distance = distance if distance > maximum_distance else maximum_distance
|
||||
|
||||
return maximum_distance
|
||||
@@ -0,0 +1,72 @@
|
||||
import math
|
||||
|
||||
from svg_to_gcode.geometry import Vector
|
||||
from svg_to_gcode.geometry import Curve
|
||||
from svg_to_gcode import formulas
|
||||
from svg_to_gcode import TOLERANCES
|
||||
|
||||
|
||||
class CircularArc(Curve):
|
||||
"""The CircularArc class inherits from the abstract Curve class and describes a circular arc."""
|
||||
|
||||
__slots__ = 'center', 'radius', 'start_angle', 'end_angle'
|
||||
|
||||
# ToDo use different instantiation parameters to be consistent with elliptical arcs
|
||||
def __init__(self, start: Vector, end: Vector, center: Vector):
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.center = center
|
||||
|
||||
self.radius = abs(self.start - self.center)
|
||||
self.start_angle = self.point_to_angle(self.start)
|
||||
self.end_angle = self.point_to_angle(self.end)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Arc(start: {self.start}, end: {self.end}, center: {self.center})"
|
||||
|
||||
def length(self):
|
||||
return abs(self.start_angle - self.end_angle) * self.radius
|
||||
|
||||
def angle_to_point(self, rad):
|
||||
at_origin = self.radius * Vector(math.cos(rad), math.sin(rad))
|
||||
translated = at_origin + self.center
|
||||
return translated
|
||||
|
||||
def point_to_angle(self, point: Vector):
|
||||
translated = (point - self.center)/self.radius # translate the point onto the unit circle
|
||||
return math.acos(translated.x)
|
||||
|
||||
def point(self, t):
|
||||
angle = formulas.linear_map(self.start_angle, self.end_angle, t)
|
||||
return self.angle_to_point(angle)
|
||||
|
||||
def derivative(self, t):
|
||||
position = self.point(t)
|
||||
return (self.center.x - position.x) / (position.y - self.center.y)
|
||||
|
||||
def sanity_check(self):
|
||||
# Assert that the Arc is not a point or a line
|
||||
try:
|
||||
assert abs(self.start - self.end) > TOLERANCES["input"]
|
||||
except AssertionError:
|
||||
raise ValueError(f"Arc is a point. The start and the end points are equivalent: "
|
||||
f"|{self.start} - {self.end}| <= {TOLERANCES['input']}")
|
||||
|
||||
try:
|
||||
assert abs(self.start - self.center) > TOLERANCES["input"]
|
||||
except AssertionError:
|
||||
raise ValueError(f"Arc is a line. The start and the center points are equivalent, "
|
||||
f"|{self.start} - {self.center}| <= {TOLERANCES['input']}")
|
||||
|
||||
try:
|
||||
assert abs(self.end - self.center) > TOLERANCES["input"]
|
||||
except AssertionError:
|
||||
raise ValueError(f"Arc is a line. The end and the center points are equivalent, "
|
||||
f"|{self.end} - {self.center}| <= {TOLERANCES['input']}")
|
||||
|
||||
# Assert that the center is equidistant from the start and the end
|
||||
try:
|
||||
assert abs(abs(self.start - self.center) - abs(self.end - self.center)) < TOLERANCES['input']
|
||||
except AssertionError:
|
||||
raise ValueError(f"Center is not equidistant to the start and end points within tolerance, "
|
||||
f"|{abs(self.start - self.center)} - {abs(self.end - self.center)}| >= {TOLERANCES['input']}")
|
||||
@@ -0,0 +1,32 @@
|
||||
from svg_to_gcode.geometry import Vector
|
||||
from svg_to_gcode.geometry import Curve
|
||||
|
||||
|
||||
class CubicBazier(Curve):
|
||||
"""The CubicBazier class inherits from the abstract Curve class and describes a cubic bazier."""
|
||||
|
||||
__slots__ = 'control1', 'control2'
|
||||
|
||||
def __init__(self, start: Vector, end: Vector, control1: Vector, control2: Vector):
|
||||
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.control1 = control1
|
||||
self.control2 = control2
|
||||
|
||||
def __repr__(self):
|
||||
return f"CubicBazier(start: {self.start}, end: {self.end}, control1: {self.control1}, control2: {self.control2})"
|
||||
|
||||
def point(self, t):
|
||||
return (1-t)**3 * self.start +\
|
||||
3 * (1-t)**2 * t * self.control1 +\
|
||||
3 * (1-t) * t**2 * self.control2 +\
|
||||
t**3 * self.end
|
||||
|
||||
def derivative(self, t):
|
||||
return 3 * (1-t)**2 * (self.control1 - self.start) +\
|
||||
6 * (1-t) * t * (self.control2 - self.control1) +\
|
||||
3 * t**2 * (self.end - self.control2)
|
||||
|
||||
def sanity_check(self):
|
||||
pass
|
||||
@@ -0,0 +1,58 @@
|
||||
import math
|
||||
|
||||
from svg_to_gcode import formulas
|
||||
from svg_to_gcode.geometry import Vector, RotationMatrix
|
||||
from svg_to_gcode.geometry import Curve
|
||||
|
||||
|
||||
class EllipticalArc(Curve):
|
||||
"""The EllipticalArc class inherits from the abstract Curve class and describes an elliptical arc."""
|
||||
|
||||
__slots__ = 'center', 'radii', 'rotation', 'start_angle', 'sweep_angle', 'end_angle', 'transformation'
|
||||
|
||||
# ToDo apply transformation beforehand (in Path) for consistency with other geometric objects. If you (the reader)
|
||||
# know how to easily apply an affine transformation to an ellipse feel free to make a pull request.
|
||||
def __init__(self, center: Vector, radii: Vector, rotation: float, start_angle: float, sweep_angle: float,
|
||||
transformation: None):
|
||||
|
||||
# Assign and verify arguments
|
||||
self.center = center
|
||||
self.radii = radii
|
||||
self.rotation = rotation
|
||||
self.start_angle = start_angle
|
||||
self.sweep_angle = sweep_angle
|
||||
self.transformation = transformation
|
||||
|
||||
# Calculate missing data
|
||||
self.end_angle = start_angle + sweep_angle
|
||||
self.start = self.angle_to_point(self.start_angle)
|
||||
self.end = self.angle_to_point(self.end_angle)
|
||||
|
||||
self.sanity_check()
|
||||
|
||||
def __repr__(self):
|
||||
return f"EllipticalArc(start: {self.start}, end: {self.end}, center: {self.center}, radii: {self.radii}," \
|
||||
f" rotation: {self.rotation}, start_angle: {self.start_angle}, sweep_angle: {self.sweep_angle})"
|
||||
|
||||
def point(self, t):
|
||||
angle = formulas.linear_map(self.start_angle, self.end_angle, t)
|
||||
return self.angle_to_point(angle)
|
||||
|
||||
def angle_to_point(self, angle):
|
||||
transformed_radii = Vector(self.radii.x * math.cos(angle), self.radii.y * math.sin(angle))
|
||||
point = RotationMatrix(self.rotation) * transformed_radii + self.center
|
||||
|
||||
if self.transformation:
|
||||
point = self.transformation.apply_affine_transformation(point)
|
||||
|
||||
return point
|
||||
|
||||
def derivative(self, t):
|
||||
angle = formulas.linear_map(self.start_angle, self.end_angle, t)
|
||||
return self.angle_to_derivative(angle)
|
||||
|
||||
def angle_to_derivative(self, rad):
|
||||
return -(self.radii.y / self.radii.x) * math.tan(rad)**-1
|
||||
|
||||
def sanity_check(self):
|
||||
pass
|
||||
@@ -0,0 +1,32 @@
|
||||
from svg_to_gcode.geometry import Vector
|
||||
from svg_to_gcode.geometry import Curve
|
||||
from svg_to_gcode import formulas
|
||||
|
||||
|
||||
# A line segment
|
||||
class Line(Curve):
|
||||
"""The Line class inherits from the abstract Curve class and describes a straight line segment."""
|
||||
|
||||
__slots__ = 'slope', 'offset'
|
||||
|
||||
def __init__(self, start, end):
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
self.slope = formulas.line_slope(start, end)
|
||||
self.offset = formulas.line_offset(start, end)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Line(start:{self.start}, end:{self.end}, slope:{self.slope}, offset:{self.offset})"
|
||||
|
||||
def length(self):
|
||||
return abs(self.start - self.end)
|
||||
|
||||
def point(self, t):
|
||||
x = self.start.x + t * (self.end.x - self.start.x)
|
||||
y = self.slope * x + self.offset
|
||||
|
||||
return Vector(x, y)
|
||||
|
||||
def derivative(self, t):
|
||||
return self.slope
|
||||
@@ -0,0 +1,88 @@
|
||||
from svg_to_gcode.geometry import Chain
|
||||
from svg_to_gcode.geometry import Curve, Line, Vector
|
||||
from svg_to_gcode import TOLERANCES
|
||||
|
||||
|
||||
class LineSegmentChain(Chain):
|
||||
"""
|
||||
The LineSegmentChain class inherits form the abstract Chain class. It represents a series of continuous straight
|
||||
line-segments.
|
||||
|
||||
LineSegmentChains can be instantiated either conventionally or through the static method line_segment_approximation(),
|
||||
which approximates any Curve with a series of line-segments contained in a new LineSegmentChain instance.
|
||||
"""
|
||||
def __repr__(self):
|
||||
return f"{type(self)}({len(self._curves)} curves: {[line.__repr__() for line in self._curves[:2]]}...)"
|
||||
|
||||
def append(self, line2: Line):
|
||||
if self._curves:
|
||||
line1 = self._curves[-1]
|
||||
|
||||
# Assert continuity
|
||||
if abs(line1.end - line2.start) > TOLERANCES['input']:
|
||||
raise ValueError(f"The end of the last line is different from the start of the new line"
|
||||
f"|{line1.end} - {line2.start}| >= {TOLERANCES['input']}")
|
||||
|
||||
# Join lines
|
||||
line2.start = line1.end
|
||||
|
||||
self._curves.append(line2)
|
||||
|
||||
@staticmethod
|
||||
def line_segment_approximation(shape, increment_growth=11 / 10, error_cap=None, error_floor=None)\
|
||||
-> "LineSegmentChain":
|
||||
"""
|
||||
This method approximates any shape using straight line segments.
|
||||
|
||||
:param shape: The shape to be approximated.
|
||||
:param increment_growth: the scale by which line_segments grow and shrink. Must be > 1.
|
||||
:param error_cap: the maximum acceptable deviation from the curve.
|
||||
:param error_floor: the maximum minimum deviation from the curve before segment length starts growing again.
|
||||
:return: A LineSegmentChain which approximates the given shape.
|
||||
"""
|
||||
|
||||
error_cap = TOLERANCES['approximation'] if error_cap is None else error_cap
|
||||
error_floor = (increment_growth - 1) * error_cap if error_floor is None else error_floor
|
||||
|
||||
if error_cap <= 0:
|
||||
raise ValueError(f"This algorithm is approximate. error_cap must be a non-zero positive float. Not {error_cap}")
|
||||
|
||||
if increment_growth <= 1:
|
||||
raise ValueError(f"increment_growth must be > 1. Not {increment_growth}")
|
||||
|
||||
lines = LineSegmentChain()
|
||||
|
||||
if isinstance(shape, Line):
|
||||
lines.append(shape)
|
||||
return lines
|
||||
|
||||
t = 0
|
||||
line_start = shape.start
|
||||
increment = 5
|
||||
|
||||
while t < 1:
|
||||
new_t = t + increment
|
||||
|
||||
if new_t > 1:
|
||||
new_t = 1
|
||||
|
||||
line_end = shape.point(new_t)
|
||||
line = Line(line_start, line_end)
|
||||
|
||||
distance = Curve.max_distance(shape, line, t_range1=(t, new_t))
|
||||
|
||||
# If the error is too high, reduce increment and restart cycle
|
||||
if distance > error_cap:
|
||||
increment /= increment_growth
|
||||
continue
|
||||
|
||||
# If the error is very low, increase increment but DO NOT restart cycle.
|
||||
if distance < error_floor:
|
||||
increment *= increment_growth
|
||||
|
||||
lines.append(line)
|
||||
|
||||
line_start = line_end
|
||||
t = new_t
|
||||
|
||||
return lines
|
||||
@@ -0,0 +1,82 @@
|
||||
import math
|
||||
|
||||
from svg_to_gcode.geometry import Vector
|
||||
|
||||
|
||||
class Matrix:
|
||||
"""The Matrix class represents matrices. It's mostly used for applying linear transformations to vectors."""
|
||||
__slots__ = 'number_of_rows', 'number_of_columns', 'matrix_list'
|
||||
|
||||
def __init__(self, matrix_list):
|
||||
"""
|
||||
:param matrix_list: the matrix represented as a list of rows.
|
||||
:type matrix_list: list[list]
|
||||
"""
|
||||
self.number_of_rows = len(matrix_list)
|
||||
self.number_of_columns = len(matrix_list[0])
|
||||
|
||||
if not all([len(row) == self.number_of_columns for row in matrix_list]):
|
||||
raise ValueError("Not a matrix. Rows in matrix_list have different lengths.")
|
||||
|
||||
if not all([all([isinstance(value, float) or isinstance(value, int) for value in row]) for row in matrix_list]):
|
||||
raise ValueError("Not a matrix. matrix_list contains non numeric values.")
|
||||
|
||||
self.matrix_list = matrix_list
|
||||
|
||||
def __repr__(self):
|
||||
matrix_str = "\n ".join([str(row) for row in self])
|
||||
return f"Matrix({matrix_str})"
|
||||
|
||||
def __iter__(self):
|
||||
yield from self.matrix_list
|
||||
|
||||
def __getitem__(self, index: int):
|
||||
return self.matrix_list[index]
|
||||
|
||||
def __mul__(self, other):
|
||||
if isinstance(other, Vector):
|
||||
return self.multiply_vector(other)
|
||||
|
||||
if isinstance(other, Matrix):
|
||||
return self.multiply_matrix(other)
|
||||
|
||||
raise TypeError(f"can't multiply matrix by type '{type(other)}'")
|
||||
|
||||
def multiply_vector(self, other_vector: Vector):
|
||||
if self.number_of_columns != 2:
|
||||
raise ValueError(f"can't multiply matrix with 2D vector. The matrix must have 2 columns, not "
|
||||
f"{self.number_of_columns}")
|
||||
|
||||
x = sum([self[0][k] * other_vector[k] for k in range(self.number_of_columns)])
|
||||
y = sum([self[1][k] * other_vector[k] for k in range(self.number_of_columns)])
|
||||
|
||||
return Vector(x, y)
|
||||
|
||||
def multiply_matrix(self, other_matrix: "Matrix"):
|
||||
if self.number_of_columns != other_matrix.number_of_rows:
|
||||
raise ValueError(f"can't multiply matrices. The first matrix must have the same number of columns as the "
|
||||
f"second has rows. {self.number_of_columns}!={other_matrix.number_of_rows}")
|
||||
|
||||
matrix_list = [[
|
||||
sum([self[i][k] * other_matrix[k][j] for k in range(self.number_of_columns)])
|
||||
for j in range(other_matrix.number_of_columns)]
|
||||
for i in range(self.number_of_rows)]
|
||||
|
||||
return Matrix(matrix_list)
|
||||
|
||||
|
||||
class IdentityMatrix(Matrix):
|
||||
def __init__(self, size):
|
||||
matrix_list = [[int(i == j) for j in range(size)] for i in range(size)]
|
||||
super().__init__(matrix_list)
|
||||
|
||||
|
||||
class RotationMatrix(Matrix):
|
||||
def __init__(self, angle, inverse=False):
|
||||
if not inverse:
|
||||
matrix_list = [[math.cos(angle), -math.sin(angle)],
|
||||
[math.sin(angle), math.cos(angle)]]
|
||||
else:
|
||||
matrix_list = [[math.cos(angle), math.sin(angle)],
|
||||
[-math.sin(angle), math.cos(angle)]]
|
||||
super().__init__(matrix_list)
|
||||
@@ -0,0 +1,29 @@
|
||||
from svg_to_gcode.geometry import Vector
|
||||
from svg_to_gcode.geometry import Curve
|
||||
|
||||
|
||||
class QuadraticBezier(Curve):
|
||||
"""The QuadraticBezier class inherits from the abstract Curve class and describes a quadratic bezier."""
|
||||
|
||||
__slots__ = 'control'
|
||||
|
||||
def __init__(self, start: Vector, end: Vector, control: Vector):
|
||||
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.control = control
|
||||
|
||||
self.sanity_check()
|
||||
|
||||
def __repr__(self):
|
||||
return f"QuadraticBezier(start: {self.start}, end: {self.end}, control: {self.control})"
|
||||
|
||||
def point(self, t):
|
||||
return self.control + ((1 - t)**2) * (self.start - self.control) + (t**2) * (self.end - self.control)
|
||||
|
||||
def derivative(self, t):
|
||||
return 2 * (1 - t) * (self.control - self.start) + 2 * t * (self.end - self.control)
|
||||
|
||||
def sanity_check(self):
|
||||
# ToDo verify if self.start == self.end forms a valid curve under the svg standard
|
||||
pass
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
This is an unfinished class which should not be committed yet.
|
||||
"""
|
||||
|
||||
from svg_to_gcode.geometry import Chain
|
||||
from svg_to_gcode.geometry import CircularArc
|
||||
from svg_to_gcode import TOLERANCES, formulas
|
||||
|
||||
|
||||
class SmoothArcChain(Chain):
|
||||
|
||||
def __repr__(self):
|
||||
return f"SmoothArcs({[arc.__repr__() for arc in self._curves]})"
|
||||
|
||||
def append(self, arc2: CircularArc):
|
||||
|
||||
if self._curves:
|
||||
arc1 = self._curves[-1]
|
||||
|
||||
# Assert continuity
|
||||
try:
|
||||
assert abs(arc1.end - arc2.start) < TOLERANCES['input']
|
||||
except AssertionError:
|
||||
raise ValueError(f"The end of the last arc is different from the start of the new arc, "
|
||||
f"|{arc1.end} - {arc2.start}| >= {TOLERANCES['input']}")
|
||||
|
||||
try:
|
||||
assert abs(arc1.derivative(arc1.end) - arc2.derivative(arc2.start)) < TOLERANCES['input']
|
||||
except AssertionError:
|
||||
raise ValueError(f"The last arc and the new arc form a discontinues curve, "
|
||||
f"|{arc1.derivative(1)} - {arc2.derivative(0)}| >= {TOLERANCES['input']}")
|
||||
|
||||
# Join arcs
|
||||
arc2.start = arc1.end
|
||||
|
||||
self._curves.append(arc2)
|
||||
|
||||
@staticmethod
|
||||
def cubic_bazier_to_arcs(bazier, _arcs=None):
|
||||
|
||||
smooth_arcs = _arcs if _arcs else SmoothArcChain()
|
||||
|
||||
start, control1, control2, end = bazier.start, bazier.control1, bazier.control2, bazier.end
|
||||
tangent_intersect = formulas.line_intersect(start, control1, end, control2)
|
||||
|
||||
# print("tangent_intersect:", tangent_intersect)
|
||||
|
||||
start_length = abs(start - tangent_intersect)
|
||||
end_length = abs(end - tangent_intersect)
|
||||
base_length = abs(start - end)
|
||||
|
||||
# print("start_length:", start_length, "end_length:", end_length, "base_length:", base_length)
|
||||
|
||||
incenter_point = (start_length * end + end_length * start + base_length * tangent_intersect) / \
|
||||
(start_length + end_length + base_length)
|
||||
|
||||
# print("incenter:", incenter_point)
|
||||
|
||||
center1 = formulas.tangent_arc_center(control1, start, incenter_point)
|
||||
center2 = formulas.tangent_arc_center(control2, end, incenter_point)
|
||||
|
||||
# print("centers:", center1, center2)
|
||||
|
||||
smooth_arcs.append(CircularArc(start, incenter_point, center1))
|
||||
smooth_arcs.append(CircularArc(incenter_point, end, center2))
|
||||
|
||||
return smooth_arcs
|
||||
@@ -0,0 +1,52 @@
|
||||
class Vector:
|
||||
"""The Vector class is a simple representation of a 2D vector."""
|
||||
|
||||
__slots__ = 'x', 'y'
|
||||
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __repr__(self):
|
||||
return f"Vector({self.x}, {self.y})"
|
||||
|
||||
def __add__(self, other):
|
||||
return Vector(self.x + other.x, self.y + other.y)
|
||||
|
||||
def __sub__(self, other):
|
||||
return Vector(self.x - other.x, self.y - other.y)
|
||||
|
||||
def __mul__(self, other):
|
||||
if isinstance(other, Vector):
|
||||
return Vector.dot_product(self, other)
|
||||
|
||||
return Vector.scalar_product(self, other)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __truediv__(self, other):
|
||||
if not (isinstance(other, int) or isinstance(other, float)):
|
||||
raise TypeError(f"""unsupported operand type(s) for /: 'Vector' and {type(other)}""")
|
||||
|
||||
return Vector.scalar_product(self, 1/other)
|
||||
|
||||
def __abs__(self):
|
||||
return (self.x ** 2 + self.y ** 2) ** 0.5
|
||||
|
||||
def __iter__(self):
|
||||
yield from (self.x, self.y) # ignore your editor, these parentheses are not redundant
|
||||
|
||||
def __getitem__(self, index: int):
|
||||
return (self.x, self.y)[index]
|
||||
|
||||
@staticmethod
|
||||
def scalar_product(v1: "Vector", n: int):
|
||||
return Vector(v1.x * n, v1.y * n)
|
||||
|
||||
@staticmethod
|
||||
def dot_product(v1: "Vector", v2: "Vector"):
|
||||
return v1.x*v2.x + v1.y*v2.y
|
||||
|
||||
@staticmethod
|
||||
def cross_product(v1: "Vector", v2: "Vector"):
|
||||
return Vector(v1.x * (v2.x + v2.y), v1.y * (v2.x + v2.y))
|
||||
Reference in New Issue
Block a user