#*************************************************************************** #* * #* Copyright (c) 2022 Yorik van Havre * #* * #* This program is free software; you can redistribute it and/or modify * #* it under the terms of the GNU Lesser General Public License (LGPL) * #* as published by the Free Software Foundation; either version 2 of * #* the License, or (at your option) any later version. * #* for detail see the LICENCE text file. * #* * #* This 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 Library General Public License for more details. * #* * #* You should have received a copy of the GNU Library General Public * #* License along with this program; if not, write to the Free Software * #* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * #* USA * #* * #*************************************************************************** """ PCBWay Macro for FreeCAD https://pcbway.com This macro sends the currently selected object to the PCBWay website to get an instant quote for manufacturing. After the object is sent, a page will be opened on the PCBWay website to allow the user to adjust details and options. """ # code inspired / borrowed from: # KiCAD PcbWay plugin: https://github.com/pcbway/PCBWay-Plug-in-for-Kicad/blob/main/plugins/thread.py # urllib-based upload: http://pymotw.com/2/urllib2/#uploading-files # mime stuff: https://stackoverflow.com/questions/27099290/where-is-mimetools-choose-boundary-function-in-python3#27174474 # version history: # version 1.0 - 2022.07.22 __title__ = "Macro_PCBWay" __author__ = "Yorik van Havre" __wiki__ = "https://www.freecad.org/wiki/Macro_PCBWay" __url__ = "https://www.pcbway.com" __version__ = "1.0" __date__ = "22/07/2022" import tempfile import os import FreeCAD import FreeCADGui import json import webbrowser import urllib.request import urllib.parse import email.generator import itertools pcb_url = "https://www.pcbway.com/common/freecadupfile" def msg(message): """prints a message where appropriate""" FreeCAD.Console.PrintError(message+"\n") if FreeCAD.GuiUp: from PySide import QtGui reply = QtGui.QMessageBox.critical(None,"PCBWay export",message) class MultiPartData(object): """Gathers data and files to be sent via HTTP POST""" def __init__(self): self.form_fields = [] self.files = [] self.boundary = email.generator._make_boundary() self.add_field("Unit","Millimeter") #FreeCAD's boundboxes are always in mm return def get_content_type(self): return 'multipart/form-data; boundary=%s' % self.boundary def add_field(self, name, value): """Add a simple field to the form data.""" self.form_fields.append((name, str(value))) return def add_file(self, fieldname, filename, fileHandle, mime): """Add a file to be uploaded.""" body = fileHandle.read() self.files.append((fieldname, filename, mime, body)) return def get_bytes(self): """Return a string representing the form data, including attached files.""" parts = [] part_boundary = '--' + self.boundary parts.extend([part_boundary,'Content-Disposition: form-data; name="%s"' % name,'',value] for name, value in self.form_fields) parts.extend([part_boundary,'Content-Disposition: file; name="%s"; filename="%s"' % (field_name, filename), 'Content-Type: %s' % content_type,'',body] for field_name, filename, content_type, body in self.files) flattened = list(itertools.chain(*parts)) flattened.append('--' + self.boundary + '--') flattened.append('') return bytes('\r\n'.join(flattened),'utf8') def main(): # validity tests mesh = None shape = None if not FreeCAD.GuiUp: return if not FreeCAD.ActiveDocument: msg("There is no opened document. Please open or create a document containing objects before running this macro.") return # get the selected object, or the only visible body or object if nothing is selected selection = FreeCADGui.Selection.getSelection() if not selection: visibles = [obj for obj in FreeCAD.ActiveDocument.Objects if obj.ViewObject.Visibility] if len(visibles) == 1: obj = visibles[0] if hasattr(obj,"Shape"): shape = obj.Shape elif hasattr(obj,"Mesh"): mesh = obj.Mesh elif len(visibles) > 1: bodies = [obj for obj in visibles if obj.isDerivedFrom("PartDesign::Body")] if len(bodies) == 1: shape = bodies[0].Shape else: msg("No object is selected. Please select one object or body before running this macro.") return elif len(selection) > 1: msg("More than one object is selected. Please select only one object or body before running this macro.") return else: obj = selection[0] if hasattr(obj,"Shape"): shape = obj.Shape if (not shape) or (not hasattr(shape,"isNull")) or shape.isNull(): msg("The selected object has no shape. Please select an object with a shape before running this macro.") return elif hasattr(obj,"Mesh"): mesh = obj.Mesh if (not shape) and (not mesh): msg("Unable to find a suitable shape in the selection or in the visible objects. Please select an object before running this macro.") return # offer to add the macro as a button on first run # not working yet! # prefs = FreeCAD.ParamGet("User parameter:Plugins/PCBWay") # if prefs.GetBool("FirstTime",True): # prefs.SetBool("FirstTime",False) # from PySide import QtGui # reply = QtGui.QMessageBox.question(None, "Install macro?", # "This is the first time you are launching the PCBWay macro. Do you wish to add a toolbar button for it?", # QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) # if reply == QtGui.QMessageBox.Yes: # icon = "https://github.com/pcbway/PCBWay-Plug-in-for-Kicad/raw/main/resources/icon.png" # u = urllib.request.urlopen(icon) # idata = u.read() # u.close() # idir = os.path.join(FreeCAD.getUserAppDataDir(),"icons") # os.makedirs(idir,exist_ok=False) # f = open(os.path.join(idir,"pcbway.png"),"wb") # f.write(idata) # f.close() if shape: # saving the file as step tf = tempfile.NamedTemporaryFile(suffix=".stp") tf.close() shape.exportStep(tf.name) bb = shape.BoundBox mime = "application/STEP" elif mesh: # saving the file as step tf = tempfile.NamedTemporaryFile(suffix=".obj") tf.close() mesh.write(tf.name) bb = mesh.BoundBox mime = "model/obj" # prepare and upload data data = MultiPartData() data.add_field('Length', bb.XLength) data.add_field('Width', bb.YLength) data.add_field('Height', bb.ZLength) data.add_file('upload[file]', os.path.basename(tf.name), open(tf.name,'r'), mime) request = urllib.request.Request(pcb_url) request.add_header('User-agent', 'FreeCAD (https://freecad.org)') body = data.get_bytes() request.add_header('Content-type', data.get_content_type()) request.add_header('Content-length', len(body)) rsp = urllib.request.urlopen(request,data=body).read() rsp_link = json.loads(rsp.decode('utf8'))['redirect'] # get the respnse and open a link webbrowser.open(rsp_link) # run when macro is launched main()