Inital commit, added client application and scad files.

master
Martin 2018-10-01 09:22:21 +02:00
commit d4e985f878
15 zmienionych plików z 2145 dodań i 0 usunięć

4
.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1,4 @@
*.pickle
*.csv
*.gcode
__pycache__

11
README.md 100644
Wyświetl plik

@ -0,0 +1,11 @@
# Embroiderino
Embroiderino is a test project which aims at providing all needed to turn an ordinary sewing machine into digital embroidery machine or at least to reuse any other embroidery machine with mainboard failure. Most of the mechanical parts are intended to be made on 3D printer.
As an electronics, arduio based 3D printer board is tested, with additional encoders connected to it. [This](https://gitlab.com/markol/XYprotoboard) will do job for now too. As a firmware
[teathimble](https://gitlab.com/markol/Teathimble_Firmware) is in development. For hoop positioning [this plotter](https://gitlab.com/markol/Simple_plotter) is adopted. All mechanics parts and sewing machine itself are mounted to chipboard.
Client application is written in python3 and TK, it can open CSV's created by [Embroidermodder2](https://github.com/Embroidermodder/Embroidermodder).
## Work in progress
Note that this is early stage development. Machine, main DC motor speed controller is a next milestone. All the code is licensed under GPL v3.

Wyświetl plik

@ -0,0 +1,19 @@
**Client application** uses serial port to communicate with microcontroller and is written in python3 and TK. This app can open CSV's embroidery designs created by [Embroidermodder2](https://github.com/Embroidermodder/Embroidermodder) or G-code specific. Embroidermodder2 can load plenty of available file formats, so you can use it for conversion or your own design.
## Features
- Path preview.
- Basic transforms (move, rotate, mirror, scale).
- Some statistics (stitches no, tool changes, path length).
## List of supported G-codes
- **G0** - liner move, jump
- **G1** - linear move, stitch
- **G28** - home axes
- **G90** - set to absolute positioning
- **G91** - set to relative positioning
- **M114** - get current pos
- **M6** - tool change, color change
- **G12** - clean tool or trim thread
- **M116** - wait

525
control_app/app.py 100644
Wyświetl plik

@ -0,0 +1,525 @@
#!/usr/bin/python3
from tkinter import Tk, Label, Button, Entry, Menu, filedialog, messagebox, colorchooser, Canvas, Frame, LabelFrame, Scale, Toplevel, PhotoImage
from tkinter.font import Font
import tkinter.ttk as ttk
from tkinter import LEFT, TOP, BOTTOM, N, YES, W,SUNKEN,X, HORIZONTAL, DISABLED, NORMAL, RAISED, FLAT, RIDGE, END
from path_preview import ResizingCanvas, load_gcode_file, save_gcode_file, load_csv_file, translate_toolpath, rotate_toolpath, reflect_toolpath, scale_toolpath, toolpath_border_points, toolpath_info, _from_rgb
from collections import namedtuple
import copy, re, math, time, pickle
import control_serial as serial
class ControlAppGUI:
def __init__(self, master):
self.master = master
# GUI layout setup
self.menu = Menu(self.master)
self.master.config(menu=self.menu)
master.grid_rowconfigure(0, weight=1)
master.grid_columnconfigure(0, weight=1)
filemenu = Menu(self.menu)
self.menu.add_cascade(label="File", menu=filemenu)
filemenu.add_command(label="New", command=self.NewFile)
openmenu = Menu(self.menu)
openmenu.add_command(label="Gcode", command=self.OpenGcodeFile)
openmenu.add_command(label="CSV", command=self.OpenCsvFile)
savemenu = Menu(self.menu)
savemenu.add_command(label="Gcode", command=self.SaveGcodeFile)
savemenu.add_command(label="CSV", command=self.SaveCsvFile)
filemenu.add_cascade(label='Open...', menu=openmenu, underline=0)
filemenu.add_cascade(label="Save...", menu=savemenu, underline=0)
filemenu.add_command(label="Set color", command=self.AskColor)
filemenu.add_separator()
filemenu.add_command(label="Exit", command=self.Quit)
editmenu = Menu(self.menu)
self.menu.add_cascade(label="Edit", menu=editmenu)
editmenu.add_command(label="Settings", command=self.Settings)
helpmenu = Menu(self.menu)
self.menu.add_cascade(label="Help", menu=helpmenu)
helpmenu.add_command(label="About...", command=self.About)
master.title("Embroiderino frontend")
self.controls = ttk.Notebook(master)
tab1 = Frame(self.controls)
tab2 = Frame(self.controls)
self.controls.add(tab1, text = "Machine control")
self.controls.add(tab2, text = "Path manipulation")
self.controls.grid(row=0,column=1, sticky=N)
self.controls.grid_rowconfigure(0, weight=1)
self.controls.grid_columnconfigure(0, weight=1)
# MACHINE TAB
self.portCombo = ttk.Combobox(tab1, values=serial.serial_ports())
self.portCombo.current(0)
self.portCombo.grid(row=1,column=0)
self.baudCombo = ttk.Combobox(tab1,state='readonly', values=("115200", "9600"), width=10)
self.baudCombo.current(0)
self.baudCombo.grid(row=1,column=1)
self.connectButton = Button(tab1, text="Connect", command=self.ToggleConnect, width=10)
self.connectButton.grid(row=1,column=2)
self.startButton = Button(tab1, text="Start job", command=self.ToggleStart, state=DISABLED)
self.startButton.grid(row=2,column=1)
self.homeButton = Button(tab1, text="Home machine", command=lambda: serial.queue_command("G28\n"), state=DISABLED)
self.homeButton.grid(row=2,column=0)
testNavigation = Frame(tab1)
leftButton = Button(testNavigation, text="<", command=lambda: serial.queue_command("G91\nG0 Y-2\nG90\n"), state=DISABLED)
leftButton.grid(row=1,column=0)
rightButton = Button(testNavigation, text=">", command=lambda: serial.queue_command("G91\nG0 Y2\nG90\n"), state=DISABLED)
rightButton.grid(row=1,column=2)
upButton = Button(testNavigation, text="/\\", command=lambda: serial.queue_command("G91\nG0 X2\nG90\n"), state=DISABLED)
upButton.grid(row=0,column=1)
downButton = Button(testNavigation, text="\\/", command=lambda: serial.queue_command("G91\nG0 X-2\nG90\n"), state=DISABLED)
downButton.grid(row=2,column=1)
testNavigation.grid(row=3,column=0)
self.navigationButtons = [leftButton, rightButton, upButton, downButton]
self.testButton = Button(tab1, text="Test border path", command=self.TestBorder, state=DISABLED)
self.testButton.grid(row=3,column=1)
self.gotoButton = Button(tab1, text="Go to", command=self.GoTo, state=DISABLED, relief=RAISED)
self.gotoButton.grid(row=3,column=2)
self.stopButton = Button(tab1, text="STOP", command=self.StopAll, state=DISABLED)
self.stopButton.grid(row=4,column=0)
progressFrame = Frame(tab1)
Label(progressFrame, text="Tool changes: ", bd=1).grid(row=0,column=0)
self.toolChangesLabel = Label(progressFrame, text="0/0", bd=1, relief=SUNKEN)
self.toolChangesLabel.grid(row=1,column=0)
Label(progressFrame, text="Tool points: ", bd=1).grid(row=0,column=2)
self.toolPointsLabel = Label(progressFrame, text="0/0", bd=1, relief=SUNKEN)
self.toolPointsLabel.grid(row=1,column=2)
Label(progressFrame, text="Estimated endtime: ", bd=1).grid(row=0,column=4)
self.timeLabel = Label(progressFrame, text="0/0", bd=1, relief=SUNKEN)
self.timeLabel.grid(row=1,column=4)
progressFrame.grid(row=5,column=0, columnspan=3)
# PATH TAB
tab2.grid_columnconfigure(0, weight=1)
Label(tab2, text="Display progress: ", bd=1).grid(row=0)
self.slider = Scale(tab2, from_=0, to=0, command=self.UpdatePath, orient=HORIZONTAL,length=300)
self.slider.grid(row=1)
toolbar = Frame(tab2, bd=1, relief=RAISED)
toolbar.grid(row=2)
self.panButton = Button(toolbar, relief=RAISED, command=self.TogglePan, text="Move path")
self.panButton.pack(side=LEFT, padx=2, pady=2)
self.rotateButton = Button(toolbar, relief=RAISED, command=self.ToggleRotate, text="Rotate path")
self.rotateButton.pack(side=LEFT, padx=2, pady=2)
self.mirrorButton = Button(toolbar, relief=RAISED, command=self.ToggleMirror, text="Mirror path")
self.mirrorButton.pack(side=LEFT, padx=2, pady=2)
self.scaleButton = Button(toolbar, relief=RAISED, command=self.ToggleScale, text="Scale path")
self.scaleButton.pack(side=LEFT, padx=2, pady=2)
# CANVAS
canvasFrame = Frame(master)
canvasFrame.grid(row=0, column=0, sticky='NWES')
self.canvas = ResizingCanvas(canvasFrame,width=400, height=400, bg="white", highlightthickness=0)
self.canvas.bind("<B1-Motion>", self.CanvasDrag)
self.canvas.bind("<Button-1>", self.CanvasClick)
self.canvas.bind("<ButtonRelease-1>", self.CanvasRelease)
self.canvas.pack(expand=YES, anchor=N+W)
#STATUS BAR
self.status = Label(master, text="Not connected", bd=1, relief=SUNKEN, anchor=W)
self.status.grid(row=2, columnspan=2, sticky='WE')
# PROGRAM VARIABLES
self.SETTINGSFNAME = "settings.pickle"
self.commands = []
self.transform = (0,0)
self.isConnected = False
self.isJobRunning = False
self.isJobPaused = False
self.lastSendCommandIndex = -1
self.lastMove = None
self.currentColor = 'black'
self.currentToolChange = 0
self.toolChangesTotal = 0
self.currentToolPoint = 0
self.toolPointsTotal = 0
self.distancesList = []
self.distanceTraveled = 0
self.positionResponseRegex = re.compile("X:(\-?\d+\.\d+),Y:(\-?\d+\.\d+)")
self.workAreaSize = [100,100]
# LOAD SOME SETTIGS
self.loadSettings()
self.canvas.setArea(self.workAreaSize[0], self.workAreaSize[1])
# UI LOGIC
def Quit(self):
if messagebox.askyesno('Confirm', 'Really quit?'):
self.master.quit()
def AskColor(self):
color = colorchooser.askcolor(title = "Colour Chooser")
def NewFile(self):
if self.isJobRunning:
return
self.toolChangesTotal = 0
self.toolPointsTotal = 0
self.distancesList = []
self.lastSendCommandIndex = -1
self.lastMove = None
self.commands = []
self.canvas.clear()
self.slider.config(to=0)
def OpenGcodeFile(self):
if self.isJobRunning:
return
with filedialog.askopenfile(filetypes = (("Machine G-code","*.gcode"),)) as f:
self.commands = load_gcode_file(f)
self.FinishLoading()
def SaveGcodeFile(self):
if not self.commands:
return
with filedialog.asksaveasfile(filetypes = (("Machine G-code","*.gcode"),), defaultextension='.gcode') as f:
save_gcode_file(f, self.commands)
def OpenCsvFile(self):
if self.isJobRunning:
return
with filedialog.askopenfile(filetypes = (("Comma separated values","*.csv"),) ) as f:
self.commands = load_csv_file(f)
self.FinishLoading()
def SaveCsvFile(self):
pass
def FinishLoading(self):
points_count = len(self.commands)
# file loaded
if points_count > 2:
self.testButton.config(state=NORMAL)
self.startButton.config(state=NORMAL)
# center loaded path
rectangle = toolpath_border_points(self.commands[1:])
center = (rectangle[2][0] - (rectangle[2][0] - rectangle[0][0])/2, rectangle[2][1] - (rectangle[2][1] - rectangle[0][1])/2)
transform = (self.workAreaSize[0]/2 - center[0], self.workAreaSize[1]/2 - center[1])
self.commands = translate_toolpath(self.commands, transform)
self.slider.config(to=points_count)
self.slider.set(points_count)
self.toolPointsTotal, self.toolChangesTotal, self.distancesList = toolpath_info(self.commands)
self.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal))
self.toolChangesLabel.config(text="%d/%d" % (self.currentToolChange, self.toolChangesTotal))
self.timeLabel.config(text="%d/%d" % (self.distancesList[self.currentToolChange]- self.distanceTraveled, self.distancesList[-1]-self.distanceTraveled))
self.canvas.draw_toolpath(self.commands)
def About(self):
#self.ToolChangePopup()
messagebox.showinfo('About this software', 'This program 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 any later version.\n\nThis program 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.\n\nWritten in 2018 by markol.')
def Settings(self):
tl = Toplevel(root)
tl.title("Global settings")
frame = Frame(tl)
frame.grid()
machineFrame = LabelFrame(frame, text="Machine hoop workarea (mm)", relief=RIDGE)
machineFrame.grid()
Label(machineFrame, text="width " ).grid(row=0, column=0, sticky=N)
workareaWidth = Entry(machineFrame, text="1")
workareaWidth.grid(row=0,column=1)
workareaWidth.delete(0, END)
workareaWidth.insert(0, str(self.workAreaSize[0]))
Label(machineFrame, text="height " ).grid(row=2, column=0, sticky=N)
workareaHeight = Entry(machineFrame, text="2")
workareaHeight.grid(row=2,column=1)
workareaHeight.delete(0, END)
workareaHeight.insert(0, str(self.workAreaSize[1]))
def saveSettings():
try:
self.workAreaSize = (int(workareaWidth.get()), int(workareaHeight.get()))
except:
messagebox.showerror("Invalid numeric values","Please provide correct workarea values!")
return
self.canvas.setArea(self.workAreaSize[0], self.workAreaSize[1])
self.storeSettings()
TmpDim = namedtuple('TmpDim', 'width height')
tmp = TmpDim(self.canvas.width, self.canvas.height)
self.canvas.on_resize(tmp)
Button(frame, text="Save", command=saveSettings, width=10).grid(row=2, column=3)
Button(frame, text="Cancel", command=lambda: tl.destroy(), width=10).grid(row=2, column=2)
def loadSettings(self):
try:
with open(self.SETTINGSFNAME, "rb") as f:
data = pickle.load(f)
self.workAreaSize = data["workAreaSize"]
except Exception as e:
print ("Unable to restore program settings:", str(e))
def storeSettings(self):
with open(self.SETTINGSFNAME, "wb") as f:
try:
data = {"workAreaSize": self.workAreaSize}
pickle.dump(data, f)
except Exception as e:
print ("error while saving settings:", str(e))
def ToggleConnect(self):
if self.isConnected:
serial.close_serial()
self.connectButton.config(text="Connect")
self.status.config(text="Not connected")
self.homeButton.config(state=DISABLED)
self.stopButton.config(state=DISABLED)
self.gotoButton.config(state=DISABLED)
self.SetNavButtonsState(False)
self.isConnected = False
else:
if serial.open_serial(self.portCombo.get(), self.baudCombo.get()):
self.connectButton.config(text="Disconnect")
self.status.config(text="Connected")
self.homeButton.config(state=NORMAL)
self.stopButton.config(state=NORMAL)
self.gotoButton.config(state=NORMAL)
self.SetNavButtonsState(True)
self.isConnected = True
self.GetPositionTimerTaks()
def TestBorder(self):
rectangle = toolpath_border_points(self.commands[1:])
for point in rectangle:
serial.queue_command("G0 X%f Y%f F5000\n" % point)
def ToggleStart(self):
if self.isJobPaused:
serial.queue.clear()
self.startButton.config(text="Resume job")
self.status.config(text="Job paused")
else:
self.startButton.config(text="Pause job")
self.status.config(text="Job in progress")
startInstructionIndex = self.lastSendCommandIndex + 1
# job launch
if not self.isJobRunning:
self.canvas.clear()
startInstructionIndex = 0
self.start = time.time()
# after every move command being sent, this callback is executed
def progressCallback(instruction_index):
point = self.commands[instruction_index]
if self.lastMove:
coord = (self.lastMove[1], self.lastMove[2], point[1], point[2])
color = self.currentColor
# set color for jump move
if "G0" in point[0]:
color = "snow2"
else:
self.currentToolPoint += 1
self.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal))
self.distanceTraveled += math.hypot(coord[0] - coord[2], coord[1] - coord[3])
line = self.canvas.create_line(self.canvas.calc_coords(coord), fill=color)
self.canvas.lift(self.canvas.pointer, line)
self.timeLabel.config(text="%d/%d" % (self.distancesList[self.currentToolChange]- self.distanceTraveled, self.distancesList[-1]-self.distanceTraveled))
self.lastSendCommandIndex = instruction_index
self.lastMove = point
# this callback pauses
def progressToolChangeCallback(instruction_index):
point = self.commands[instruction_index]
self.lastSendCommandIndex = instruction_index
self.currentColor = _from_rgb((point[1], point[2], point[3]))
self.ToggleStart()
self.currentToolChange += 1
self.toolChangesLabel.config(text="%d/%d" % (self.currentToolChange, self.toolChangesTotal))
self.ToolChangePopup(self.currentColor)
self.isJobRunning = True
commandsCount = len(self.commands)
# all the commands until tool change command, are queued at once
for i in range(startInstructionIndex, commandsCount):
point = self.commands[i]
# pause on color change
if "M6" in point[0]:
serial.queue_command("G0 F25000\n", lambda _, index = i: progressToolChangeCallback(index))
break
else:
serial.queue_command("%s X%f Y%f\n" % (point[0],point[1], point[2]), lambda _, index = i: progressCallback(index))
# queue job finish callback, it is unnecessary added after every pause but is cleaned in pause callback
serial.queue_command("M114\n", self.JobFinished)
self.isJobPaused = not self.isJobPaused
def SetNavButtonsState(self, enabled = False):
newState = NORMAL if enabled else DISABLED
for b in self.navigationButtons:
b.config(state=newState)
def TogglePan(self):
self.rotateButton.config(relief=RAISED)
self.scaleButton.config(relief=RAISED)
if self.isJobRunning:
return
if self.panButton.config('relief')[-1] == SUNKEN:
self.panButton.config(relief=RAISED)
else:
self.panButton.config(relief=SUNKEN)
def ToggleRotate(self):
self.panButton.config(relief=RAISED)
self.scaleButton.config(relief=RAISED)
if self.isJobRunning:
return
if self.rotateButton.config('relief')[-1] == SUNKEN:
self.rotateButton.config(relief=RAISED)
else:
self.rotateButton.config(relief=SUNKEN)
def ToggleMirror(self):
self.panButton.config(relief=RAISED)
self.rotateButton.config(relief=RAISED)
self.scaleButton.config(relief=RAISED)
if self.isJobRunning:
return
self.commands = reflect_toolpath(self.commands, self.workAreaSize[0]/2)
self.canvas.draw_toolpath(self.commands[0:int(self.slider.get())])
def ToggleScale(self):
self.panButton.config(relief=RAISED)
self.rotateButton.config(relief=RAISED)
self.mirrorButton.config(relief=RAISED)
if self.isJobRunning:
return
if self.scaleButton.config('relief')[-1] == SUNKEN:
self.scaleButton.config(relief=RAISED)
else:
self.scaleButton.config(relief=SUNKEN)
def GoTo(self):
if self.isJobRunning:
return
if self.gotoButton.config('relief')[-1] == SUNKEN:
self.gotoButton.config(relief=RAISED)
else:
self.gotoButton.config(relief=SUNKEN)
def StopAll(self):
serial.queue.clear()
self.JobFinished(False)
self.status.config(text="Job stopped on user demand")
def JobFinished(self, messagePopup = True):
self.isJobRunning = False
self.isJobPaused = False
self.lastSendCommandIndex = -1
self.lastMove = None
self.distanceTraveled = 0
self.currentToolChange = 0
self.currentToolPoint = 0
self.currentColor = 'black'
self.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal))
self.toolChangesLabel.config(text="%d/%d" % (self.currentToolChange, self.toolChangesTotal))
self.timeLabel.config(text="%d/%d" % (self.distancesList[self.currentToolChange]- self.distanceTraveled, self.distancesList[-1]-self.distanceTraveled))
self.startButton.config(text="Start job")
self.status.config(text="Job finished")
timeTaken = time.time() - self.start
# non blocking popup messagebox
if messagePopup:
tl = Toplevel(root)
tl.title("Job finished")
frame = Frame(tl)
frame.grid()
Label(frame, text='Current job is finished and took %s.' % time.strftime("%H hours, %M minutes, %S seconds", time.gmtime(timeTaken)) ).grid(row=0, column=0, sticky=N)
Button(frame, text="OK", command=lambda: tl.destroy(), width=10).grid(row=1, column=0)
def CanvasClick(self, event):
if self.isJobRunning:
return
self.dragStart = [event.x, event.y]
#self.transform = math.atan2(event.x, event.y)
self.transform = 0
# go to
if self.gotoButton.config('relief')[-1] == SUNKEN:
point = self.canvas.canvas_point_to_machine(self.dragStart)
serial.queue_command("G0 X%f Y%f\n" % point)
#print("Clicked at: ", self.dragStart)
def CanvasRelease(self, event):
if self.isJobRunning:
return
print ("Applied transform", self.transform)
def CanvasDrag(self, event):
if self.isJobRunning:
return
vect = (self.dragStart[0]-event.x, self.dragStart[1]-event.y)
# move event
if self.panButton.config('relief')[-1] == SUNKEN:
self.transform = self.canvas.canvas_vector_to_machine(vect)
self.commands = translate_toolpath(self.commands, self.transform)
self.canvas.draw_toolpath(self.commands[0:int(self.slider.get())])
self.dragStart[0] = event.x
self.dragStart[1] = event.y
# rotate event
if self.rotateButton.config('relief')[-1] == SUNKEN:
angle = math.atan2(vect[0], vect[1]) # atan2(y, x) or atan2(sin, cos)
self.commands = rotate_toolpath(self.commands, (self.workAreaSize[0]/2,self.workAreaSize[1]/2), -(self.transform-angle))
self.canvas.draw_toolpath(self.commands[0:int(self.slider.get())])
self.transform = angle
# scale event
if self.scaleButton.config('relief')[-1] == SUNKEN:
factor = math.sqrt((vect[0])**2 + (vect[1])**2) / 500
f = factor - self.transform
if vect[0] < 0:
f = -f
self.commands = scale_toolpath(self.commands, f)
self.canvas.draw_toolpath(self.commands[0:int(self.slider.get())])
self.transform = factor
def UpdatePath(self, val):
if self.isJobRunning:
return
self.canvas.draw_toolpath(self.commands[0:int(val)])
def GetPositionTimerTaks(self):
if self.isConnected:
def TimerCallback(response):
response = self.positionResponseRegex.search(response)
if response:
pos = (float(response.group(1)), float(response.group(2)))
self.canvas.move_pointer(pos)
serial.queue_command("M114\n", TimerCallback, priority = -1)
self.master.after(2000, self.GetPositionTimerTaks)
def CleanUp(self):
serial.close_serial()
def ToolChangePopup(self, newColor = "black"):
tl = Toplevel(root)
tl.title("Tool change")
frame = Frame(tl)
frame.grid()
canvas = Canvas(frame, width=100, height=130)
canvas.grid(row=1, column=0)
#imgvar = PhotoImage(file="pyrocket.png")
#canvas.create_image(50,70, image=imgvar)
#canvas.image = imgvar
msgbody1 = Label(frame, text="There is time to change tool for a " )
msgbody1.grid(row=1, column=1, sticky=N)
lang = Label(frame, text="next color", font=Font(size=20, weight="bold"), fg=newColor)
lang.grid(row=1, column=2, sticky=N)
msgbody2 = Label(frame, text="Resume the current job after change.")
msgbody2.grid(row=1, column=3, sticky=N)
okbttn = Button(frame, text="OK", command=lambda: tl.destroy(), width=10)
okbttn.grid(row=2, column=4)
root = Tk()
my_gui = ControlAppGUI(root)
def on_closing():
my_gui.CleanUp()
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()

Wyświetl plik

@ -0,0 +1,206 @@
import sys
import glob
import serial
import time, threading
from queue import PriorityQueue
class MyPriorityQueue(PriorityQueue):
def __init__(self):
PriorityQueue.__init__(self)
self.counter = 0
def put(self, item, priority = 1):
PriorityQueue.put(self, (priority, self.counter, item))
self.counter += 1
def get(self, *args, **kwargs):
_, _, item = PriorityQueue.get(self, *args, **kwargs)
return item
def clear(self):
while not PriorityQueue.empty(self):
try:
PriorityQueue.get(self, False)
except Empty:
continue
PriorityQueue.task_done(self)
def peek(self):
if not self.empty():
return self.queue[0][2]
else:
return None
ser = serial.Serial()
lock = threading.Lock()
worker = None
queue = MyPriorityQueue()
def serial_ports():
""" Lists serial port names
:raises EnvironmentError:
On unsupported or unknown platforms
:returns:
A list of the serial ports available on the system
"""
if sys.platform.startswith('win'):
ports = ['COM%s' % (i + 1) for i in range(256)]
elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
# this excludes your current terminal "/dev/tty"
ports = glob.glob('/dev/tty[A-Za-z]*')
elif sys.platform.startswith('darwin'):
ports = glob.glob('/dev/tty.*')
else:
raise EnvironmentError('Unsupported platform')
result = []
for port in ports:
try:
s = serial.Serial(port)
s.close()
result.append(port)
except (OSError, serial.SerialException):
pass
return result
def open_serial(port, baud, responseCallback = None):
#initialization and open the port
#possible timeout values:
# 1. None: wait forever, block call
# 2. 0: non-blocking mode, return immediately
# 3. x, x is bigger than 0, float allowed, timeout block call
ser.port = port
ser.baudrate = baud
ser.bytesize = serial.EIGHTBITS #number of bits per bytes
ser.parity = serial.PARITY_NONE #set parity check: no parity
ser.stopbits = serial.STOPBITS_ONE #number of stop bits
ser.timeout = None #block read
#ser.timeout = 1 #non-block read
#ser.timeout = 2 #timeout block read
ser.xonxoff = True #disable software flow control
ser.rtscts = False #disable hardware (RTS/CTS) flow control
ser.dsrdtr = False #disable hardware (DSR/DTR) flow control
ser.writeTimeout = 2 #timeout for write
try:
ser.open()
if responseCallback:
responseCallback(read_serial())
global worker
worker = SendingThread(None)
worker.start()
return True
except Exception as e:
msg = "error open serial port: " + str(e)
print(msg)
if responseCallback:
responseCallback(msg)
return False
def close_serial():
if ser.isOpen():
global worker
worker.terminate()
ser.close()
def send_serial(msg, responseCallback = None):
#print('sending %s \n' % msg)
if not ser.isOpen():
return False;
# when there is data in read buffer when nothing waits for it, read that
bytes_to_read = ser.inWaiting()
if not lock.locked() and bytes_to_read > 0:
junk = ser.read(bytes_to_read)
#print(junk)
# critical section
with lock:
ser.write(msg)
response = read_serial()
if responseCallback:
responseCallback(response)
print("response for %s : %s" % (msg, response))
if "err" in response:
return False
return True
def read_serial():
if not ser.isOpen():
return
response = bytes()
bytes_to_read = ser.inWaiting()
# wait for response to be send from device
while bytes_to_read < 2:
time.sleep(0.05)
bytes_to_read = ser.inWaiting()
response += ser.read()
responseStr = response.decode("ascii")
if not ("ok" in responseStr or "err" in responseStr or "\n" in responseStr):
time.sleep(0.05)
# make sure there is no data left,
#readline or substring check may lock program when response is corrupted
bytes_to_read = ser.inWaiting()
while bytes_to_read > 0:
response += ser.read()
bytes_to_read = ser.in_waiting
# for low bauds only, make sure there are no more bytes processing
if(bytes_to_read <= 0 and ser.baudrate < 38400):
time.sleep(0.2)
bytes_to_read = ser.inWaiting()
response = response.decode("ascii")
return response
def queue_command_list(msgs):
for msg in msgs:
queue_command(msg)
def queue_command(msg, responseCallback = None, priority = None):
new_entry = ((msg.encode()), responseCallback)
if priority:
if priority < 0:
peek_item = queue.peek()
# don't queue high priority same command again
if peek_item and peek_item[0] == new_entry[0]:
return
queue.put(new_entry, priority)
else:
queue.put(new_entry)
if __name__ == '__main__':
print(serial_ports())
class SendingThread(threading.Thread):
def __init__(self, parent):
"""
@param parent: The gui object that should recieve the value
@param value: deque object list of tuples commands strings to send with callbacks
"""
threading.Thread.__init__(self)
self._parent = parent
self.running = True
def run(self):
while self.running:
try:
# do not wait infinity, use timeout
msg = queue.get(True, 2)
except:
# timeout on empty queue rises exception
continue
send_serial(msg[0], msg[1])
queue.task_done()
def terminate(self):
self.running = False

Wyświetl plik

@ -0,0 +1,116 @@
import time, threading, re
from queue import PriorityQueue
class MyPriorityQueue(PriorityQueue):
def __init__(self):
PriorityQueue.__init__(self)
self.counter = 0
def put(self, item, priority = 1):
PriorityQueue.put(self, (priority, self.counter, item))
self.counter += 1
def get(self, *args, **kwargs):
_, _, item = PriorityQueue.get(self, *args, **kwargs)
return item
def clear(self):
while not PriorityQueue.empty(self):
try:
PriorityQueue.get(self, False)
except Empty:
continue
PriorityQueue.task_done(self)
worker = None
last_pos = [0.0,0.0]
gcode_regexX = re.compile("X(\-?\d+\.?\d+)")
gcode_regexY = re.compile("Y(\-?\d+\.?\d+)")
queue = MyPriorityQueue()
def serial_ports():
""" Lists serial port names
:raises EnvironmentError:
On unsupported or unknown platforms
:returns:
A list of the serial ports available on the system
"""
return ["/dev/ttyUSB0"]
def open_serial(port, baud, responseCallback = None):
print(port, baud)
if responseCallback:
responseCallback("Connection establish")
global worker
worker = SendingThread(None)
worker.start()
return True
def close_serial():
global worker
if worker:
worker.terminate()
def send_serial(msg, responseCallback = None):
print(msg)
time.sleep(0.1)
global last_pos
response = "ok\n"
if "M114" in msg:
response = "X%f Y%f\nok\n" % (last_pos[0],last_pos[1])
if "G0" in msg or "G1" in msg:
regex_result = gcode_regexX.search(msg)
if regex_result:
last_pos[0] = float(regex_result.group(1))
regex_result = gcode_regexY.search(msg)
if regex_result:
last_pos[1] = float(regex_result.group(1))
if responseCallback:
responseCallback(response)
return True
def read_serial():
return "OK"
def queue_command_list(msgs):
pass
def queue_command(msg, responseCallback = None, priority = None):
global queue
new_entry = (msg, responseCallback)
if priority:
queue.put(new_entry, priority)
else:
queue.put(new_entry)
if __name__ == '__main__':
print(serial_ports())
class SendingThread(threading.Thread):
def __init__(self, parent):
"""
@param parent: The gui object that should recieve the value
@param value: deque object list of tuples commands strings to send with callbacks
"""
threading.Thread.__init__(self)
self._parent = parent
self.running = True
def run(self):
while self.running:
try:
# do not wait infinity, use timeout
msg = queue.get(True, 2)
except:
# timeout on empty queue rises exception
continue
send_serial(msg[0], msg[1])
queue.task_done()
def terminate(self):
self.running = False

Wyświetl plik

@ -0,0 +1,265 @@
from tkinter import Canvas, ALL
from operator import itemgetter
import copy, math, re
# a subclass of Canvas for dealing with resizing of windows
class ResizingCanvas(Canvas):
def __init__(self,parent, area_width=100, area_height=100, **kwargs):
Canvas.__init__(self,parent,**kwargs)
parent.bind("<Configure>", self.on_resize)
self.parent = parent
self.height = self.winfo_reqheight()
self.width = self.winfo_reqwidth()
self.setArea(area_width, area_height)
self.pointer = self.create_oval(0, 0, 4, 4)
def setArea(self, area_width, area_height):
self.aspect_ratio = area_width / area_height
# these vars are for work area setup for machine, in mm's
self.area_height = area_height
self.area_width = area_width
self.height_ratio = self.height / self.area_height
self.width_ratio = self.width / self.area_width
def on_resize(self,event):
# start by using the width as the controlling dimension
desired_width = event.width
desired_height = int(event.width / self.aspect_ratio)
# if the window is too tall to fit, use the height as
# the controlling dimension
if desired_height > event.height:
desired_height = event.height
desired_width = int(event.height * self.aspect_ratio)
# determine the ratio of old width/height to new width/height
wscale = float(desired_width)/self.width
hscale = float(desired_height)/self.height
self.width = desired_width
self.height = desired_height
self.height_ratio = self.height / self.area_height
self.width_ratio = self.width / self.area_width
# resize the canvas
self.config(width=self.width, height=self.height)
#self.parent.config(width=self.width, height=self.height)
# rescale all the objects tagged with the "all" tag
self.scale("all",0,0,wscale,hscale)
# print(self.width, self.height)
def clear(self):
self.delete(ALL)
self.pointer = self.create_oval(0, 0, 4, 4)
# gcode coordinates into canvas coords
# takes tuple (x1, y1, x2, y2)
def calc_coords(self, coords):
x1 = coords[0]*self.width_ratio
y1 = self.height - coords[1]*self.height_ratio
x2 = coords[2]*self.width_ratio
y2 = self.height - coords[3]*self.height_ratio
return int(x1), int(y1), int(x2), int(y2)
def canvas_vector_to_machine(self, point):
return (-point[0]/self.width_ratio, point[1]/self.height_ratio)
def canvas_point_to_machine(self, point):
return (point[0]/self.width_ratio, (self.height - point[1])/self.height_ratio)
def machine_point_to_canvas(self, point):
return (point[0]*self.width_ratio, self.height - point[1]*self.height_ratio)
# coords is a 2 element tuple (x1, y1)
def move_pointer(self, point):
x1, y1 = self.machine_point_to_canvas(point)
self.coords(self.pointer, (x1-2, y1-2, x1+2, y1+2)) # change coordinates
# takes a list of points as tuples: (x,y,color)
def draw_toolpath(self, points):
self.clear()
if(len(points) < 2):
return
last_point = points[0]
current_color = "black"
color = current_color
for point in points[1:]:
if "G0" in point[0]:
color = "snow2"
elif "M6" in point[0]:
current_color = _from_rgb((point[1], point[2], point[3]))
continue
coord = (last_point[1], last_point[2], point[1], point[2])
last_point = point
line = self.create_line(self.calc_coords(coord), fill=color)
self.lift(self.pointer, line)
color = current_color
#self.move_pointer((0,0,0,0))
# moves list of commands (points) along vector
def translate_toolpath(points, translate=(0,0)):
#new_points = copy.deepcopy(points)
for point in points:
if not ("G0" in point[0] or "G1" in point[0]):
continue
point[1] += translate[0]
point[2] += translate[1]
return points
def rotate_toolpath(points, origin, theta):
for point in points:
if not ("G0" in point[0] or "G1" in point[0]):
continue
point[1], point[2] = rotate(point[1:3], origin, theta)
return points
def rotate(point, origin, angle):
"""
Rotate a point counterclockwise by a given angle around a given origin.
The angle should be given in radians.
"""
ox, oy = origin
px, py = point
qx = ox + math.cos(angle) * (px - ox) - math.sin(angle) * (py - oy)
qy = oy + math.sin(angle) * (px - ox) + math.cos(angle) * (py - oy)
return qx, qy
def reflect_toolpath(points, d):
for point in points:
if not ("G0" in point[0] or "G1" in point[0]):
continue
point[1] = 2*d - point[1]
return points
def scale_toolpath(points, f):
for point in points:
if not ("G0" in point[0] or "G1" in point[0]):
continue
point[1] += point[1]*f
point[2] += point[2]*f
return points
def toolpath_border_points(points):
points = [elem for elem in points if "G0" in elem[0] or "G1" in elem[0]]
x_max = max(points,key=itemgetter(1))[1]
x_min = min(points,key=itemgetter(1))[1]
y_max = max(points,key=itemgetter(2))[2]
y_min = min(points,key=itemgetter(2))[2]
return ((x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max), (x_min, y_min))
def toolpath_info(points):
''' returns info such as total number of tool points, number of tool changes, tool distances between tool changes'''
last_point = None
total_distance = 0
tool_changes = 0
distances = []
for point in points:
if not ("G0" in point[0] or "G1" in point[0]):
if "M6" in point[0]:
tool_changes += 1
distances.append(total_distance)
continue
if last_point:
total_distance += math.hypot(point[1] - last_point[1], point[2] - last_point[2])
last_point = point
distances.append(total_distance)
return sum(el[0] == "G1" for el in points), tool_changes, distances
def load_csv_file(csvfile, offset=(0,0)):
''' loads csv textfile, takes opended file handler and toolpath offset
returns list of commands for machine'''
import csv
#dialect = csv.Sniffer().sniff(csvfile.read(1024))
#csvfile.seek(0)
#reader = csv.reader(csvfile, dialect)
reader = csv.reader(csvfile, delimiter=',')
command = "G0"
commands = [("G28",0,0)]
colors = []
# load each point into commands list
for row in reader:
# empyt row
if len(row) <= 0:
continue
if '*' == row[0]:
if 'JUMP' in row[1]:
command = "G0"
elif 'STITCH' in row[1]:
command = "G1"
elif 'COLOR' in row[1]:
command = "M6"
if len(colors) > 0:
r, g, b = colors.pop(0)
else:
r = g = b = 0
commands.append([command, r, g, b])
continue
elif 'TRIMM' in row[1]:
command = "G12"
commands.append([command, ])
continue
elif 'END' in row[1]:
continue
commands.append([command, float(row[2].replace(",","."))+offset[0], float(row[3].replace(",","."))+offset[1]])
elif '$' == row[0]:
colors.append((int(row[2]), int(row[3]), int(row[4])))
#print(', '.join(row))
csvfile.close()
#return [("G28", 0,0),("G0",100,100),("G1", 110,150)]
return commands
def save_csv_file(f, commands):
pass
def load_gcode_file(f):
result = []
regexNumber = re.compile("(\-?\d+\.?\d+)")
regexGcode = re.compile("([G,M]\d+)\s+?([X,Y,F,R,G,B]\-?\d+\.?\d+)?\s+?([X,Y,F,R,G,B,F]\-?\d+\.?\d+)?\s+?([X,Y,F,R,G,B]\-?\d+\.?\d+)?")
line = f.readline()
while line:
line = line.upper()
regexResult = regexGcode.search(line)
if regexResult:
params = (regexResult.group(2), regexResult.group(3), regexResult.group(4))
command = [regexResult.group(1), 0, 0]
for param in params:
if not param:
continue
if "X" in param:
command[1] = float(regexNumber.search(param).group(1))
if "Y" in param:
command[2] = float(regexNumber.search(param).group(1))
if "F" in param:
command.append(float(regexNumber.search(param).group(1)))
if "R" in param:
command[1] = int(regexNumber.search(param).group(1))
if "G" in param:
command[2] = int(regexNumber.search(param).group(1))
if "B" in param:
command.append(int(regexNumber.search(param).group(1)))
result.append(command)
line = f.readline()
return result
def save_gcode_file(f, commands):
for command in commands:
if "M6" in command[0]:
command = "%s R%d G%d B%d\n" % (command[0], command[1], command[2], command[3])
elif "G0" in command[0] or "G1" in command[0]:
if len(command) > 3:
command = "%s X%f Y%f F%f\n" % (command[0], command[1], command[2], command[3])
else:
command = "%s X%f Y%f\n" % (command[0], command[1], command[2])
else:
command = "%s\n" % command[0]
f.write(command)
f.close()
def _from_rgb(rgb):
"""translates an rgb tuple of int to a tkinter friendly color code
"""
return "#%02x%02x%02x" % rgb

Wyświetl plik

@ -0,0 +1,10 @@
screw_hole_dia = 3.5;
height = 12;
difference(){
translate([-10,-5,0]) cube([20,10,height]);
cylinder(r=screw_hole_dia / 2, h = height);
translate([0,0,3]) cylinder(r=screw_hole_dia * 0.95, h = height);
}
for (i=[1:2:height])
translate([-10,4+i/10,i]) mirror([0,0,1]) rotate([0,90,0]) cylinder(r=1.8, h=20, $fn = 2);

Wyświetl plik

@ -0,0 +1,98 @@
use <polyround.scad>;
// NOTE: consider losses on round corners when setting workarea dimensions
workareaWidth = 130;
workareaHeight = 150;
frameHeight = 8;
innerFrameThickness = 5;
outerFrameThickness = 9;
innerOuterFramesSpacing = 0.5;
mountingHolesSpacing = 40;
// print with infill above 50% for better stiffness
// this default workarea 130x150 needes 200x200 printer platform space
hoop();
module hoop(){
// inner frame
innerFrameWidth = workareaWidth+innerFrameThickness;
innerFrameHeight = workareaHeight+innerFrameThickness;
linear_extrude(height = 1.5) difference(){
roundRectangle(innerFrameWidth, innerFrameHeight, center=true);
intersection(){
roundRectangle(workareaWidth, workareaHeight, center=true);
square(size=[workareaWidth, workareaHeight], center=true);
}
}
linear_extrude(height = frameHeight) difference(){
roundRectangle(innerFrameWidth, innerFrameHeight, center=true);
roundRectangle(workareaWidth, workareaHeight, center=true);
}
// outer frame
spacing = innerOuterFramesSpacing*2;
outerFrameWidth = innerFrameWidth+outerFrameThickness+spacing;
outerFrameHeight = innerFrameHeight+outerFrameThickness+spacing;
difference(){
union(){
linear_extrude(height = frameHeight) difference(){
roundRectangle(outerFrameWidth , outerFrameHeight, center=true);
roundRectangle(innerFrameWidth+spacing, innerFrameHeight+spacing, center=true);
}
// tightening handle
// adjust magic number divider to elminate gap between frame and handle
translate([-8,outerFrameHeight/28+outerFrameHeight/2,0])
difference(){
cube([20,12,frameHeight]);
//screw hole
translate([20,8,frameHeight/2]) rotate([0,-90,0]) {cylinder(r=1.8, h=20); cylinder(r=3.5*0.95, h=3, $fn=6);}
}
}
translate([-1,0,0]) cube([2,innerFrameHeight,frameHeight]);
}
// outer frame handle
translate([outerFrameWidth/15+ innerFrameWidth/2,0,0]) {
difference(){
union(){
translate([0,-20,0]) cube([40,40,frameHeight/2]);
translate([36,-(mountingHolesSpacing+10)/2,0]) hull(){
cube([4,mountingHolesSpacing+10,6+frameHeight/2]);
translate([-10,0,0])cube([10,mountingHolesSpacing+10,1]);
}
}
// mounting holes
for(i=[1,-1])
translate([0,i*mountingHolesSpacing/2,3+frameHeight/2]) rotate([0,90,0]){
cylinder(r=5.5, h=36); cylinder(r=1.8, h=41);
}
}
}
}
module roundRectangle(width=20, height=25, center = false)
{
// those two values bellow affects roundness of the frames
divider = 15;
cornerR = min([width,height])/5;
points=[
[0, 0, cornerR],
[-width/divider, height/2, height*2],
[0, height, cornerR],
[width/2, height+height/divider, width*2],
[width, height, cornerR],
[width+width/divider, height/2, height*2],
[width, 0, cornerR],
[width/2, -height/divider, width*2]
];
if(center){
translate([-width/2, -height/2,0]) polygon(polyRound(points,5));
}
else{
polygon(polyRound(points,5));
}
}

Wyświetl plik

@ -0,0 +1,23 @@
mountingHolesSpacing = 40;
mountingHolesDia = 3.5;
width = mountingHolesSpacing+10;
totalSpacing = 20;
// print with support material, 30-50% infill
rotate([90,0,0])
difference(){
union(){
translate([totalSpacing-8,-width/2,17]) cube([8,width,8]);
translate([0,-width/4,17]) cube([16,width/2,8]);
translate([0,-width/2,14]) cube([totalSpacing,width,3]);
translate([0,-width/2,0]) cube([8,width,15]);
}
for(i=[1,-1])
translate([0,i*mountingHolesSpacing/2,21]) rotate([0,90,0]){
cylinder(r=5.5, h=totalSpacing-4); cylinder(r=mountingHolesDia/2, h=totalSpacing);
}
// tool holder holes
for(i=[1,-1])
translate([8,i*mountingHolesSpacing/2,6]) rotate([0,-90,0]){cylinder(r=mountingHolesDia/2, h=41); cylinder(r=mountingHolesDia*0.95, h=3, $fn = 6);}
}

Wyświetl plik

@ -0,0 +1,14 @@
knoobHeight = 20;
rodDia = 3.5;
difference(){
union(){
for(i=[0:24:360])
rotate([0,0,i]) translate([rodDia*0.8,0,0]) cylinder(r1=2, r2=1.2, h=knoobHeight, $fn = 3);
cylinder(r=rodDia-0.2,h=knoobHeight);
cylinder(r=rodDia*1.4,h=3);
}
cylinder(r=rodDia/2,h=knoobHeight);
cylinder(r=rodDia*0.95,h=3, $fn=6);
}

Wyświetl plik

@ -0,0 +1,56 @@
encoder_wheel_axial_hole_dia = 9.3;
encoder_blades_no = 10;
encoder_wheel_dia = encoder_wheel_axial_hole_dia*1.6 + 10;
encoder_mount_screw_dia = 5;
encoder_wheel();
//encoder_mount();
// this part was designed to fit into particular machine model, probably you need to redesign this
module encoder_mount()
{
rotate([0,-90,45]) linear_extrude(height=2) polygon([[0,4],[0,28],[13,28]]);
difference(){
translate([-22,18,0]) cube([5,4,31]);
// holes
translate([-20,22,18]) rotate([90,0,0]) {
cylinder(r=1.0, h=20);
translate([0,11,0]) cylinder(r=1.0, h=20);
}
}
difference(){
hull(){
for (i = [1,-1])
translate([i*2,0,0]) cylinder(r=encoder_mount_screw_dia, h=2);
translate([-22,16,0]) cube([5,6,2]);
}
hull(){
for (i = [1,-1])
translate([i*2,0,0]) cylinder(r=encoder_mount_screw_dia/2, h=2);
}
}
}
module encoder_wheel(){
blade_angle = 360 / encoder_blades_no/2;
difference(){
union(){
for(i=[0:blade_angle:360])
rotate([0,0,i*2]) encoder_blade(blade_angle);
cylinder(r=encoder_wheel_axial_hole_dia*0.8, h = 8);
}
cylinder(r=encoder_wheel_axial_hole_dia/2, h = 10);
translate([encoder_wheel_axial_hole_dia-7.5,-3,0]) cube([4,6,10]);
}
}
module encoder_blade(angle = 10)
intersection(){
difference(){
cylinder(r=encoder_wheel_dia/2, h = 1);
translate([0,-encoder_wheel_dia,0]) cube([encoder_wheel_dia, encoder_wheel_dia*2, 1]);
}
rotate([0,0,angle]) cube([encoder_wheel_dia, encoder_wheel_dia, 1]);
};

Wyświetl plik

@ -0,0 +1,60 @@
needle_shaft_dia = 5;
mount_shaft_dia = 7.3;
//flag2();
sensor_mount();
module sensor_mount(){
holes_spacing = 15;
difference(){
union(){
translate([-(5+holes_spacing)/2,0,0]){ cube([holes_spacing+5, 13,2]); cube([holes_spacing+5, 8,mount_shaft_dia/2+1]);}
// mounting clamp
translate([-10,4,9]) rotate([180,0,0]) rotate([0,90,0])
difference(){
cylinder(r=mount_shaft_dia*0.9, h=20);
cylinder(r=mount_shaft_dia/2, h=20);
translate([-mount_shaft_dia/2+1,0,0]) cube([mount_shaft_dia-2,mount_shaft_dia,20]);
//holes
for(i=[0,7, 14])
translate([0,-mount_shaft_dia-0.5,3+i]) rotate([-90,0,0]) {cylinder(r=3.5,h=2); cylinder(r=1.5,h=8);}
}
}
//screws holes
for (i=[holes_spacing/2,-holes_spacing/2])
translate([i,4,0]) cylinder(r=1.2, h=5);
//keys holes
for (i=[0,6])
translate([0,4+i,0]) cylinder(r=1.5, h=2, $fn=8);
}
}
module flag2(){
translate([-4,-7,-10]) {cube([8, 5, 10]); cube([16, 5, 1]);}
difference(){
union(){
translate([-4,-7,0]) cube([8, 10, 1]);
translate([-6,-7,0]) cube([12, 5, 5]);
}
translate([-4,-7,1]) cube([8, 10, 5]);
cylinder(r=1.2, h=2, $fn=8);
}
}
module flag1(){
difference(){
union(){
translate([-needle_shaft_dia,0,0]) cube([10,needle_shaft_dia+1,4]);
cylinder(r=needle_shaft_dia, h=4);
//flag
translate([needle_shaft_dia,0,0]) {cube([5,6,4]); translate([5,0,0]) cube([1,15,6]);}
}
cylinder(r=needle_shaft_dia/2, h=4);
translate([-2,0,0]) cube([4,needle_shaft_dia+5,4]);
translate([-needle_shaft_dia,needle_shaft_dia/2+1.5,2]) rotate([0,90,0]) cylinder(r=1.0, h=15);
}
}

Wyświetl plik

@ -0,0 +1,75 @@
// distance from end to rod beginning (contact)
distance = 47;
// default value 20 is fine
column_height = 20;
// print both parts with 40-50% infill
mount_plotter();
//translate ([52,0,0])
translate ([52,5,0])
mount_clamp();
module screw_hole(d=3.5, fn=10, pocket = false)
{
cylinder(r=d/2, h = 20);
translate([0,0,-7]){
cylinder(r=d*0.95, h = 10, $fn=fn);
if (pocket)
{
translate([-d,-20,0]) cube([2*d,20,8]);
}
}
}
rod_r = 10.5 / 2;
module mount_clamp()
{
//translate([0,0,0]) cylinder(r=rod_r, h=column_height);
translate([-8,11.5,0]) difference(){
cube([10, 6, column_height]);
translate([3,-2.5,0]) rotate([0,0,8]) cube([5, 5, column_height]);
}
difference(){
translate([2,-12,0]) cube([8, 29.5, column_height]);
// rod hole
translate([0,0,0]) cylinder(r=rod_r*sqrt(2), h=column_height, $fn = 4);
// screw hole
translate([8,-8,column_height/2]) rotate([0,-90,0]) screw_hole();
}
}
module mount_plotter()
{
difference(){
mount_column();
translate([10,0,column_height+4]) hull(){ sphere(r=8); translate([distance-22,0,0]) sphere(r=8); };
}
}
module mount_column(){
base_width = 40;
difference() {
// base
translate([0,-base_width/2,0]) hull(){
cube([5, base_width,column_height ]);
translate([10,base_width/4,0]) cube([5, base_width/2 ,column_height ]);
}
// base screw holes
for( i = [1,-1])
translate([7,i*(base_width-10)/2, column_height/2]) rotate([180,90,0]) screw_hole();
}
difference(){
union(){
// column
translate([0,-11,0]) cube([distance+rod_r-sqrt(2), 22,column_height]);
// tooth
translate([distance+1,0,0]) rotate([0,0,5]) cube([3.5, 14,column_height]);
translate([distance-1.5,-column_height/6,column_height/2]) rotate([0,90,0]) cylinder(r=column_height/2, h = 5);
}
// rod hole
translate([distance+rod_r,0,0]) cylinder(r=rod_r, h=column_height, $fn=8);
translate([distance-3,-8,column_height/2]) rotate([0,90,0]) screw_hole(fn=6, pocket = true);
}
}

Wyświetl plik

@ -0,0 +1,663 @@
// Library: round-anything
// Version: 1.0
// Author: IrevDev
// Contributors: TLC123
// Copyright: 2017
// License: GPL 3
//examples();
module examples(){
//Example of how a parametric part might be designed with this tool
width=20; height=25;
slotW=8; slotH=15;
slotPosition=8;
minR=1.5; farcornerR=6;
internalR=3;
points=[
[0, 0, farcornerR],
[0, height, minR],
[slotPosition, height, minR],
[slotPosition, height-slotH, internalR],
[slotPosition+slotW, height-slotH, internalR],
[slotPosition+slotW, height, minR],
[width, height, minR],
[width, 0, minR]
];
points2=[
[0, 0, farcornerR],
["l", height, minR],
[slotPosition, "l", minR],
["l", height-slotH, internalR],
[slotPosition+slotW, "l", internalR],
["l", height, minR],
[width, "l", minR],
["l", height*0.2, minR],
[45, 0, minR+5, "ayra"]
];//,["l",0,minR]];
echo(processRadiiPoints(points2));
translate([-25,0,0]){
polygon(polyRound(points,5));
}
%translate([-25,0,0.2]){
polygon(getpoints(points));//transparent copy of the polgon without rounding
}
translate([-50,0,0]){
polygon(polyRound(points2,5));
}
%translate([-50,0,0.2]){
polygon(getpoints(processRadiiPoints(points2)));//transparent copy of the polgon without rounding
}
//Example of features 2
// 1 2 3 4 5 6
b=[[-4,0,1],[5,3,1.5],[0,7,0.1],[8,7,10],[20,20,0.8],[10,0,10]]; //points
polygon(polyRound(b,30));/*polycarious() will make the same shape but doesn't have radii conflict handling*/ //polygon(polycarious(b,30));
%translate([0,0,0.3])polygon(getpoints(b));//transparent copy of the polgon without rounding
//Example of features 3
// 1 2 3 4 5 6
p=[[0,0,1.2],[0,20,1],[15,15,1],[3,10,3],[15,0,1],[6,2,10]];//points
a=polyRound(p,5);
translate([25,0,0]){
polygon(a);
}
%translate([25,0,0.2]){
polygon(getpoints(p));//transparent copy of the polgon without rounding
}
//example of radii conflict handling and debuging feature
r1a=10; r1b=10;
r2a=30; r2b=30;
r3a=10; r3b=40;
r4a=15; r4b=20;
c1=[[0,0,0],[0,20,r1a],[20,20,r1b],[20,0,0]];//both radii fit and don't need to be changed
translate([-25,-30,0]){
polygon(polyRound(c1,8));
}
echo(str("c1 debug= ",polyRound(c1,8,mode=1)," all zeros indicates none of the radii were reduced"));
c2=[[0,0,0],[0,20,r2a],[20,20,r2b],[20,0,0]];//radii are too large and are reduced to fit
translate([0,-30,0]){
polygon(polyRound(c2,8));
}
echo(str("c2 debug= ",polyRound(c2,8,mode=1)," 2nd and 3rd radii reduced by 20mm i.e. from 30 to 10mm radius"));
c3=[[0,0,0],[0,20,r3a],[20,20,r3b],[20,0,0]];//radii are too large again and are reduced to fit, but keep their ratios
translate([25,-30,0]){
polygon(polyRound(c3,8));
}
echo(str("c3 debug= ",polyRound(c3,8,mode=1)," 2nd and 3rd radii reduced by 6 and 24mm respectively"));
//resulting in radii of 4 and 16mm,
//notice the ratio from the orginal radii stays the same r3a/r3b = 10/40 = 4/16
c4=[[0,0,0],[0,20,r4a],[20,20,r4b],[20,0,0]];//radii are too large again but not corrected this time
translate([50,-30,0]){
polygon(polyRound(c4,8,mode=2));//mode 2 = no radii limiting
}
//example of rounding random points, this has no current use but is a good demonstration
random=[for(i=[0:20])[rnd(0,50),rnd(0,50),/*rnd(0,30)*/1000]];
R =polyRound(random,7);
translate([-25,25,0]){
polyline(R);
}
//example of different modes of the CentreN2PointsArc() function 0=shortest arc, 1=longest arc, 2=CW, 3=CCW
p1=[0,5];p2=[10,5];centre=[5,0];
translate([60,0,0]){
color("green"){
polygon(CentreN2PointsArc(p1,p2,centre,0,20));//draws the shortest arc
}
color("cyan"){
polygon(CentreN2PointsArc(p1,p2,centre,1,20));//draws the longest arc
}
}
translate([75,0,0]){
color("purple"){
polygon(CentreN2PointsArc(p1,p2,centre,2,20));//draws the arc CW (which happens to be the short arc)
}
color("red"){
polygon(CentreN2PointsArc(p2,p1,centre,2,20));//draws the arc CW but p1 and p2 swapped order resulting in the long arc being drawn
}
}
radius=6;
radiipoints=[[0,0,0],[10,20,radius],[20,0,0]];
tangentsNcen=round3points(radiipoints);
translate([100,0,0]){
for(i=[0:2]){
color("red")translate(getpoints(radiipoints)[i])circle(1);//plots the 3 input points
color("cyan")translate(tangentsNcen[i])circle(1);//plots the two tangent poins and the circle centre
}
translate([tangentsNcen[2][0],tangentsNcen[2][1],-0.2])circle(r=radius,$fn=25);//draws the cirle
%polygon(getpoints(radiipoints));//draws a polygon
}
//for(i=[0:len(b2)-1]) translate([b2[i].x,b2[i].y,2])#circle(0.2);
ex=[[0,0,-1],[2,8,0],[5,4,3],[15,10,0.5],[10,2,1]];
translate([15,-50,0]){
ang=55;
minR=0.2;
rotate([0,0,ang+270])translate([0,-5,0])square([10,10],true);
clipP=[[9,1,0],[9,0,0],[9.5,0,0],[9.5,1,0.2],[10.5,1,0.2],[10.5,0,0],[11,0,0],[11,1,0]];
a=RailCustomiser(ex,o1=0.5,minR=minR,a1=ang-90,a2=0,mode=2);
b=revList(RailCustomiser(ex,o1=-0.5,minR=minR,a1=ang-90,a2=0,mode=2));
points=concat(a,clipP,b);
points2=concat(ex,clipP,b);
polygon(polyRound(points,20));
//%polygon(polyRound(points2,20));
}
//the following exapmle shows how the offsets in RailCustomiser could be used to makes shells
translate([-20,-60,0]){
for(i=[-9:0.5:1])polygon(polyRound(RailCustomiser(ex,o1=i-0.4,o2=i,minR=0.1),20));
}
// This example shows how a list of points can be used multiple times in the same
nutW=5.5; nutH=3; boltR=1.6;
minT=2; minR=0.8;
nutCapture=[
[-boltR, 0, 0],
[-boltR, minT, 0],
[-nutW/2, minT, minR],
[-nutW/2, minT+nutH, minR],
[nutW/2, minT+nutH, minR],
[nutW/2, minT, minR],
[boltR, minT, 0],
[boltR, 0, 0],
];
aSquare=concat(
[[0,0,0]],
moveRadiiPoints(nutCapture,tran=[5,0],rot=0),
[[20,0,0]],
moveRadiiPoints(nutCapture,tran=[20,5],rot=90),
[[20,10,0]],
[[0,10,0]]
);
echo(aSquare);
translate([40,-60,0]){
polygon(polyRound(aSquare,20));
translate([10,12,0])polygon(polyRound(nutCapture,20));
}
translate([70,-52,0]){
a=mirrorPoints(ex,0,[1,0]);
polygon(polyRound(a,20));
}
translate([0,-90,0]){
r_extrude(3,0.5*$t,0.5*$t,100)polygon(polyRound(b,30));
#translate([7,4,3])r_extrude(3,-0.5,0.95,100)circle(1,$fn=30);
}
translate([-30,-90,0])
shell2d(-0.5,0,0)polygon(polyRound(b,30));
}
function polyRound(radiipoints,fn=5,mode=0)=
/*Takes a list of radii points of the format [x,y,radius] and rounds each point
with fn resolution
mode=0 - automatic radius limiting - DEFAULT
mode=1 - Debug, output radius reduction for automatic radius limiting
mode=2 - No radius limiting*/
let(
getpoints=mode==2?1:2,
p=getpoints(radiipoints), //make list of coordinates without radii
Lp=len(p),
//remove the middle point of any three colinear points
newrp=[
for(i=[0:len(p)-1]) if(isColinear(p[wrap(i-1,Lp)],p[wrap(i+0,Lp)],p[wrap(i+1,Lp)])==0||p[wrap(i+0,Lp)].z!=0)radiipoints[wrap(i+0,Lp)]
],
newrp2=processRadiiPoints(newrp),
temp=[
for(i=[0:len(newrp2)-1]) //for each point in the radii array
let(
thepoints=[for(j=[-getpoints:getpoints])newrp2[wrap(i+j,len(newrp2))]],//collect 5 radii points
temp2=mode==2?round3points(thepoints,fn):round5points(thepoints,fn,mode)
)
mode==1?temp2:newrp2[i][2]==0?
[[newrp2[i][0],newrp2[i][1]]]: //return the original point if the radius is 0
CentreN2PointsArc(temp2[0],temp2[1],temp2[2],0,fn) //return the arc if everything is normal
]
)
[for (a = temp) for (b = a) b];//flattern and return the array
function round5points(rp,fn,debug=0)=
rp[2][2]==0&&debug==0?[[rp[2][0],rp[2][1]]]://return the middle point if the radius is 0
rp[2][2]==0&&debug==1?0://if debug is enabled and the radius is 0 return 0
let(
p=getpoints(rp), //get list of points
r=[for(i=[1:3]) abs(rp[i][2])],//get the centre 3 radii
//start by determining what the radius should be at point 3
//find angles at points 2 , 3 and 4
a2=cosineRuleAngle(p[0],p[1],p[2]),
a3=cosineRuleAngle(p[1],p[2],p[3]),
a4=cosineRuleAngle(p[2],p[3],p[4]),
//find the distance between points 2&3 and between points 3&4
d23=pointDist(p[1],p[2]),
d34=pointDist(p[2],p[3]),
//find the radius factors
F23=(d23*tan(a2/2)*tan(a3/2))/(r[0]*tan(a3/2)+r[1]*tan(a2/2)),
F34=(d34*tan(a3/2)*tan(a4/2))/(r[1]*tan(a4/2)+r[2]*tan(a3/2)),
newR=min(r[1],F23*r[1],F34*r[1]),//use the smallest radius
//now that the radius has been determined, find tangent points and circle centre
tangD=newR/tan(a3/2),//distance to the tangent point from p3
circD=newR/sin(a3/2),//distance to the circle centre from p3
//find the angle from the p3
an23=getAngle(p[1],p[2]),//angle from point 3 to 2
an34=getAngle(p[3],p[2]),//angle from point 3 to 4
//find tangent points
t23=[p[2][0]-cos(an23)*tangD,p[2][1]-sin(an23)*tangD],//tangent point between points 2&3
t34=[p[2][0]-cos(an34)*tangD,p[2][1]-sin(an34)*tangD],//tangent point between points 3&4
//find circle centre
tmid=getMidpoint(t23,t34),//midpoint between the two tangent points
anCen=getAngle(tmid,p[2]),//angle from point 3 to circle centre
cen=[p[2][0]-cos(anCen)*circD,p[2][1]-sin(anCen)*circD]
)
//circle center by offseting from point 3
//determine the direction of rotation
debug==1?//if debug in disabled return arc (default)
(newR-r[1]):
[t23,t34,cen];
function round3points(rp,fn)=
rp[1][2]==0?[[rp[1][0],rp[1][1]]]://return the middle point if the radius is 0
let(
p=getpoints(rp), //get list of points
r=rp[1][2],//get the centre 3 radii
ang=cosineRuleAngle(p[0],p[1],p[2]),//angle between the lines
//now that the radius has been determined, find tangent points and circle centre
tangD=r/tan(ang/2),//distance to the tangent point from p2
circD=r/sin(ang/2),//distance to the circle centre from p2
//find the angles from the p2 with respect to the postitive x axis
a12=getAngle(p[0],p[1]),//angle from point 2 to 1
a23=getAngle(p[2],p[1]),//angle from point 2 to 3
//find tangent points
t12=[p[1][0]-cos(a12)*tangD,p[1][1]-sin(a12)*tangD],//tangent point between points 1&2
t23=[p[1][0]-cos(a23)*tangD,p[1][1]-sin(a23)*tangD],//tangent point between points 2&3
//find circle centre
tmid=getMidpoint(t12,t23),//midpoint between the two tangent points
angCen=getAngle(tmid,p[1]),//angle from point 2 to circle centre
cen=[p[1][0]-cos(angCen)*circD,p[1][1]-sin(angCen)*circD] //circle center by offseting from point 2
)
[t12,t23,cen];
function parallelFollow(rp,thick=4,minR=1,mode=1)=
//rp[1][2]==0?[rp[1][0],rp[1][1],0]://return the middle point if the radius is 0
thick==0?[rp[1][0],rp[1][1],0]://return the middle point if the radius is 0
let(
p=getpoints(rp), //get list of points
r=thick,//get the centre 3 radii
ang=cosineRuleAngle(p[0],p[1],p[2]),//angle between the lines
//now that the radius has been determined, find tangent points and circle centre
tangD=r/tan(ang/2),//distance to the tangent point from p2
sgn=CWorCCW(rp),//rotation of the three points cw or ccw?let(sgn=mode==0?1:-1)
circD=mode*sgn*r/sin(ang/2),//distance to the circle centre from p2
//find the angles from the p2 with respect to the postitive x axis
a12=getAngle(p[0],p[1]),//angle from point 2 to 1
a23=getAngle(p[2],p[1]),//angle from point 2 to 3
//find tangent points
t12=[p[1][0]-cos(a12)*tangD,p[1][1]-sin(a12)*tangD],//tangent point between points 1&2
t23=[p[1][0]-cos(a23)*tangD,p[1][1]-sin(a23)*tangD],//tangent point between points 2&3
//find circle centre
tmid=getMidpoint(t12,t23),//midpoint between the two tangent points
angCen=getAngle(tmid,p[1]),//angle from point 2 to circle centre
cen=[p[1][0]-cos(angCen)*circD,p[1][1]-sin(angCen)*circD],//circle center by offseting from point 2
outR=max(minR,rp[1][2]-thick*sgn*mode) //ensures radii are never too small.
)
concat(cen,outR);
function findPoint(ang1,refpoint1,ang2,refpoint2,r=0)=
let(
m1=tan(ang1),
c1=refpoint1.y-m1*refpoint1.x,
m2=tan(ang2),
c2=refpoint2.y-m2*refpoint2.x,
outputX=(c2-c1)/(m1-m2),
outputY=m1*outputX+c1
)
[outputX,outputY,r];
function RailCustomiser(rp,o1=0,o2,mode=0,minR=0,a1,a2)=
/*This function takes a series of radii points and plots points to run along side at a constanit distance, think of it as offset but for line instead of a polygon
rp=radii points, o1&o2=offset 1&2,minR=min radius, a1&2=angle 1&2
mode=1 - include endpoints a1&2 are relative to the angle of the last two points and equal 90deg if not defined
mode=2 - endpoints not included
mode=3 - include endpoints a1&2 are absolute from the x axis and are 0 if not defined
negative radiuses only allowed for the first and last radii points
As it stands this function could probably be tidied a lot, but it works, I'll tidy later*/
let(
o2undef=o2==undef?1:0,
o2=o2undef==1?0:o2,
CWorCCW1=sign(o1)*CWorCCW(rp),
CWorCCW2=sign(o2)*CWorCCW(rp),
o1=abs(o1),
o2b=abs(o2),
Lrp3=len(rp)-3,
Lrp=len(rp),
a1=mode==0&&a1==undef?
getAngle(rp[0],rp[1])+90:
mode==2&&a1==undef?
0:
mode==0?
getAngle(rp[0],rp[1])+a1:
a1,
a2=mode==0&&a2==undef?
getAngle(rp[Lrp-1],rp[Lrp-2])+90:
mode==2&&a2==undef?
0:
mode==0?
getAngle(rp[Lrp-1],rp[Lrp-2])+a2:
a2,
OffLn1=[for(i=[0:Lrp3]) o1==0?rp[i+1]:parallelFollow([rp[i],rp[i+1],rp[i+2]],o1,minR,mode=CWorCCW1)],
OffLn2=[for(i=[0:Lrp3]) o2==0?rp[i+1]:parallelFollow([rp[i],rp[i+1],rp[i+2]],o2b,minR,mode=CWorCCW2)],
Rp1=abs(rp[0].z),
Rp2=abs(rp[Lrp-1].z),
endP1a=findPoint(getAngle(rp[0],rp[1]), OffLn1[0], a1,rp[0], Rp1),
endP1b=findPoint(getAngle(rp[Lrp-1],rp[Lrp-2]), OffLn1[len(OffLn1)-1], a2,rp[Lrp-1], Rp2),
endP2a=findPoint(getAngle(rp[0],rp[1]), OffLn2[0], a1,rp[0], Rp1),
endP2b=findPoint(getAngle(rp[Lrp-1],rp[Lrp-2]), OffLn2[len(OffLn1)-1], a2,rp[Lrp-1], Rp2),
absEnda=getAngle(endP1a,endP2a),
absEndb=getAngle(endP1b,endP2b),
negRP1a=[cos(absEnda)*rp[0].z*10+endP1a.x, sin(absEnda)*rp[0].z*10+endP1a.y, 0.0],
negRP2a=[cos(absEnda)*-rp[0].z*10+endP2a.x, sin(absEnda)*-rp[0].z*10+endP2a.y, 0.0],
negRP1b=[cos(absEndb)*rp[Lrp-1].z*10+endP1b.x, sin(absEndb)*rp[Lrp-1].z*10+endP1b.y, 0.0],
negRP2b=[cos(absEndb)*-rp[Lrp-1].z*10+endP2b.x, sin(absEndb)*-rp[Lrp-1].z*10+endP2b.y, 0.0],
OffLn1b=(mode==0||mode==2)&&rp[0].z<0&&rp[Lrp-1].z<0?
concat([negRP1a],[endP1a],OffLn1,[endP1b],[negRP1b])
:(mode==0||mode==2)&&rp[0].z<0?
concat([negRP1a],[endP1a],OffLn1,[endP1b])
:(mode==0||mode==2)&&rp[Lrp-1].z<0?
concat([endP1a],OffLn1,[endP1b],[negRP1b])
:mode==0||mode==2?
concat([endP1a],OffLn1,[endP1b])
:
OffLn1,
OffLn2b=(mode==0||mode==2)&&rp[0].z<0&&rp[Lrp-1].z<0?
concat([negRP2a],[endP2a],OffLn2,[endP2b],[negRP2b])
:(mode==0||mode==2)&&rp[0].z<0?
concat([negRP2a],[endP2a],OffLn2,[endP2b])
:(mode==0||mode==2)&&rp[Lrp-1].z<0?
concat([endP2a],OffLn2,[endP2b],[negRP2b])
:mode==0||mode==2?
concat([endP2a],OffLn2,[endP2b])
:
OffLn2
)//end of let()
o2undef==1?OffLn1b:concat(OffLn2b,revList(OffLn1b));
function revList(list)=//reverse list
let(Llist=len(list)-1)
[for(i=[0:Llist]) list[Llist-i]];
function CWorCCW(p)=
let(
Lp=len(p),
e=[for(i=[0:Lp-1])
(p[wrap(i+0,Lp)].x-p[wrap(i+1,Lp)].x)*(p[wrap(i+0,Lp)].y+p[wrap(i+1,Lp)].y)
]
)
sign(sum(e));
function CentreN2PointsArc(p1,p2,cen,mode=0,fn)=
/* This function plots an arc from p1 to p2 with fn increments using the cen as the centre of the arc.
the mode determines how the arc is plotted
mode==0, shortest arc possible
mode==1, longest arc possible
mode==2, plotted clockwise
mode==3, plotted counter clockwise
*/
let(
CWorCCW=CWorCCW([cen,p1,p2]),//determine the direction of rotation
//determine the arc angle depending on the mode
p1p2Angle=cosineRuleAngle(p2,cen,p1),
arcAngle=
mode==0?p1p2Angle:
mode==1?p1p2Angle-360:
mode==2&&CWorCCW==-1?p1p2Angle:
mode==2&&CWorCCW== 1?p1p2Angle-360:
mode==3&&CWorCCW== 1?p1p2Angle:
mode==3&&CWorCCW==-1?p1p2Angle-360:
cosineRuleAngle(p2,cen,p1)
,
r=pointDist(p1,cen),//determine the radius
p1Angle=getAngle(cen,p1) //angle of line 1
)
[for(i=[0:fn]) [cos(p1Angle+(arcAngle/fn)*i*CWorCCW)*r+cen[0],sin(p1Angle+(arcAngle/fn)*i*CWorCCW)*r+cen[1]]];
function moveRadiiPoints(rp,tran=[0,0],rot=0)=
[for(i=rp)
let(
a=getAngle([0,0],[i.x,i.y]),//get the angle of the this point
h=pointDist([0,0],[i.x,i.y]) //get the hypotenuse/radius
)
[h*cos(a+rot)+tran.x,h*sin(a+rot)+tran.y,i.z]//calculate the point's new position
];
module round2d(OR=3,IR=1){
offset(OR){
offset(-IR-OR){
offset(IR){
children();
}
}
}
}
module shell2d(o1,OR=0,IR=0,o2=0){
difference(){
round2d(OR,IR){
offset(max(o1,o2)){
children(0);//original 1st child forms the outside of the shell
}
}
round2d(IR,OR){
difference(){//round the inside cutout
offset(min(o1,o2)){
children(0);//shrink the 1st child to form the inside of the shell
}
if($children>1){
for(i=[1:$children-1]){
children(i);//second child and onwards is used to add material to inside of the shell
}
}
}
}
}
}
module internalSq(size,r,center=0){
tran=center==1?[0,0]:size/2;
translate(tran){
square(size,true);
offs=sin(45)*r;
for(i=[-1,1],j=[-1,1]){
translate([(size.x/2-offs)*i,(size.y/2-offs)*j])circle(r);
}
}
}
module r_extrude(ln,r1=0,r2=0,fn=30){
n1=sign(r1);n2=sign(r2);
r1=abs(r1);r2=abs(r2);
translate([0,0,r1]){
linear_extrude(ln-r1-r2){
children();
}
}
for(i=[0:1/fn:1]){
translate([0,0,i*r1]){
linear_extrude(r1/fn){
offset(n1*sqrt(sq(r1)-sq(r1-i*r1))-n1*r1){
children();
}
}
}
translate([0,0,ln-r2+i*r2]){
linear_extrude(r2/fn){
offset(n2*sqrt(sq(r2)-sq(i*r2))-n2*r2){
children();
}
}
}
}
}
function mirrorPoints(b,rot=0,atten=[0,0])= //mirrors a list of points about Y, ignoring the first and last points and returning them in reverse order for use with polygon or polyRound
let(
a=moveRadiiPoints(b,[0,0],-rot),
temp3=[for(i=[0+atten[0]:len(a)-1-atten[1]])
[a[i][0],-a[i][1],a[i][2]]
],
temp=moveRadiiPoints(temp3,[0,0],rot),
temp2=revList(temp3)
)
concat(b,temp2);
function processRadiiPoints(rp)=
[for(i=[0:len(rp)-1])
processRadiiPoints2(rp,i)
];
function processRadiiPoints2(list,end=0,idx=0,result=0)=
idx>=end+1?result:
processRadiiPoints2(list,end,idx+1,relationalRadiiPoints(result,list[idx]));
function cosineRuleBside(a,c,C)=c*cos(C)-sqrt(sq(a)+sq(c)+sq(cos(C))-sq(c));
function absArelR(po,pn)=
let(
th2=atan(po[1]/po[0]),
r2=sqrt(sq(po[0])+sq(po[1])),
r3=cosineRuleBside(r2,pn[1],th2-pn[0])
)
[cos(pn[0])*r3,sin(pn[0])*r3,pn[2]];
function relationalRadiiPoints(po,pi)=
let(
p0=pi[0],
p1=pi[1],
p2=pi[2],
pv0=pi[3][0],
pv1=pi[3][1],
pt0=pi[3][2],
pt1=pi[3][3],
pn=
(pv0=="y"&&pv1=="x")||(pv0=="r"&&pv1=="a")||(pv0=="y"&&pv1=="a")||(pv0=="x"&&pv1=="a")||(pv0=="y"&&pv1=="r")||(pv0=="x"&&pv1=="r")?
[p1,p0,p2,concat(pv1,pv0,pt1,pt0)]:
[p0,p1,p2,concat(pv0,pv1,pt0,pt1)],
n0=pn[0],
n1=pn[1],
n2=pn[2],
nv0=pn[3][0],
nv1=pn[3][1],
nt0=pn[3][2],
nt1=pn[3][3],
temp=
pn[0]=="l"?
[po[0],pn[1],pn[2]]
:pn[1]=="l"?
[pn[0],po[1],pn[2]]
:nv0==undef?
[pn[0],pn[1],pn[2]]//abs x, abs y as default when undefined
:nv0=="a"?
nv1=="r"?
nt0=="a"?
nt1=="a"||nt1==undef?
[cos(n0)*n1,sin(n0)*n1,n2]//abs angle, abs radius
:absArelR(po,pn)//abs angle rel radius
:nt1=="r"||nt1==undef?
[po[0]+cos(pn[0])*pn[1],po[1]+sin(pn[0])*pn[1],pn[2]]//rel angle, rel radius
:[pn[0],pn[1],pn[2]]//rel angle, abs radius
:nv1=="x"?
nt0=="a"?
nt1=="a"||nt1==undef?
[pn[1],pn[1]*tan(pn[0]),pn[2]]//abs angle, abs x
:[po[0]+pn[1],(po[0]+pn[1])*tan(pn[0]),pn[2]]//abs angle rel x
:nt1=="r"||nt1==undef?
[po[0]+pn[1],po[1]+pn[1]*tan(pn[0]),pn[2]]//rel angle, rel x
:[pn[1],po[1]+(pn[1]-po[0])*tan(pn[0]),pn[2]]//rel angle, abs x
:nt0=="a"?
nt1=="a"||nt1==undef?
[pn[1]/tan(pn[0]),pn[1],pn[2]]//abs angle, abs y
:[(po[1]+pn[1])/tan(pn[0]),po[1]+pn[1],pn[2]]//abs angle rel y
:nt1=="r"||nt1==undef?
[po[0]+(pn[1]-po[0])/tan(90-pn[0]),po[1]+pn[1],pn[2]]//rel angle, rel y
:[po[0]+(pn[1]-po[1])/tan(pn[0]),pn[1],pn[2]]//rel angle, abs y
:nv0=="r"?
nv1=="x"?
nt0=="a"?
nt1=="a"||nt1==undef?
[pn[1],sign(pn[0])*sqrt(sq(pn[0])-sq(pn[1])),pn[2]]//abs radius, abs x
:[po[0]+pn[1],sign(pn[0])*sqrt(sq(pn[0])-sq(po[0]+pn[1])),pn[2]]//abs radius rel x
:nt1=="r"||nt1==undef?
[po[0]+pn[1],po[1]+sign(pn[0])*sqrt(sq(pn[0])-sq(pn[1])),pn[2]]//rel radius, rel x
:[pn[1],po[1]+sign(pn[0])*sqrt(sq(pn[0])-sq(pn[1]-po[0])),pn[2]]//rel radius, abs x
:nt0=="a"?
nt1=="a"||nt1==undef?
[sign(pn[0])*sqrt(sq(pn[0])-sq(pn[1])),pn[1],pn[2]]//abs radius, abs y
:[sign(pn[0])*sqrt(sq(pn[0])-sq(po[1]+pn[1])),po[1]+pn[1],pn[2]]//abs radius rel y
:nt1=="r"||nt1==undef?
[po[0]+sign(pn[0])*sqrt(sq(pn[0])-sq(pn[1])),po[1]+pn[1],pn[2]]//rel radius, rel y
:[po[0]+sign(pn[0])*sqrt(sq(pn[0])-sq(pn[1]-po[1])),pn[1],pn[2]]//rel radius, abs y
:nt0=="a"?
nt1=="a"||nt1==undef?
[pn[0],pn[1],pn[2]]//abs x, abs y
:[pn[0],po[1]+pn[1],pn[2]]//abs x rel y
:nt1=="r"||nt1==undef?
[po[0]+pn[0],po[1]+pn[1],pn[2]]//rel x, rel y
:[po[0]+pn[0],pn[1],pn[2]]//rel x, abs y
)
temp;
function invtan(run,rise)=
let(a=abs(atan(rise/run)))
rise==0&&run>0?
0:rise>0&&run>0?
a:rise>0&&run==0?
90:rise>0&&run<0?
180-a:rise==0&&run<0?
180:rise<0&&run<0?
a+180:rise<0&&run==0?
270:rise<0&&run>0?
360-a:"error";
function cosineRuleAngle(p1,p2,p3)=
let(
p12=abs(pointDist(p1,p2)),
p13=abs(pointDist(p1,p3)),
p23=abs(pointDist(p2,p3))
)
acos((sq(p23)+sq(p12)-sq(p13))/(2*p23*p12));
function sum(list, idx = 0, result = 0) =
idx >= len(list) ? result : sum(list, idx + 1, result + list[idx]);
function sq(x)=x*x;
function getGradient(p1,p2)=(p2.y-p1.y)/(p2.x-p1.x);
function getAngle(p1,p2)=p1==p2?0:invtan(p2[0]-p1[0],p2[1]-p1[1]);
function getMidpoint(p1,p2)=[(p1[0]+p2[0])/2,(p1[1]+p2[1])/2]; //returns the midpoint of two points
function pointDist(p1,p2)=sqrt(abs(sq(p1[0]-p2[0])+sq(p1[1]-p2[1]))); //returns the distance between two points
function isColinear(p1,p2,p3)=getGradient(p1,p2)==getGradient(p2,p3)?1:0;//return 1 if 3 points are colinear
module polyline(p) {
for(i=[0:max(0,len(p)-1)]){
line(p[i],p[wrap(i+1,len(p) )]);
}
} // polyline plotter
module line(p1, p2 ,width=0.3) { // single line plotter
hull() {
translate(p1){
circle(width);
}
translate(p2){
circle(width);
}
}
}
function getpoints(p)=[for(i=[0:len(p)-1])[p[i].x,p[i].y]];// gets [x,y]list of[x,y,r]list
function wrap(x,x_max=1,x_min=0) = (((x - x_min) % (x_max - x_min)) + (x_max - x_min)) % (x_max - x_min) + x_min; // wraps numbers inside boundaries
function rnd(a = 1, b = 0, s = []) =
s == [] ?
(rands(min(a, b), max( a, b), 1)[0]):(rands(min(a, b), max(a, b), 1, s)[0]); // nice rands wrapper