diff --git a/extensions/cutcraft/README.md b/extensions/cutcraft/README.md
new file mode 100644
index 00000000..a05ed7e9
--- /dev/null
+++ b/extensions/cutcraft/README.md
@@ -0,0 +1,57 @@
+# cut-craft
+
+
+## Python package
+
+Note that this package is written for Python 3, however requires Python 2 compatibility for Inkscape integration.
+
+The `cutcraft` package contains components in the following categories:
+
+| Folder | Description |
+| --------- | ---------------------------------------------------- |
+| core | Core components (point, line etc). |
+| platforms | Platforms used to construct shapes (circular etc). |
+| shapes | Fundamental 3D shapes (cylinder, cone, sphere etc). |
+| supports | Vertical supports to hold the shape levels apart. |
+
+
+## Core
+
+| Module | Description |
+| --------- | --------------------------------------------------------------------------- |
+| point | A 2D point with `x` and `y` coordinates. |
+| rectangle | Two `point`s defining topleft and bottom right for a rectangle. |
+| trace | An ordered collection of `point`s. |
+| part | A collection of one or more `trace`s. |
+| line | A type of `trace` with two `point`s defining the start and end of the line. |
+| circle | A type of `trace` with `point`s defining a circle. |
+| neopixel | A type of `trace` with the `point`s defining a cutout suitable to fit a variety of [NeoPixels](https://www.adafruit.com/category/168). |
+
+
+## Shapes
+
+| Module | Description |
+| -------- | -------------------------------------- |
+| shape | The core 3D functionality for a shape. |
+| cone | A cone `shape`. |
+| cylinder | A cylinder `shape`. |
+| sphere | A 3D spherical `shape`. |
+
+> Note that the fundamental `shape`s listed above can be used flexibly considering the number of `circle` segments can be specified. For example a `cone` with 4 segments becomes a **pyramid**, and a `cylinder` with 4 segments becomes a **cube**.
+
+
+## Supports
+
+| Module | Description |
+| -------- | --------------------------------------------------- |
+| support | The core support structure functionality. |
+| pier | A pier like `support` to hold `shape` levels apart. |
+| face | A solid face to `support` `shape` levels. |
+
+
+## Python 2 vs 3 Compatibility
+
+The initial aim was to develop only for Python 3, however [Inkscape](https://inkscape.org) currently uses Python 2 as the default interpreter for extensions. As a result, the following should be noted while reviewing the code:
+
+1) The calls to `super()` are written in a way that works with both versions of Python.
+2) The `math.isclose()` function is not available in Python 2 so a local version has been created in [util.py](util.py).
diff --git a/extensions/cutcraft/__init__.py b/extensions/cutcraft/__init__.py
new file mode 100644
index 00000000..a181bdac
--- /dev/null
+++ b/extensions/cutcraft/__init__.py
@@ -0,0 +1,18 @@
+# -*- 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 .
+
diff --git a/extensions/cutcraft/core/__init__.py b/extensions/cutcraft/core/__init__.py
new file mode 100644
index 00000000..ad1ad755
--- /dev/null
+++ b/extensions/cutcraft/core/__init__.py
@@ -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 .
+
+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"]
diff --git a/extensions/cutcraft/core/circle.py b/extensions/cutcraft/core/circle.py
new file mode 100644
index 00000000..58a78340
--- /dev/null
+++ b/extensions/cutcraft/core/circle.py
@@ -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 .
+
+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, ''] for cut in range(1, cuts+1)]
+ else:
+ angles = [[rotation+end/segments*seg, 'SEG'] for seg in range(segments)] + \
+ [[rotation+end/cuts*cut-c, ''] 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]=='' 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] == '':
+ 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)
diff --git a/extensions/cutcraft/core/fingerjoint.py b/extensions/cutcraft/core/fingerjoint.py
new file mode 100644
index 00000000..e481782a
--- /dev/null
+++ b/extensions/cutcraft/core/fingerjoint.py
@@ -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 .
+
+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)]
diff --git a/extensions/cutcraft/core/line.py b/extensions/cutcraft/core/line.py
new file mode 100644
index 00000000..ff2e7b94
--- /dev/null
+++ b/extensions/cutcraft/core/line.py
@@ -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 .
+
+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]) + ")"
diff --git a/extensions/cutcraft/core/neopixel.py b/extensions/cutcraft/core/neopixel.py
new file mode 100644
index 00000000..dde38700
--- /dev/null
+++ b/extensions/cutcraft/core/neopixel.py
@@ -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 .
+
+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
diff --git a/extensions/cutcraft/core/part.py b/extensions/cutcraft/core/part.py
new file mode 100644
index 00000000..8fc147bf
--- /dev/null
+++ b/extensions/cutcraft/core/part.py
@@ -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 .
+
+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 "") + ")"
diff --git a/extensions/cutcraft/core/point.py b/extensions/cutcraft/core/point.py
new file mode 100644
index 00000000..5fc91cf5
--- /dev/null
+++ b/extensions/cutcraft/core/point.py
@@ -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 .
+
+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) + ")"
diff --git a/extensions/cutcraft/core/rectangle.py b/extensions/cutcraft/core/rectangle.py
new file mode 100644
index 00000000..298c501a
--- /dev/null
+++ b/extensions/cutcraft/core/rectangle.py
@@ -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 .
+
+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) + ")"
diff --git a/extensions/cutcraft/core/trace.py b/extensions/cutcraft/core/trace.py
new file mode 100644
index 00000000..901347ea
--- /dev/null
+++ b/extensions/cutcraft/core/trace.py
@@ -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 .
+
+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
diff --git a/extensions/cutcraft/platforms/__init__.py b/extensions/cutcraft/platforms/__init__.py
new file mode 100644
index 00000000..9984f16c
--- /dev/null
+++ b/extensions/cutcraft/platforms/__init__.py
@@ -0,0 +1,23 @@
+# -*- 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 .
+
+from .platform import Platform
+from .circular import Circular
+from .rollerframe import RollerFrame
+
+__all__ = ["Platform", "Circular", "RollerFrame"]
diff --git a/extensions/cutcraft/platforms/circular.py b/extensions/cutcraft/platforms/circular.py
new file mode 100644
index 00000000..558e055a
--- /dev/null
+++ b/extensions/cutcraft/platforms/circular.py
@@ -0,0 +1,40 @@
+# -*- 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 .
+
+from ..core.point import Point
+from ..core.part import Part
+from ..core.circle import Circle
+from .platform import Platform
+from math import pi
+
+class Circular(Platform):
+ """ Circular Platform. """
+ def __init__(self, radius, inradius, segments, cuts, cutdepth, start=0.0, end=pi*2, rotation=0.0,
+ origin=Point(0.0, 0.0), thickness=0.0):
+ super(Circular, self).__init__(thickness)
+ self.radius = radius
+ self.inradius = inradius
+ self.segments = segments
+ outer = Circle(self.radius, segments, cuts, cutdepth=cutdepth, start=start, end=end,
+ rotation=rotation, origin=origin, thickness=thickness)
+ outer.close()
+ inner = Circle(self.inradius, segments, 0, start=start, end=end,
+ rotation=rotation, origin=origin, thickness=thickness)
+ inner.close()
+ self.traces.append(outer)
+ self.traces.append(reversed(inner))
diff --git a/extensions/cutcraft/platforms/platform.py b/extensions/cutcraft/platforms/platform.py
new file mode 100644
index 00000000..65a1d243
--- /dev/null
+++ b/extensions/cutcraft/platforms/platform.py
@@ -0,0 +1,25 @@
+# -*- 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 .
+
+from ..core.part import Part
+
+class Platform(Part):
+ def __init__(self, thickness):
+ super(Platform, self).__init__()
+ self.thickness = thickness
+ return
diff --git a/extensions/cutcraft/platforms/rollerframe.py b/extensions/cutcraft/platforms/rollerframe.py
new file mode 100644
index 00000000..753aec8f
--- /dev/null
+++ b/extensions/cutcraft/platforms/rollerframe.py
@@ -0,0 +1,379 @@
+# -*- 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 .
+
+from ..core.point import Point
+from ..core.part import Part
+from ..core.trace import Trace
+from ..core.circle import Circle
+from ..core.fingerjoint import FingerJoint
+from ..core.neopixel import NeoPixel
+from .platform import Platform
+from ..util import intersection
+from math import pi, sqrt, asin, atan
+
+#import inkex
+
+class RollerFrame(Platform):
+ """ RollerBot Platform. """
+ def __init__(self, supwidth, wheelradius, upperradius, lowerradius,
+ facesize, barsize, primarygapwidth, secondarygapwidth,
+ scale, part_id, thickness=0.0):
+ super(RollerFrame, self).__init__(thickness)
+ self.supwidth = supwidth
+ self.barsize = barsize
+
+ cutdepth = supwidth / 3.0
+ barradius = sqrt(2.0*(barsize/2.0)**2)
+
+ facewidth = primarygapwidth*2.0 + thickness*3.0
+ faceheight = facesize + thickness
+ fjoint = FingerJoint(faceheight, thickness*2.0, 'height', thickness=thickness) # Face
+ bjoint = FingerJoint(faceheight, thickness*2.0, 'depth', thickness=thickness) # Base
+ wjoint = FingerJoint(facewidth, thickness*2.0, 'width', thickness=thickness) # Length
+
+ if part_id<5:
+ # The circular segments for the main body structure.
+
+ # Outer section.
+ x = barsize/2.0
+ y = sqrt(lowerradius**2 - x**2)
+ a = atan(x/y)
+ outer = Circle(upperradius, 72, 5, cutdepth=cutdepth, start=0.0, end=pi, thickness=thickness) + \
+ Point(0.0, -upperradius + cutdepth) + \
+ Point(-thickness, -upperradius + cutdepth) + \
+ Point(-thickness, -upperradius) + \
+ Point(-barsize/2.0, -upperradius) + \
+ Circle(lowerradius, 72, 4, cutdepth=cutdepth, start=pi+a, end=pi*2-a, thickness=thickness) + \
+ Point(-barsize/2.0, upperradius) + \
+ Point(-thickness, upperradius) + \
+ Point(-thickness, upperradius - cutdepth) + \
+ Point(0.0, upperradius - cutdepth)
+ outer.close()
+ self.traces.append(outer)
+
+ if part_id in (0,4):
+ # Central Motor Position.
+ inner = Trace() + \
+ Point(-barsize/2.0, -barsize/2.0) + \
+ Point(-barsize/2.0, barsize/2.0) + \
+ Point(-barsize/6.0, barsize/2.0) + \
+ Point(-barsize/6.0, barsize/2.0-thickness/2.0) + \
+ Point(barsize/6.0, barsize/2.0-thickness/2.0) + \
+ Point(barsize/6.0, barsize/2.0) + \
+ Point(barsize/2.0, barsize/2.0) + \
+ Point(barsize/2.0, -barsize/2.0) + \
+ Point(barsize/6.0, -barsize/2.0) + \
+ Point(barsize/6.0, -barsize/2.0+thickness/2.0) + \
+ Point(-barsize/6.0, -barsize/2.0+thickness/2.0) + \
+ Point(-barsize/6.0, -barsize/2.0)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ # Outer parts are complete, inner parts have cutouts.
+ if part_id in (1,2,3):
+ # Central Motor Position and Bar.
+ inner = Trace() + \
+ Point(-barsize/2.0*1.3, -barsize/2.0) + \
+ Point(-barsize/2.0*1.3, -barsize/2.0*0.55) + \
+ Point(-barsize/2.0, -barsize/2.0*0.55) + \
+ Point(-barsize/2.0, barsize/2.0*0.55) + \
+ Point(-barsize/2.0*1.3, barsize/2.0*0.55) + \
+ Point(-barsize/2.0*1.3, barsize/2.0) + \
+ Point(barsize/2.0, barsize/2.0) + \
+ Point(barsize/2.0, barsize/10.0) + \
+ Point(barsize/2.0*1.2, barsize/20.0) + \
+ Point(barsize/2.0*1.2, -barsize/20.0) + \
+ Point(barsize/2.0, -barsize/10.0) + \
+ Point(barsize/2.0, -barsize/2.0)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ # Upper segment cut-outs.
+ x = supwidth/2.0
+ y = sqrt((upperradius-supwidth)**2 - x**2)
+ a_outer = atan(x/y)
+ y = sqrt((barradius+supwidth)**2 - x**2)
+ a_inner = atan(x/y)
+
+ inner = self._segment(upperradius-supwidth, barradius+supwidth,
+ 0, 1, cutdepth, 0.0, a_outer, a_inner)
+ self.traces.append(reversed(inner))
+
+ fa = (pi/2.0 - self._faceangle(facesize, upperradius)) / 2.0
+ (fx, fy) = intersection(upperradius, angle=fa)
+ if 0:
+ inner = Trace() + \
+ Point(fx, -fy) + \
+ Point(fy, -fy) + \
+ Point(fy, -fx)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ oy = fy-thickness*2.0
+ (ox, oa) = intersection(upperradius, y=oy)
+ if 0:
+ inner = Trace() + \
+ Point(ox, -oy) + \
+ Point(oy, -oy) + \
+ Point(oy, -ox)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ iy = oy
+ (ix, ia) = intersection(upperradius-supwidth, y=iy)
+
+ if part_id==2:
+ inner = Circle(upperradius-supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/3+a_outer, end=pi-a_outer,
+ thickness=self.thickness) + \
+ reversed(Circle(barradius+supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/3+a_inner, end=pi-a_inner,
+ thickness=self.thickness))
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ # Temporary cut to remove where the face will be installed.
+ oy = fy-thickness
+ (ox, oa) = intersection(upperradius, y=oy)
+ inner = Trace() + \
+ Point(ox, -ox) + \
+ Point(oy, -ox) + \
+ Point(oy, -oy) + \
+ Point(ox, -oy)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ else:
+ inner = Circle(upperradius-supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/3*1+a_outer, end=pi/2+ia,
+ thickness=self.thickness) + \
+ reversed(Circle(barradius+supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/3*1+a_inner, end=pi/3*2-a_inner,
+ thickness=self.thickness))
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ ia = pi/2 - ia
+ (ix, iy) = intersection(upperradius-supwidth, angle=ia)
+
+ inner = Circle(upperradius-supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/2+ia, end=pi/3*3-a_outer,
+ thickness=self.thickness) + \
+ reversed(Circle(barradius+supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/3*2+a_inner, end=pi/3*3-a_inner,
+ thickness=self.thickness)) + \
+ Point(ix, -ix)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ # Face and base cutout slots.
+ cy = fy-thickness
+ for (x1, x2) in zip(fjoint.fingers[1::2],fjoint.fingers[2::2]):
+ inner = Trace() + \
+ Point(cy+x1, -cy) + \
+ Point(cy+x2, -cy) + \
+ Point(cy+x2, -cy-thickness) + \
+ Point(cy+x1, -cy-thickness)
+ inner.close()
+ self.traces.append(reversed(inner))
+ for (y1, y2) in zip(bjoint.fingers[1::2],bjoint.fingers[2::2]):
+ inner = Trace() + \
+ Point(cy, -cy-y2) + \
+ Point(cy, -cy-y1) + \
+ Point(cy+thickness, -cy-y1) + \
+ Point(cy+thickness, -cy-y2)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ if 0:
+ if part_id==2:
+ for seg in range(2):
+ segnext = seg*2+1
+ inner = self._segment(upperradius-supwidth, barradius+supwidth,
+ seg, segnext, cutdepth, 0.0, a_outer, a_inner)
+ self.traces.append(reversed(inner))
+ else:
+ for seg in range(3):
+ segnext = seg+1
+ inner = self._segment(upperradius-supwidth, barradius+supwidth,
+ seg, segnext, cutdepth, 0.0, a_outer, a_inner)
+ self.traces.append(reversed(inner))
+
+ # Lower segment cut-outs.
+ x = supwidth/2.0
+ y = sqrt((lowerradius-supwidth)**2 - x**2)
+ a_outer = atan(x/y)
+ y = sqrt((barradius+supwidth)**2 - x**2)
+ a_inner = atan(x/y)
+
+ for seg in range(3):
+ segnext = seg+1
+ inner = self._segment(lowerradius-supwidth, barradius+supwidth,
+ seg, segnext, cutdepth, pi, a_outer, a_inner)
+ self.traces.append(reversed(inner))
+
+ if part_id in (1,2,3):
+ r_mid = barradius+supwidth + ((upperradius-supwidth) - (barradius+supwidth))/2.0
+ self._slot(barsize/2.0 + supwidth/2.0, cutdepth*1.5, cutdepth)
+ self._slot(barsize/2.0 + supwidth/2.0, -cutdepth*1.5, cutdepth)
+ self._slot(barsize/2.0 + supwidth/2.0, r_mid+cutdepth*1.5, cutdepth)
+ self._slot(barsize/2.0 + supwidth/2.0, r_mid-cutdepth*1.5, cutdepth)
+
+ elif part_id in (5,6):
+ # The board supports.
+ x = primarygapwidth
+ y = ((upperradius-supwidth) + (barradius+supwidth))/2.0 + supwidth*2.0
+ t = Trace() + \
+ Point(0.0, 0.0) + \
+ Point(0.0, supwidth-cutdepth*2.0) + \
+ Point(-cutdepth, supwidth-cutdepth*2.0) + \
+ Point(-cutdepth, supwidth-cutdepth*1.0) + \
+ Point(0.0, supwidth-cutdepth*1.0) + \
+ Point(0.0, supwidth*2.0-cutdepth*2.0) + \
+ Point(-cutdepth, supwidth*2.0-cutdepth*2.0) + \
+ Point(-cutdepth, supwidth*2.0-cutdepth*1.0) + \
+ Point(0.0, supwidth*2.0-cutdepth*1.0) + \
+ Point(0.0, y-supwidth*2.0+cutdepth*1.0) + \
+ Point(-cutdepth, y-supwidth*2.0+cutdepth*1.0) + \
+ Point(-cutdepth, y-supwidth*2.0+cutdepth*2.0) + \
+ Point(0.0, y-supwidth*2.0+cutdepth*2.0) + \
+ Point(0.0, y-supwidth+cutdepth*1.0) + \
+ Point(-cutdepth, y-supwidth+cutdepth*1.0) + \
+ Point(-cutdepth, y-supwidth+cutdepth*2.0) + \
+ Point(0.0, y-supwidth+cutdepth*2.0) + \
+ Point(0.0, y) + \
+ Point(x, y) + \
+ Point(x, y-supwidth-cutdepth*1.0) + \
+ Point(x+cutdepth, y-supwidth-cutdepth*1.0) + \
+ Point(x+cutdepth, y-supwidth-cutdepth*2.0) + \
+ Point(x, y-supwidth-cutdepth*2.0) + \
+ Point(x, supwidth-cutdepth*1.0) + \
+ Point(x+cutdepth, supwidth-cutdepth*1.0) + \
+ Point(x+cutdepth, supwidth-cutdepth*2.0) + \
+ Point(x, supwidth-cutdepth*2.0) + \
+ Point(x, 0.0)
+ t.close()
+ self.traces.append(t)
+
+ elif part_id==7:
+ # The face components.
+ t = Trace(x=[thickness], y=[thickness])
+ for i, pos in enumerate(fjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(pos, thickness)
+ t += Point(pos, 0.0)
+ else:
+ t += Point(pos, 0.0)
+ t += Point(pos, thickness)
+ t += Point(faceheight, thickness)
+ t += Point(faceheight, facewidth-thickness)
+ for i, pos in enumerate(reversed(fjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(pos, facewidth-thickness)
+ t += Point(pos, facewidth)
+ else:
+ t += Point(pos, facewidth)
+ t += Point(pos, facewidth-thickness)
+ t += Point(thickness, facewidth-thickness)
+ for i, pos in enumerate(reversed(wjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(thickness, pos)
+ t += Point(0.0, pos)
+ else:
+ t += Point(0.0, pos)
+ t += Point(thickness, pos)
+ t.close()
+ self.traces.append(t)
+
+ elif part_id==8:
+ t = Trace(x=[thickness], y=[0.0])
+ t += Point(facewidth-thickness, 0.0)
+ for i, pos in enumerate(bjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(facewidth-thickness, pos)
+ t += Point(facewidth, pos)
+ else:
+ t += Point(facewidth, pos)
+ t += Point(facewidth-thickness, pos)
+ t += Point(facewidth-thickness, faceheight)
+ for i, pos in enumerate(reversed(wjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(pos, faceheight)
+ t += Point(pos, faceheight-thickness)
+ else:
+ t += Point(pos, faceheight-thickness)
+ t += Point(pos, faceheight)
+ t += Point(thickness, faceheight)
+ for i, pos in enumerate(reversed(bjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(thickness, pos)
+ t += Point(0.0, pos)
+ else:
+ t += Point(0.0, pos)
+ t += Point(thickness, pos)
+ t.close()
+ self.traces.append(t)
+
+ for eye in range(2):
+ np = NeoPixel(style='rings', origin=Point(facewidth/4.0*(1+eye*2), faceheight/2.5), scale=scale, rotate=pi/2)
+ self.traces.extend(np.traces)
+ np = NeoPixel(style='strip', origin=Point(facewidth/2.0, faceheight*0.80), scale=scale)
+ self.traces.extend(np.traces)
+
+ # Camera
+ csize = 8.5*scale
+ t = Trace() + \
+ Point(facewidth/2.0 - csize/2.0, faceheight/2.5 - csize/2.0) + \
+ Point(facewidth/2.0 + csize/2.0, faceheight/2.5 - csize/2.0) + \
+ Point(facewidth/2.0 + csize/2.0, faceheight/2.5 + csize/2.0) + \
+ Point(facewidth/2.0 - csize/2.0, faceheight/2.5 + csize/2.0)
+ t.close()
+ self.traces.append(t)
+
+ def _faceangle(self, size, radius):
+ # Returns total angle required for a face.
+ o = sqrt(2.0*(size**2))*0.5
+ h = radius
+ return 2.0*asin(o/h)
+
+ def _segment(self, r_outer, r_inner, seg, segnext, cutdepth, a_offset, a_outer, a_inner):
+ # Create an inner segment cutout.
+ t = Circle(r_outer, 18, 0, cutdepth=cutdepth,
+ start=a_offset+pi/3*seg+a_outer, end=a_offset+pi/3*segnext-a_outer,
+ thickness=self.thickness) + \
+ reversed(Circle(r_inner, 18, 0, cutdepth=cutdepth,
+ start=a_offset+pi/3*seg+a_inner, end=a_offset+pi/3*segnext-a_inner,
+ thickness=self.thickness))
+ if a_offset == 0.0 and seg == 0:
+ r_mid = r_inner + (r_outer - r_inner)/2.0
+ t += Trace() + \
+ Point(self.supwidth / 2.0, r_mid - self.supwidth) + \
+ Point(self.barsize/2.0 + self.supwidth, r_mid - self.supwidth) + \
+ Point(self.barsize/2.0 + self.supwidth, r_mid + self.supwidth) + \
+ Point(self.supwidth / 2.0, r_mid + self.supwidth)
+ t.close()
+ return t
+
+ def _slot(self, x, y, cutdepth):
+ slot = Trace() + \
+ Point(x-self.thickness/2.0, y+cutdepth/2.0) + \
+ Point(x+self.thickness/2.0, y+cutdepth/2.0) + \
+ Point(x+self.thickness/2.0, y-cutdepth/2.0) + \
+ Point(x-self.thickness/2.0, y-cutdepth/2.0)
+ slot.close()
+ self.traces.append(reversed(slot))
diff --git a/extensions/cutcraft/shapes/__init__.py b/extensions/cutcraft/shapes/__init__.py
new file mode 100644
index 00000000..210f8b69
--- /dev/null
+++ b/extensions/cutcraft/shapes/__init__.py
@@ -0,0 +1,26 @@
+# -*- 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 .
+
+from .shape import Shape
+from .box import Box
+from .cone import Cone
+from .cylinder import Cylinder
+from .sphere import Sphere
+from .rollerbot import RollerBot
+
+__all__ = ["Shape", "Box", "Cone", "Cylinder", "Sphere", "RollerBot"]
diff --git a/extensions/cutcraft/shapes/box.py b/extensions/cutcraft/shapes/box.py
new file mode 100644
index 00000000..3da510bf
--- /dev/null
+++ b/extensions/cutcraft/shapes/box.py
@@ -0,0 +1,163 @@
+
+# -*- 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 .
+
+from ..core.part import Part
+from ..core.point import Point
+from ..core.trace import Trace
+from ..core.fingerjoint import FingerJoint
+from .shape import Shape
+
+class Box(Shape):
+ """ List of segments that make up a part. """
+ def __init__(self, width, depth, height, thickness, kerf, top=True, bottom=True,
+ left=True, right=True, front=True, back=True):
+ super(Box, self).__init__(thickness, kerf)
+
+ self.width = width
+ self.depth = depth
+ self.height = height
+
+ self.faces = []
+ self.parts = []
+
+ for face in range(6):
+ p = self._face(face, width, depth, height, thickness, top, bottom, left, right, front, back)
+ self.faces.append((p, face))
+ self.parts.append((p, face))
+
+ if kerf:
+ for part, _ in self.parts:
+ part.applykerf(kerf)
+
+ def _face(self, face, width, depth, height, thickness, top, bottom, left, right, front, back):
+ faces = (top, bottom, left, right, front, back)
+
+ # Check if the requested face is active for this box.
+ if faces[face] == False:
+ return None
+
+ wjoint = FingerJoint(width, thickness*2.0, 'width', thickness=thickness)
+ djoint = FingerJoint(depth, thickness*2.0, 'depth', thickness=thickness)
+ hjoint = FingerJoint(height, thickness*2.0, 'height', thickness=thickness)
+
+ if face in (0, 1):
+ t = Trace(x=[thickness], y=[thickness])
+ for i, pos in enumerate(djoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(pos, thickness)
+ t += Point(pos, 0.0)
+ else:
+ t += Point(pos, 0.0)
+ t += Point(pos, thickness)
+ t += Point(depth-thickness, thickness)
+ for i, pos in enumerate(wjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(depth-thickness, pos)
+ t += Point(depth, pos)
+ else:
+ t += Point(depth, pos)
+ t += Point(depth-thickness, pos)
+ t += Point(depth-thickness, width-thickness)
+ for i, pos in enumerate(reversed(djoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(pos, width-thickness)
+ t += Point(pos, width)
+ else:
+ t += Point(pos, width)
+ t += Point(pos, width-thickness)
+ t += Point(thickness, width-thickness)
+ for i, pos in enumerate(reversed(wjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(thickness, pos)
+ t += Point(0.0, pos)
+ else:
+ t += Point(0.0, pos)
+ t += Point(thickness, pos)
+ elif face in (2, 3):
+ t = Trace(x=[0.0], y=[0.0])
+ for i, pos in enumerate(djoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(pos, 0.0)
+ t += Point(pos, thickness)
+ else:
+ t += Point(pos, thickness)
+ t += Point(pos, 0.0)
+ t += Point(depth, 0.0)
+ for i, pos in enumerate(hjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(depth, pos)
+ t += Point(depth-thickness, pos)
+ else:
+ t += Point(depth-thickness, pos)
+ t += Point(depth, pos)
+ t += Point(depth, height)
+ for i, pos in enumerate(reversed(djoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(pos, height)
+ t += Point(pos, height-thickness)
+ else:
+ t += Point(pos, height-thickness)
+ t += Point(pos, height)
+ t += Point(0.0, height)
+ for i, pos in enumerate(reversed(hjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(0.0, pos)
+ t += Point(thickness, pos)
+ else:
+ t += Point(thickness, pos)
+ t += Point(0.0, pos)
+ pass
+ elif face in (4, 5):
+ t = Trace(x=[thickness], y=[0.0])
+ for i, pos in enumerate(wjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(pos, 0.0)
+ t += Point(pos, thickness)
+ else:
+ t += Point(pos, thickness)
+ t += Point(pos, 0.0)
+ t += Point(width-thickness, 0.0)
+ for i, pos in enumerate(hjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(width-thickness, pos)
+ t += Point(width, pos)
+ else:
+ t += Point(width, pos)
+ t += Point(width-thickness, pos)
+ t += Point(width-thickness, height)
+ for i, pos in enumerate(reversed(wjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(pos, height)
+ t += Point(pos, height-thickness)
+ else:
+ t += Point(pos, height-thickness)
+ t += Point(pos, height)
+ t += Point(thickness, height)
+ for i, pos in enumerate(reversed(hjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(thickness, pos)
+ t += Point(0.0, pos)
+ else:
+ t += Point(0.0, pos)
+ t += Point(thickness, pos)
+ pass
+
+ t.close()
+
+ return Part() + t
diff --git a/extensions/cutcraft/shapes/cone.py b/extensions/cutcraft/shapes/cone.py
new file mode 100644
index 00000000..1bf7e532
--- /dev/null
+++ b/extensions/cutcraft/shapes/cone.py
@@ -0,0 +1,35 @@
+# -*- 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 .
+
+from ..core.part import Part
+from ..platforms.circular import Circular
+from .shape import Shape
+
+class Cone(Shape):
+ """ List of segments that make up a part. """
+ def __init__(self, height, radius1, radius2, segments, cuts, cutdepth, platforms,
+ thickness, kerf):
+ super(Cone, self).__init__(thickness, kerf)
+
+# levels = [p/(platforms-1)*height for p in range(platforms)]
+# radii = [radius1 + (radius2-radius1)*p/(platforms-1) for p in range(platforms)]
+
+# for level, radius in zip(levels, radii):
+# p = cc.Part()
+# p += cp.Circular(radius, segments, cuts, cutdepth, thickness=thickness, kerf=kerf).part
+# self.parts.append((p, level))
diff --git a/extensions/cutcraft/shapes/cylinder.py b/extensions/cutcraft/shapes/cylinder.py
new file mode 100644
index 00000000..89c69469
--- /dev/null
+++ b/extensions/cutcraft/shapes/cylinder.py
@@ -0,0 +1,48 @@
+# -*- 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 .
+
+from ..core.part import Part
+from ..platforms.circular import Circular
+from ..supports.pier import Pier
+from .shape import Shape
+
+class Cylinder(Shape):
+ """ List of segments that make up a part. """
+ def __init__(self, height, radius, inradius, segments, cuts, cutdepth, supwidth, platforms,
+ thickness, kerf):
+ super(Cylinder, self).__init__(thickness, kerf)
+
+ self.platforms = []
+ self.piers = []
+
+ # List of vertical positions for the platforms
+ levels = [float(p)/float(platforms-1)*(height-thickness) for p in range(platforms)]
+
+ for level in levels:
+ p = Circular(radius, inradius, segments, cuts, cutdepth, thickness=thickness)
+ self.platforms.append((p, level))
+ self.parts.append((p, level))
+
+ for _ in range(cuts):
+ p = Pier(height, supwidth, supwidth-cutdepth, [(level, 0.0) for level in levels], thickness=thickness)
+ self.piers.append((p, None))
+ self.parts.append((p, None))
+
+ if kerf:
+ for part, _ in self.parts:
+ part.applykerf(kerf)
diff --git a/extensions/cutcraft/shapes/rollerbot.py b/extensions/cutcraft/shapes/rollerbot.py
new file mode 100644
index 00000000..e48bee1b
--- /dev/null
+++ b/extensions/cutcraft/shapes/rollerbot.py
@@ -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 .
+
+from ..core.part import Part
+from ..platforms.rollerframe import RollerFrame
+from ..supports.pier import Pier
+from .shape import Shape
+
+class RollerBot(Shape):
+ """ List of segments that make up a part. """
+ def __init__(self, width, supwidth, wheelradius, upperradius, lowerradius,
+ facesize, barsize, primarygapwidth, secondarygapwidth, scale,
+ thickness, kerf):
+ super(RollerBot, self).__init__(thickness, kerf)
+
+ self.platforms = []
+ self.piers = []
+
+ cutdepth = supwidth / 3.0
+
+ for level in range(9):
+# for level in range(7):
+ p = RollerFrame(supwidth, wheelradius, upperradius, lowerradius,
+ facesize, barsize, primarygapwidth, secondarygapwidth,
+ scale, level, thickness=thickness)
+ self.platforms.append((p, 0.0))
+ self.parts.append((p, 0.0))
+
+ levels = [0.0, secondarygapwidth+thickness,
+ secondarygapwidth+primarygapwidth+thickness*2.0,
+ secondarygapwidth+primarygapwidth*2.0+thickness*3.0,
+ secondarygapwidth*2.0+primarygapwidth*2.0+thickness*4.0 ]
+ height = secondarygapwidth*2.0+primarygapwidth*2.0+thickness*5.0
+
+ for _ in range(9):
+ p = Pier(height, supwidth, supwidth-cutdepth, [(level, 0.0) for level in levels], thickness=thickness)
+ self.piers.append((p, None))
+ self.parts.append((p, None))
+
+ levels = [0.0, secondarygapwidth+thickness ]
+ height = secondarygapwidth+thickness*2.0
+
+ for _ in range(4):
+ p = Pier(height, supwidth, supwidth-cutdepth, [(level, 0.0) for level in levels], thickness=thickness)
+ self.piers.append((p, None))
+ self.parts.append((p, None))
+
+ if kerf:
+ for part, _ in self.parts:
+ part.applykerf(kerf)
diff --git a/extensions/cutcraft/shapes/shape.py b/extensions/cutcraft/shapes/shape.py
new file mode 100644
index 00000000..f9d577ce
--- /dev/null
+++ b/extensions/cutcraft/shapes/shape.py
@@ -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 .
+
+class Shape(object):
+ def __init__(self, thickness, kerf):
+ self.thickness = thickness
+ self.kerf = kerf
+ self.parts = []
+ return
+
+ def close(self):
+ for part in self.parts:
+ part[0].close()
diff --git a/extensions/cutcraft/shapes/sphere.py b/extensions/cutcraft/shapes/sphere.py
new file mode 100644
index 00000000..6d8f3d0f
--- /dev/null
+++ b/extensions/cutcraft/shapes/sphere.py
@@ -0,0 +1,26 @@
+# -*- 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 .
+
+from ..core.part import Part
+from ..platforms.circular import Circular
+from .shape import Shape
+
+class Sphere(Shape):
+ """ List of segments that make up a part. """
+ def __init__(self):
+ super(Sphere, self).__init__()
diff --git a/extensions/cutcraft/supports/__init__.py b/extensions/cutcraft/supports/__init__.py
new file mode 100644
index 00000000..43c2ecf8
--- /dev/null
+++ b/extensions/cutcraft/supports/__init__.py
@@ -0,0 +1,22 @@
+# -*- 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 .
+
+from .support import Support
+from .pier import Pier
+
+__all__ = ["Support", "Pier"]
diff --git a/extensions/cutcraft/supports/pier.py b/extensions/cutcraft/supports/pier.py
new file mode 100644
index 00000000..9db48e8b
--- /dev/null
+++ b/extensions/cutcraft/supports/pier.py
@@ -0,0 +1,85 @@
+# -*- 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 .
+
+from ..core.trace import Trace
+from .support import Support
+from ..util import isclose
+
+class Pier(Support):
+ """ List of segments that make up a part. """
+ def __init__(self, height, depth, cutdepth, levels, thickness):
+ super(Pier, self).__init__(height, thickness)
+
+ self.depth = depth
+ self.cutdepth = cutdepth
+
+ # The 'levels' list is defined as follows:
+ # [(height1, x-offset1), (height2, x-offset2), ...]
+ # where the values are:
+ # height: The height of the bottom of the platform (0.0=at base level, height-thickness=at top level).
+ # x-offset: The distance from the core axis for this level. Used to create slopes and curves.
+
+ ys, xs = zip(*sorted(levels))
+ if any([(h2) - (h1) < 3*thickness for h1, h2
+ in zip(ys[:-1], ys[1:])]):
+ raise RuntimeError("Pier levels are too close. Try decreasing the number of levels.")
+
+ self.topcut = isclose(ys[-1], height-thickness)
+ self.bottomcut = isclose(ys[0], 0.0)
+ self.vertical = all([isclose(x1, x2) for x1, x2 in zip(xs[:-1], xs[1:])])
+
+ # Starting at the bottom inner point, trace up the uncut side.
+ if self.vertical:
+ tx = [0.0, 0.0]
+ ty = [0.0, height]
+ else:
+ tx = list(xs)
+ ty = list(ys[:-1]) + [height]
+
+ # Add the top points.
+ xtop = xs[0]
+ xbottom = xs[-1]
+ if self.topcut:
+ tx.extend([xtop+depth-cutdepth, xtop+depth-cutdepth, xtop+depth])
+ ty.extend([height, height-thickness, height-thickness])
+ else:
+ tx.extend([xtop+depth])
+ ty.extend([height])
+
+ if self.topcut and self.bottomcut:
+ xs = xs[1:-1]
+ ys = ys[1:-1]
+ elif self.topcut:
+ xs = xs[:-1]
+ ys = ys[:-1]
+ elif self.bottomcut:
+ xs = xs[1:]
+ ys = ys[1:]
+
+ for y, x in zip(reversed(ys), reversed(xs)):
+ tx.extend([x+depth, x+depth-cutdepth, x+depth-cutdepth, x+depth])
+ ty.extend([y+thickness, y+thickness, y, y])
+
+ if self.bottomcut:
+ tx.extend([xbottom+depth, xbottom+depth-cutdepth, xbottom+depth-cutdepth, xbottom])
+ ty.extend([thickness, thickness, 0.0, 0.0])
+ else:
+ tx.extend([xbottom+depth, xbottom])
+ ty.extend([0.0, 0.0])
+
+ self.traces.append(Trace(x=tx, y=ty))
diff --git a/extensions/cutcraft/supports/support.py b/extensions/cutcraft/supports/support.py
new file mode 100644
index 00000000..252788f9
--- /dev/null
+++ b/extensions/cutcraft/supports/support.py
@@ -0,0 +1,26 @@
+# -*- 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 .
+
+from ..core.part import Part
+
+class Support(Part):
+ """ General support. """
+ def __init__(self, height, thickness):
+ super(Support, self).__init__()
+ self.height = height
+ self.thickness = thickness
diff --git a/extensions/cutcraft/util.py b/extensions/cutcraft/util.py
new file mode 100644
index 00000000..4e81e783
--- /dev/null
+++ b/extensions/cutcraft/util.py
@@ -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 .
+
+from math import pi, atan2, cos, sin, sqrt
+
+def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
+ # Required as Inkscape uses old version of Python that does not include math.isclose().
+ return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
+
+def iscloselist(a, b):
+ # Functionality of isclose() for lists of values.
+ return all([isclose(aval, bval) for aval, bval in zip(a, b)])
+
+def intersection(radius, angle=None, x=None, y=None):
+ # With a circle of a given radius determine the intercepts for an angle, x or y coordinate.
+ if angle:
+ # Returns (x,y) tuple of intercept.
+ return (cos(angle)*radius, sin(angle)*radius)
+ elif x:
+ y = sqrt((radius)**2 - x**2)
+ return (y, atan2(y, x))
+ elif y:
+ x = sqrt((radius)**2 - y**2)
+ return (x, atan2(y, x))
+ else:
+ raise ValueError("Invalid values passed to intersection().")
diff --git a/extensions/fablabchemnitz_cutcraftbox.inx b/extensions/fablabchemnitz_cutcraftbox.inx
new file mode 100644
index 00000000..55661c7a
--- /dev/null
+++ b/extensions/fablabchemnitz_cutcraftbox.inx
@@ -0,0 +1,60 @@
+
+
+ <_name>Cut-Craft Box
+ fablabchemnitz.de.cutcraft.box
+
+
+
+
+
+
+
+ <_param name="help1" type="description" xml:space="preserve">------------------------------
+ 60.0
+ 30.0
+ 30.0
+ <_param name="help2" type="description" xml:space="preserve">------------------------------
+ 5.0
+ 0.01
+
+
+
+
+
+
+ <_param name="use1" type="description" xml:space="preserve">Cut Craft Box: Help
+
+
+Measurement Units: Unit of measurement for all subsequent values entered in this dialog.
+
+
+Width: Cylinder Width.
+
+Depth: Cylinder Depth.
+
+Height: Cylinder Height.
+
+
+
+Thickness: Thickness of the material.
+
+Kerf: Laser Cutter Kerf (tolerance). Varies based on cutter and material thickness.
+
+Line Thickness: Thickness of the cutting line on the display.
+
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz_cutcraftbox.py b/extensions/fablabchemnitz_cutcraftbox.py
new file mode 100644
index 00000000..391667ad
--- /dev/null
+++ b/extensions/fablabchemnitz_cutcraftbox.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+
+import inkex
+from fablabchemnitz_cutcraftshape import CutCraftShape
+import cutcraft.platforms as cp
+from cutcraft.shapes import Box
+
+class CutCraftBox(CutCraftShape):
+ def __init__(self):
+ CutCraftShape.__init__(self)
+ self.arg_parser.add_argument("--width", type=float, default=6.0, help="Box Width")
+ self.arg_parser.add_argument("--depth", type=float, default=6.0, help="Box Depth")
+ self.arg_parser.add_argument("--height", type=float, default=60.0, help="Box height")
+
+ def effect(self):
+ CutCraftShape.effect(self)
+
+ width = self.svg.unittouu( str(self.options.width) + self.unit )
+ depth = self.svg.unittouu( str(self.options.depth) + self.unit )
+ height = self.svg.unittouu( str(self.options.height) + self.unit )
+
+ shape = Box(width, depth, height, self.thickness, self.kerf)
+
+ self.pack(shape)
+
+if __name__ == '__main__':
+ CutCraftBox().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz_cutcraftcylinder.inx b/extensions/fablabchemnitz_cutcraftcylinder.inx
new file mode 100644
index 00000000..edc02d5e
--- /dev/null
+++ b/extensions/fablabchemnitz_cutcraftcylinder.inx
@@ -0,0 +1,73 @@
+
+
+ <_name>Cut-Craft Cylinder
+ fablabchemnitz.de.cutcraft.cylinder
+
+
+
+
+
+
+
+ <_param name="help1" type="description" xml:space="preserve">------------------------------
+ 60.0
+ 60.0
+ 30.0
+ 3
+ 2
+ 3
+ 6.0
+ <_param name="help2" type="description" xml:space="preserve">------------------------------
+ 5.0
+ 0.01
+
+
+
+
+
+
+ <_param name="use1" type="description" xml:space="preserve">Cut Craft Cylinder: Help
+
+
+Measurement Units: Unit of measurement for all subsequent values entered in this dialog.
+
+
+Height: Cylinder Height.
+
+Outer Diameter: Outside diameter of the Cylinder.
+
+Inner Diameter: Inside diameter of the Cylinder.
+
+
+Number of Vertices: Number of vertices for the Cylinder (3 = Triangle, 4 = Square, ... 90 = Circular).
+
+Number of Levels: Number of horizontal circular platforms.
+
+Number of Supports: Number of vertical supports holding the cylinder together.
+
+Support Width: Width of the vertical supports holding the cylinder together.
+
+
+
+Thickness: Thickness of the material.
+
+Kerf: Laser Cutter Kerf (tolerance). Varies based on cutter and material thickness.
+
+Line Thickness: Thickness of the cutting line on the display.
+
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz_cutcraftcylinder.py b/extensions/fablabchemnitz_cutcraftcylinder.py
new file mode 100644
index 00000000..b5c8a380
--- /dev/null
+++ b/extensions/fablabchemnitz_cutcraftcylinder.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+import inkex
+from fablabchemnitz_cutcraftshape import CutCraftShape
+import cutcraft.platforms as cp
+from cutcraft.shapes import Cylinder
+
+class CutCraftCylinder(CutCraftShape):
+ def __init__(self):
+ CutCraftShape.__init__(self)
+ self.arg_parser.add_argument("--vertices", type=int, default=3, help="Number of vertices")
+ self.arg_parser.add_argument("--levels", type=int, default=3, help="Number of levels")
+ self.arg_parser.add_argument("--supports", type=int, default=3, help="Number of supports")
+ self.arg_parser.add_argument("--supwidth", type=float, default=6.0, help="Support Width")
+ self.arg_parser.add_argument("--height", type=float, default=60.0, help="Cylinder height")
+ self.arg_parser.add_argument("--outer", type=float, default=60.0, help="Diameter of cylinder")
+ self.arg_parser.add_argument("--inner", type=float, default=30.0, help="Diameter of central hole - 0.0 for no hole")
+
+ def effect(self):
+ CutCraftShape.effect(self)
+
+ vertices = self.options.vertices
+ levels = self.options.levels
+ supports = self.options.supports
+ supwidth = self.svg.unittouu( str(self.options.supwidth) + self.unit )
+ height = self.svg.unittouu( str(self.options.height) + self.unit )
+ outer = self.svg.unittouu( str(self.options.outer) + self.unit )
+ inner = self.svg.unittouu( str(self.options.inner) + self.unit )
+
+ if outer<=inner:
+ self._error("ERROR: Outer diameter must be greater than inner diameter.")
+ exit()
+
+ shape = Cylinder(height, outer/2.0, inner/2.0, vertices, supports, supwidth/2.0, supwidth, levels,
+ self.thickness, self.kerf)
+
+ self.pack(shape)
+
+if __name__ == '__main__':
+ CutCraftCylinder().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz_cutcraftrollerbot.inx b/extensions/fablabchemnitz_cutcraftrollerbot.inx
new file mode 100644
index 00000000..480e8e56
--- /dev/null
+++ b/extensions/fablabchemnitz_cutcraftrollerbot.inx
@@ -0,0 +1,53 @@
+
+
+ <_name>Cut-Craft RollerBot
+ fablabchemnitz.de.cutcraft.rollerbot
+
+
+
+
+
+
+
+ <_param name="help1" type="description" xml:space="preserve">------------------------------
+ 12.0
+ <_param name="help2" type="description" xml:space="preserve">------------------------------
+ 5.0
+ 0.01
+
+
+
+
+
+
+ <_param name="use1" type="description" xml:space="preserve">Cut Craft RollerBot: Help
+
+
+Measurement Units: Unit of measurement for all subsequent values entered in this dialog.
+
+
+Support Width: Width of the supports holding the robot together.
+
+
+Thickness: Thickness of the material.
+
+Kerf: Laser Cutter Kerf (tolerance). Varies based on cutter and material thickness.
+
+Line Thickness: Thickness of the cutting line on the display.
+
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz_cutcraftrollerbot.py b/extensions/fablabchemnitz_cutcraftrollerbot.py
new file mode 100644
index 00000000..6a88d5f8
--- /dev/null
+++ b/extensions/fablabchemnitz_cutcraftrollerbot.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+import inkex
+from fablabchemnitz_cutcraftshape import CutCraftShape
+import cutcraft.platforms as cp
+from cutcraft.shapes import RollerBot
+
+class CutCraftRollerBot(CutCraftShape):
+ def __init__(self):
+ CutCraftShape.__init__(self)
+ self.arg_parser.add_argument("--supwidth", type=float, default=6.0, help="Support Width")
+
+ def effect(self):
+ CutCraftShape.effect(self)
+
+ supwidth = self.svg.unittouu( str(self.options.supwidth) + self.unit )
+
+ # Constants in the current RollerBot design.
+ wheelradius = self.svg.unittouu( str(100.0) + "mm" )
+ upperradius = self.svg.unittouu( str(92.0) + "mm" )
+ lowerradius = self.svg.unittouu( str(82.0) + "mm" )
+ facesize = self.svg.unittouu( str(50.0) + "mm" )
+ barsize = self.svg.unittouu( str(25.4) + "mm" )
+ scale = self.svg.unittouu( str(1.0) + "mm" )
+
+ primarygapwidth = self.svg.unittouu( str(70.0) + "mm" ) # Must be greater than width of Raspberry PI / Arduino.
+ secondarygapwidth = self.svg.unittouu( str(25.0) + "mm" )
+ width = primarygapwidth*2.0 + secondarygapwidth*2.0 + self.thickness*5.0
+
+ shape = RollerBot(width, supwidth, wheelradius, upperradius, lowerradius,
+ facesize, barsize, primarygapwidth, secondarygapwidth, scale,
+ self.thickness, self.kerf)
+
+ self.pack(shape)
+
+if __name__ == '__main__':
+ CutCraftRollerBot().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz_cutcraftshape.py b/extensions/fablabchemnitz_cutcraftshape.py
new file mode 100644
index 00000000..df9cde93
--- /dev/null
+++ b/extensions/fablabchemnitz_cutcraftshape.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+
+import gettext
+import inkex
+from math import floor
+from cutcraft.core import Point, Rectangle
+from lxml import etree
+
+#TODOS
+'''
+since InkScape 1.0 / Python 3 adjustments are required to fix "TypeError: '<' not supported between instances of 'Pier' and 'Pier'". A __lt__ method has to be implemented
+"for this reasion items = sorted([(p[0].area(),p[0]) for p in shape.parts], reverse=True)" was commented out
+'''
+
+class CutCraftNode(object):
+ def __init__(self, rect):
+ self.children = []
+ self.rectangle = rect
+ self.part = None
+
+ def insert(self, part, shape):
+ if len(self.children)>0:
+ node = self.children[0].insert(part, shape)
+ if node is not None:
+ return node
+ else:
+ return self.children[1].insert(part, shape)
+
+ if self.part is not None:
+ return None
+
+ pwidth, pheight = part.bbox().expanded().size()
+ nwidth, nheight = self.rectangle.expanded().size()
+
+ if pwidth>nwidth or pheight>nheight:
+ # Too small.
+ return None
+ if pwidth==nwidth and pheight==nheight:
+ # This node fits.
+ self.part = part
+ return self
+
+ nleft, ntop = self.rectangle.expanded().topleft.tup()
+ nright, nbottom = self.rectangle.expanded().bottomright.tup()
+
+ if nwidth - pwidth > nheight - pheight:
+ r1 = Rectangle(Point(nleft, ntop),
+ Point(nleft+pwidth, nbottom))
+ r2 = Rectangle(Point(nleft+pwidth+1.0, ntop),
+ Point(nright, nbottom))
+ else:
+ r1 = Rectangle(Point(nleft, ntop),
+ Point(nright, ntop+pheight))
+ r2 = Rectangle(Point(nleft, ntop+pheight+1.0),
+ Point(nright, nbottom))
+
+ self.children = [CutCraftNode(r1), CutCraftNode(r2)]
+
+ return self.children[0].insert(part, shape)
+
+class CutCraftShape(inkex.Effect):
+ def __init__(self):
+ inkex.Effect.__init__(self)
+ self.arg_parser.add_argument("--active-tab", default="Options", help="The tab selected when OK was pressed")
+ self.arg_parser.add_argument("--unit", default="mm", help="unit of measure for circular pitch and center diameter")
+ self.arg_parser.add_argument("--thickness", type=float, default=20.0, help="Material Thickness")
+ self.arg_parser.add_argument("--kerf", type=float, default=20.0, help="Laser Cutter Kerf")
+ self.arg_parser.add_argument("--linethickness", default="1px", help="Line Thickness")
+
+ def effect(self):
+ self.unit = self.options.unit
+ self.thickness = self.svg.unittouu( str(self.options.thickness) + self.unit)
+ self.kerf = self.svg.unittouu( str(self.options.kerf) + self.unit)
+ self.linethickness = self.svg.unittouu(self.options.linethickness)
+
+ svg = self.document.getroot()
+ self.docwidth = self.svg.unittouu(svg.get('width'))
+ self.docheight = self.svg.unittouu(svg.get('height'))
+
+ self.parent=self.svg.get_current_layer()
+
+ layer = etree.SubElement(svg, 'g')
+ layer.set(inkex.addNS('label', 'inkscape'), 'newlayer')
+ layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+
+ def _debug(self, string):
+ inkex.debug( gettext.gettext(str(string)) )
+
+ def _error(self, string):
+ inkex.errormsg( gettext.gettext(str(string)) )
+
+ def pack(self, shape):
+ # Pack the individual parts onto the current canvas.
+ line_style = { 'stroke': '#000000',
+ 'stroke-width': str(self.linethickness),
+ 'fill': 'none' }
+
+ #items = sorted([(p[0].area(),p[0]) for p in shape.parts], reverse=True)
+ items = [(p[0].area(),p[0]) for p in shape.parts]
+ #for p in shape.parts:
+ # inkex.utils.debug(p[0])
+
+ rootnode = CutCraftNode(Rectangle(Point(0.0, 0.0), Point(floor(self.docwidth), floor(self.docheight))))
+
+ for i, (_, part) in enumerate(items):
+ node = rootnode.insert(part, self)
+ if node is None:
+ self._error("ERROR: Cannot fit parts onto canvas.\n" +
+ "Try a larger canvas and then manually arrange if required.")
+ exit()
+
+ bbox = part.bbox().expanded()
+ part += -bbox.topleft
+ part += node.rectangle.topleft
+
+ line_attribs = { 'style' : str(inkex.Style(line_style)),
+ inkex.addNS('label','inkscape') : 'Test ' + str(i),
+ 'd' : part.svg() }
+ _ = etree.SubElement(self.parent, inkex.addNS('path','svg'), line_attribs)
\ No newline at end of file