commit d4e985f87819dbfabf3bd7efc7a685884aa6c646 Author: Martin Date: Mon Oct 1 09:22:21 2018 +0200 Inital commit, added client application and scad files. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae94088 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pickle +*.csv +*.gcode +__pycache__ diff --git a/README.md b/README.md new file mode 100644 index 0000000..feca9df --- /dev/null +++ b/README.md @@ -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. + diff --git a/control_app/README.md b/control_app/README.md new file mode 100644 index 0000000..06dc662 --- /dev/null +++ b/control_app/README.md @@ -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 diff --git a/control_app/app.py b/control_app/app.py new file mode 100644 index 0000000..92da911 --- /dev/null +++ b/control_app/app.py @@ -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("", self.CanvasDrag) + self.canvas.bind("", self.CanvasClick) + self.canvas.bind("", 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() + diff --git a/control_app/control_serial.py b/control_app/control_serial.py new file mode 100644 index 0000000..c56a0bf --- /dev/null +++ b/control_app/control_serial.py @@ -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 diff --git a/control_app/control_serial_mockup.py b/control_app/control_serial_mockup.py new file mode 100644 index 0000000..bb93e8f --- /dev/null +++ b/control_app/control_serial_mockup.py @@ -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 \ No newline at end of file diff --git a/control_app/path_preview.py b/control_app/path_preview.py new file mode 100644 index 0000000..837d5ec --- /dev/null +++ b/control_app/path_preview.py @@ -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("", 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 diff --git a/mechanics/anchor_mount.scad b/mechanics/anchor_mount.scad new file mode 100644 index 0000000..0231353 --- /dev/null +++ b/mechanics/anchor_mount.scad @@ -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); \ No newline at end of file diff --git a/mechanics/hoop.scad b/mechanics/hoop.scad new file mode 100644 index 0000000..3fd46db --- /dev/null +++ b/mechanics/hoop.scad @@ -0,0 +1,98 @@ +use ; + +// 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)); + } + + + } diff --git a/mechanics/hoop_plotter_adapter.scad b/mechanics/hoop_plotter_adapter.scad new file mode 100644 index 0000000..e58f16f --- /dev/null +++ b/mechanics/hoop_plotter_adapter.scad @@ -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);} +} diff --git a/mechanics/knoob.scad b/mechanics/knoob.scad new file mode 100644 index 0000000..102a255 --- /dev/null +++ b/mechanics/knoob.scad @@ -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); +} \ No newline at end of file diff --git a/mechanics/machine_specific/motor_encoder_parts.scad b/mechanics/machine_specific/motor_encoder_parts.scad new file mode 100644 index 0000000..170e0ba --- /dev/null +++ b/mechanics/machine_specific/motor_encoder_parts.scad @@ -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]); +}; \ No newline at end of file diff --git a/mechanics/machine_specific/needle_encoder_parts.scad b/mechanics/machine_specific/needle_encoder_parts.scad new file mode 100644 index 0000000..de19f04 --- /dev/null +++ b/mechanics/machine_specific/needle_encoder_parts.scad @@ -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); +} +} \ No newline at end of file diff --git a/mechanics/plotter_mount.scad b/mechanics/plotter_mount.scad new file mode 100644 index 0000000..4254980 --- /dev/null +++ b/mechanics/plotter_mount.scad @@ -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); + } + +} \ No newline at end of file diff --git a/mechanics/polyround.scad b/mechanics/polyround.scad new file mode 100644 index 0000000..bf23f7d --- /dev/null +++ b/mechanics/polyround.scad @@ -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 \ No newline at end of file