Added Cut-Craft extensions (another box makers)

This commit is contained in:
Mario Voigt
2020-07-31 13:46:07 +02:00
parent a45a58f171
commit 6ca5ffdea7
33 changed files with 2234 additions and 0 deletions

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Michael Matthews
#
# This file is part of CutCraft.
#
# CutCraft is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CutCraft is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CutCraft. If not, see <http://www.gnu.org/licenses/>.
from .point import Point
from .line import Line
from .rectangle import Rectangle
from .trace import Trace
from .circle import Circle
from .part import Part
from .neopixel import NeoPixel
from .fingerjoint import FingerJoint
__all__ = ["Point", "Line", "Rectangle", "Trace", "Circle", "Part", "NeoPixel", "FingerJoint"]

View File

@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Michael Matthews
#
# This file is part of CutCraft.
#
# CutCraft is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CutCraft is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CutCraft. If not, see <http://www.gnu.org/licenses/>.
from .point import Point
from .line import Line
from .trace import Trace
from ..util import isclose
from math import pi, sin, cos, asin
class Circle(Trace):
def __init__(self, radius, segments, cuts, cutdepth=0.0, start=0.0, end=pi*2.0, rotation=0.0,
origin=Point(0.0, 0.0), thickness=0.0, kerf=0.0):
super(Circle, self).__init__()
self.thickness = thickness
self.kerf = kerf
partial = True if start != 0.0 or end != pi*2.0 else False
if cuts==0:
c = 0.0
else:
if self.thickness <= 0.0:
raise ValueError("cutcraft.circle: parameter 'thickness' not set when 'cuts' greater than zero.")
if cutdepth <= 0.0:
raise ValueError("cutcraft.circle: parameter 'cutdepth' not set when 'cuts' greater than zero.")
c = asin(self.thickness/2/radius)
if partial:
angles = [[rotation+start+(end-start)/segments*seg, 'SEG'] for seg in range(segments+1)] + \
[[rotation+start+(end-start)/(cuts+1)*cut-c, '<CUT'] for cut in range(1, cuts+1)] + \
[[rotation+start+(end-start)/(cuts+1)*cut+c, 'CUT>'] for cut in range(1, cuts+1)]
else:
angles = [[rotation+end/segments*seg, 'SEG'] for seg in range(segments)] + \
[[rotation+end/cuts*cut-c, '<CUT'] for cut in range(cuts)] + \
[[rotation+end/cuts*cut+c, 'CUT>'] for cut in range(cuts)]
angles = sorted(angles)
if angles[0][1] == 'CUT>':
angles = angles[1:] + [angles[0]]
for i, angle in enumerate(angles):
angle.append(self._cnext(angles, i, 'SEG'))
angle.append(self._cprev(angles, i, 'SEG'))
angle.append(self._cnext(angles, i, 'CUT>') if angle[1]=='<CUT' else None)
angle.append(self._cprev(angles, i, '<CUT') if angle[1]=='CUT>' else None)
for i, angle in enumerate(angles):
if angle[1] == 'SEG':
angle.append([self._pos(angle[0], radius)])
for i, angle in enumerate(angles):
if angle[1] != 'SEG':
mult = -1 if angle[1] == '<CUT' else 1
a = angle[0] - mult*c
a2 = a + mult*pi/2
# Line from previous to next segment point.
line1 = Line(angles[angle[2]][6][0], angles[angle[3]][6][0])
# Line from origin offset by thickness.
p1 = self._pos(a2, self.thickness/2)
p2 = p1 + self._pos(a, radius)
line2 = Line(p1, p2)
pintersect = line1.intersection(line2)
pinset = pintersect + self._pos(a, -cutdepth)
if angle[1] == '<CUT':
angle.append([pintersect, pinset])
else:
angle.append([pinset, pintersect])
d1 = pinset.distance(Point(0.0,0.0))
d2 = angles[angle[5]][6][1].distance(Point(0.0,0.0))
if d1<d2:
angles[angle[5]][6][1] = pinset - self._pos(a2, self.thickness)
elif d2<d1:
angle[6][0] = angles[angle[5]][6][1] + self._pos(a2, self.thickness)
pass
incut = False
for i, angle in enumerate(angles):
atype = angle[1]
if atype=='<CUT':
incut = True
elif atype=='CUT>':
incut = False
if atype != 'SEG' or (atype == 'SEG' and not incut):
for pos in angle[6]:
x = origin.x + pos.x
y = origin.y + pos.y
if len(self.x)==0 or not (isclose(x, self.x[-1]) and isclose(y, self.y[-1])):
self.x.append(x)
self.y.append(y)
return
def _cnext(self, angles, i, item):
if i>=len(angles):
i=-1
for j, angle in enumerate(angles[i+1:]):
if angle[1] == item:
return i+1+j
for j, angle in enumerate(angles):
if angle[1] == item:
return j
return None
def _cprev(self, angles, i, item):
if i<=0:
i=len(angles)
for j, angle in enumerate(angles[:i][::-1]):
if angle[1] == item:
return i-j-1
for j, angle in enumerate(angles[::-1]):
if angle[1] == item:
return j
return None
def _pos(self, angle, radius):
return Point(sin(angle)*radius, cos(angle)*radius)

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Michael Matthews
#
# This file is part of CutCraft.
#
# CutCraft is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CutCraft is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CutCraft. If not, see <http://www.gnu.org/licenses/>.
from math import floor
class FingerJoint(object):
def __init__(self, length, fingerwidth, style, thickness=0.0):
super(FingerJoint, self).__init__()
self.thickness = thickness
if style in ('depth','height'):
self.fingers = [0.0] + \
[pos + fingerwidth*2.0 for pos in self._fingers(length-fingerwidth*4.0, fingerwidth)] + \
[length]
elif style=='width':
self.fingers = [pos + thickness for pos in self._fingers(length-thickness*2.0, fingerwidth)]
else:
raise ValueError("cutcraft.core.fingerjoin: invalid value of '{}' for parameter 'style'.".format(style))
return
def _fingers(self, length, fingerwidth):
count = int(floor(length / fingerwidth))
count = count-1 if count%2==0 else count
return [length/count*c for c in range(count+1)]

View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Michael Matthews
#
# This file is part of CutCraft.
#
# CutCraft is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CutCraft is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CutCraft. If not, see <http://www.gnu.org/licenses/>.
from .point import Point
from math import sqrt
def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
# Required as Inkscape does not include math.isclose().
return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
class Line(object):
""" Line class defined by start and end Points. """
def __init__(self, point1, point2):
self.pts = (point1, point2)
self.x = [point1.x, point2.x]
self.y = [point1.y, point2.y]
return
def _line(self):
# Convert line segment into a line equation (infinite length).
p1 = self.pts[0]
p2 = self.pts[1]
A = (p1.y - p2.y)
B = (p2.x - p1.x)
C = (p1.x*p2.y - p2.x*p1.y)
return A, B, -C
def intersection(self, other):
# Find the intersection of the lines (infinite length - not segments)
L1 = self._line()
L2 = other._line()
D = L1[0] * L2[1] - L1[1] * L2[0]
Dx = L1[2] * L2[1] - L1[1] * L2[2]
Dy = L1[0] * L2[2] - L1[2] * L2[0]
if D != 0:
x = Dx / D
y = Dy / D
return Point(x, y)
else:
return None
def normal(self):
# Return the unit normal
dx = self.x[1] - self.x[0]
dy = self.y[1] - self.y[0]
d = sqrt(dx*dx + dy*dy)
return dx/d, -dy/d
def addkerf(self, kerf):
nx, ny = self.normal()
offset = Point(ny*kerf, nx*kerf)
self.pts = (self.pts[0] + offset, self.pts[1] + offset)
self.x = [self.pts[0].x, self.pts[1].x]
self.y = [self.pts[0].y, self.pts[1].y]
def __eq__(self, other):
return (self.pts == other.pts)
def __ne__(self, other):
return (self.pts != other.pts)
def __repr__(self):
return "Line(" + repr(self.pts[0]) + ", " + repr(self.pts[1]) + ")"
def __str__(self):
return "(" + str(self.pts[0]) + ", " + str(self.pts[1]) + ")"

View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Michael Matthews
#
# This file is part of CutCraft.
#
# CutCraft is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CutCraft is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CutCraft. If not, see <http://www.gnu.org/licenses/>.
from .point import Point
from .trace import Trace
from .part import Part
from math import pi, sin, cos, sqrt
class NeoPixel(Part):
rings = [[1, 0.0], [6, 16.0/2.0], [12, 30.0/2.0]]
size = 5.5
""" Line class defined by start and end Points. """
def __init__(self, style='rings', origin=Point(0.0, 0.0), scale=1.0, rotate=0.0):
super(NeoPixel, self).__init__()
self.scale = scale
if style=='rings':
for ring in self.rings:
pixels = ring[0]
radius = ring[1] * self.scale
for pixel in range(pixels):
a = rotate + pi*2 * pixel / pixels
seg = self._pixel(origin + Point(sin(a) * radius, cos(a) * radius),
pi/4 + a)
self += seg
elif style=='strip':
xo = origin.x
yo = origin.y
xsize = 25.4*2.0*self.scale
size = self.size*self.scale
seg = Trace() + \
Point(xo-xsize/2.0, yo+size/2.0) + \
Point(xo-xsize/2.0, yo-size/2.0) + \
Point(xo+xsize/2.0, yo-size/2.0) + \
Point(xo+xsize/2.0, yo+size/2.0)
seg.close()
self += seg
return
def _pixel(self, position, rotation):
seg = Trace()
xo = position.x
yo = position.y
size = sqrt(2.0*(self.size*self.scale)**2)
for corner in range(4):
# Points added in counterclockwise direction as this is an inner cut.
a = rotation-2.0*pi*corner/4.0
seg += Point(xo + sin(a) * size/2.0, yo + cos(a) * size/2.0)
seg.close()
return seg

View File

@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Michael Matthews
#
# This file is part of CutCraft.
#
# CutCraft is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CutCraft is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CutCraft. If not, see <http://www.gnu.org/licenses/>.
from copy import deepcopy
from .point import Point
from .rectangle import Rectangle
from .trace import Trace
class Part(object):
""" List of traces that make up a part. """
def __init__(self):
self.traces = []
return
def close(self):
""" Close each traces back to their start. """
for trace in self.traces:
trace.close()
return
def applykerf(self, kerf):
""" Apply an offset to allow for the kerf when cutting. """
for trace in self.traces:
trace.applykerf(kerf)
return
def svg(self):
# Generate SVG string for this part.
return " ".join([trace.svg() for trace in self.traces])
def bbox(self):
bboxes = [trace.bbox() for trace in self.traces]
x = [p1.x for p1, p2 in bboxes] + [p2.x for p1, p2 in bboxes]
y = [p1.y for p1, p2 in bboxes] + [p2.y for p1, p2 in bboxes]
return Rectangle(Point(min(x), min(y)), Point(max(x), max(y)))
def area(self):
bbox = self.bbox()
return bbox.area()
def size(self):
bbox = self.bbox()
return bbox.size()
def __add__(self, other):
p = Part()
if isinstance(other, Part):
p.traces = self.traces + deepcopy(other.traces)
elif isinstance(other, Trace):
p.traces = deepcopy(self.traces)
p.traces.append(other)
elif isinstance(other, Point):
p.traces = self.traces
for trace in p.traces:
trace.offset(other)
else:
raise RuntimeError("Can only add a Part, Trace or Point to an existing Part.")
return p
def __iadd__(self, other):
if isinstance(other, Part):
self.traces.extend(other.traces)
elif isinstance(other, Trace):
self.traces.append(deepcopy(other))
elif isinstance(other, Point):
for trace in self.traces:
trace.offset(other)
else:
raise RuntimeError("Can only add a Part, Trace or Point to an existing Part.")
return self
def __repr__(self):
return "Part" + str(self)
def __str__(self):
l = len(self.traces)
return "(" + str(l) + " trace" + ("s" if l>1 else "") + ")"

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Michael Matthews
#
# This file is part of CutCraft.
#
# CutCraft is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CutCraft is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CutCraft. If not, see <http://www.gnu.org/licenses/>.
from math import sqrt
class Point(object):
""" Point (x,y) class suppporting addition for offsets. """
def __init__(self, x, y):
self.x = x
self.y = y
def distance(self, other):
""" Distance between two points. """
x = self.x - other.x
y = self.y - other.y
return sqrt(x*x+y*y)
def tup(self):
return (self.x, self.y)
def __eq__(self, other):
return (self.x == other.x and self.y == other.y)
def __ne__(self, other):
return (self.x != other.x or self.y != other.y)
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iadd__(self, other):
self.x += other.x
self.y += other.y
return self
def __sub__(self, other):
return Point(self.x - other.x, self.y - other.y)
def __isub__(self, other):
self.x -= other.x
self.y -= other.y
return self
def __neg__(self):
return Point(-self.x, -self.y)
def __repr__(self):
return "Point(" + str(self.x) + ", " + str(self.y) + ")"
def __str__(self):
return "(" + str(self.x) + ", " + str(self.y) + ")"

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Michael Matthews
#
# This file is part of CutCraft.
#
# CutCraft is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CutCraft is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CutCraft. If not, see <http://www.gnu.org/licenses/>.
from math import ceil, floor
from .point import Point
class Rectangle(object):
""" Rectangle class defined by top-left and bottom-right Points. """
def __init__(self, point1, point2):
# Correct the input points in case they are not topleft, bottomright as expected.
self.topleft = Point(min(point1.x, point2.x), min(point1.y, point2.y))
self.bottomright = Point(max(point1.x, point2.x), max(point1.y, point2.y))
return
def size(self):
# Calculate the size as: width, height.
return self.bottomright.x-self.topleft.x, self.bottomright.y-self.topleft.y
def area(self):
width, height = self.size()
return width*height
def expanded(self):
# Expand the current Rectangle out to integer boundary.
return Rectangle(Point(floor(self.topleft.x), floor(self.topleft.y)),
Point(ceil(self.bottomright.x), ceil(self.bottomright.y)))
def svg(self):
# Generate SVG string for this rectangle.
ptx = [self.topleft.x, self.bottomright.x, self.bottomright.x, self.topleft.x]
pty = [self.topleft.y, self.topleft.y, self.bottomright.y, self.bottomright.y]
return "M {} {} ".format(ptx[0], pty[0]) + \
" ".join(["L {} {}".format(x, y) for x, y in zip(ptx[1:], pty[1:])]) + \
" L {} {}".format(ptx[0], pty[0])
def __eq__(self, other):
return (self.topleft == other.topleft and self.bottomright == other.bottomright )
def __ne__(self, other):
return (self.topleft != other.topleft or self.bottomright != other.bottomright)
def __repr__(self):
return "Rectangle(" + repr(self.topleft) + ", " + repr(self.bottomright) + ")"
def __str__(self):
return "(" + str(self.topleft) + ", " + str(self.bottomright) + ")"

View File

@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Michael Matthews
#
# This file is part of CutCraft.
#
# CutCraft is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CutCraft is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CutCraft. If not, see <http://www.gnu.org/licenses/>.
from .point import Point
from .line import Line
from ..util import isclose, iscloselist
class Trace(object):
""" List of coordinates that make a boundary. """
def __init__(self, x=None, y=None):
self.x = [] if x is None else x
self.y = [] if y is None else y
self.closed = False
return
def close(self):
""" Close the trace back to the start. """
if not self.closed:
if isclose(self.x[0], self.x[-1]) and isclose(self.y[0], self.y[-1]):
# Start and end should be the same.
self.x[-1] = self.x[0]
self.y[-1] = self.y[0]
else:
# Add new end point to close the loop.
self.x.append(self.x[0])
self.y.append(self.y[0])
self.closed = True
return
def applykerf(self, kerf):
""" Apply an offset to allow for the kerf when cutting. """
self.close()
# Convert the points to lines.
lines = [Line(Point(x1, y1), Point(x2, y2)) for x1, y1, x2, y2 in
zip(self.x[:-1], self.y[:-1], self.x[1:], self.y[1:])]
# Add the kerf to the lines.
for line in lines:
line.addkerf(kerf)
# Extract the line intersections as the new points.
pts = [line1.intersection(line2) for line1, line2 in zip(lines, lines[-1:] + lines[:-1])]
self.clear()
self.x += [pt.x for pt in pts]
self.y += [pt.y for pt in pts]
self.x += self.x[:1]
self.y += self.y[:1]
return
def offset(self, pt):
""" Move a trace by an x/y offset. """
self.x = [x + pt.x for x in self.x]
self.y = [y + pt.y for y in self.y]
def clear(self):
self.x = []
self.y = []
def svg(self):
# Generate SVG string for this trace.
if len(self.x)<2:
return ""
return "M {} {} ".format(self.x[0], self.y[0]) + \
" ".join(["L {} {}".format(x, y) for x, y in zip(self.x[1:], self.y[1:])])
def bbox(self):
return Point(min(self.x), min(self.y)), Point(max(self.x), max(self.y))
def __len__(self):
return len(self.x)
def __eq__(self, other):
return (iscloselist(self.x,other.x) and iscloselist(self.y, other.y))
def __ne__(self, other):
return (not iscloselist(self.x, other.x) or not iscloselist(self.y, other.y))
def __add__(self, other):
new = Trace()
if isinstance(other, Point):
new.x = self.x + [other.x]
new.y = self.y + [other.y]
elif isinstance(other, Trace):
new.x = self.x + other.x
new.y = self.y + other.y
else:
raise RuntimeError("Can only add a Trace or Point to an existing Trace.")
return new
def __iadd__(self, other):
if isinstance(other, Point):
self.x.append(other.x)
self.y.append(other.y)
elif isinstance(other, Trace):
self.x += other.x
self.y += other.y
else:
raise RuntimeError("Can only add a Trace or Point to an existing Trace.")
return self
def __repr__(self):
return "Trace(" + str(self.x) + ", " + str(self.y) + ")"
def __str__(self):
return "(" + str(self.x) + ", " + str(self.y) + ")"
def __getitem__(self, key):
""" Used to override the slice functionality (eg: reversing). """
new = Trace()
new.x = self.x[key]
new.y = self.y[key]
return new
def __setitem__(self, key, value):
""" Used to override the slice functionality. """
if isinstance(value, Point):
self.x[key] = value.x
self.y[key] = value.y
else:
raise RuntimeError("Can only update a single item in an existing Trace.")
return self
def __delitem__(self, key):
""" Used to override the slice functionality (eg: reversing). """
del self.x[key]
del self.y[key]
return self
def __reversed__(self):
""" Used to override the slice functionality (eg: reversing). """
new = Trace()
new.x = list(reversed(self.x))
new.y = list(reversed(self.y))
return new