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