#*************************************************************************** #* Copyright (c) 2013 Yorik van Havre * #* Copyright (c) 2020 Travis Apple * #* * #* 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 * #* * #*************************************************************************** # # REFS: # https://github.com/mrdoob/three.js/blob/master/examples/webgl_interactive_buffergeometry.html # https://threejs.org/examples/#webgl_buffergeometry_lines # https://forum.freecadweb.org/viewtopic.php?t=51245 # https://forum.freecadweb.org/viewtopic.php?t=29487 # https://threejs.org/examples/#webgl_raycast_sprite # # Params for export() # 'colors' is of the form: {'Body': [1,0,0], 'Body001': [1,1,0], 'Body002': [1,0,1] } # 'camera' is of the form: "PerspectiveCamera {\n viewportMapping ADJUST_CAMERA\n position 30.242626 -51.772324 85.63475\n orientation -0.4146691 0.088459305 -0.90566254 4.7065201\nnearDistance 53.126431\n farDistance 123.09125\n aspectRatio 1\n focalDistance 104.53851\n heightAngle 0.78539819\n\n}" # The 'camera' string for the active document may be generated from: import OfflineRenderingUtils; OfflineRenderingUtils.getCamera(FreeCAD.ActiveDocument.FileName); # # Development reload oneliner: # def re(): from importlib import reload;import importWebGL;reload(importWebGL);o=FreeCAD.getDocument("YourDocName");importWebGL.export([o.getObject("YourBodyName")],u"C:/path/to/your/file.htm"); """FreeCAD WebGL Exporter""" import FreeCAD,Mesh,Draft,Part,OfflineRenderingUtils,json,six import textwrap if FreeCAD.GuiUp: import FreeCADGui from DraftTools import translate else: FreeCADGui = None def translate(ctxt, txt): return txt if open.__module__ in ['__builtin__','io']: pythonopen = open ## @package importWebGL # \ingroup ARCH # \brief FreeCAD WebGL Exporter # # This module provides tools to export HTML files containing the # exported objects in WebGL format and a simple three.js-based viewer. disableCompression = False # Compress object data before sending to JS base = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!#$%&()*+-:;/=>?@[]^_,.{|}~`' # safe str chars for js in all cases baseFloat = ',.-0123456789' def getHTMLTemplate(): return textwrap.dedent("""\ $pagetitle """) def export( exportList, filename, colors = None, camera = None ): """Exports objects to an html file""" global disableCompression, base, baseFloat data = { 'camera':{}, 'file':{}, 'objects':[] } if not FreeCADGui and not camera: camera = OfflineRenderingUtils.getCamera(FreeCAD.ActiveDocument.FileName) if camera: # REF: https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/Arch/OfflineRenderingUtils.py camnode = OfflineRenderingUtils.getCoinCamera(camera) cameraPosition = camnode.position.getValue().getValue() data['camera']['type'] = 'Orthographic' if 'PerspectiveCamera' in camera: data['camera']['type'] = 'Perspective' data['camera']['focalDistance'] = camnode.focalDistance.getValue() data['camera']['position_x'] = cameraPosition[0] data['camera']['position_y'] = cameraPosition[1] data['camera']['position_z'] = cameraPosition[2] else: v = FreeCADGui.ActiveDocument.ActiveView data['camera']['type'] = v.getCameraType() data['camera']['focalDistance'] = v.getCameraNode().focalDistance.getValue() data['camera']['position_x'] = v.viewPosition().Base.x data['camera']['position_y'] = v.viewPosition().Base.y data['camera']['position_z'] = v.viewPosition().Base.z # Take the objects out of groups objectslist = Draft.get_group_contents(exportList, walls=True, addgroups=False) # objectslist = Arch.pruneIncluded(objectslist) for obj in objectslist: # Pull all obj data before we dig down the links label = obj.Label color = '#cccccc'; opacity = 1.0 if FreeCADGui: color = Draft.getrgb(obj.ViewObject.ShapeColor, testbw = False) opacity = int((100 - obj.ViewObject.Transparency)/5) / 20 # 0>>1 with step of 0.05 elif colors: if label in colors: color = Draft.getrgb(colors[label], testbw = False) validObject = False if obj.isDerivedFrom('Mesh::Feature'): mesh = obj.Mesh validObject = True if obj.isDerivedFrom('Part::Feature'): objShape = obj.Shape validObject = True if obj.isDerivedFrom('App::Link'): linkPlacement = obj.LinkPlacement while True: # drill down to get to the actual obj if obj.isDerivedFrom("App::Link"): if obj.ViewObject.OverrideMaterial: color = Draft.getrgb(obj.ViewObject.ShapeMaterial.DiffuseColor, testbw = False) obj = obj.LinkedObject if hasattr(obj, "__len__"): FreeCAD.Console.PrintMessage(label + ": Sub-Links are Unsupported.\n") break elif obj.isDerivedFrom('Part::Feature'): objShape = obj.Shape.copy(False) objShape.Placement = linkPlacement validObject = True break elif obj.isDerivedFrom("Mesh::Feature"): mesh = obj.Mesh.copy() mesh.Placement = linkPlacement validObject = True break if not validObject: continue objdata = { 'name': label, 'color': color, 'opacity': opacity, 'verts':'', 'facets':'', 'wires':[], 'faceColors':[], 'facesToFacets':[], 'floats':[] } if obj.isDerivedFrom('Part::Feature'): deviation = 0.5 if FreeCADGui: deviation = obj.ViewObject.Deviation # obj.ViewObject.DiffuseColor is length=1 when all faces are the same color, length=len(faces) for when they're not if len(obj.ViewObject.DiffuseColor) == len(objShape.Faces): for fc in obj.ViewObject.DiffuseColor: objdata['faceColors'].append( Draft.getrgb(fc, testbw = False) ) # get verts and facets for ENTIRE object shapeData = objShape.tessellate( deviation ) mesh = Mesh.Mesh(shapeData) if len(objShape.Faces) > 1: # Map each Facet created by tessellate() to a Face so that it can be colored correctly using faceColors # This is done by matching the results of a tessellate() on EACH FACE to the overall tessellate stored in shapeData # if there is any error in matching these two then we display the whole object as one face and forgo the face colors for f in objShape.Faces: faceData = f.tessellate( deviation ) found = True for fv in range( len(faceData[0]) ): # face verts. List of type Vector() found = False for sv in range( len(shapeData[0]) ): #shape verts if faceData[0][fv] == shapeData[0][sv]: # do not use isEqual() here faceData[0][fv] = sv # replace with the index of shapeData[0] found = True break if not found: break if not found: FreeCAD.Console.PrintMessage("Facet to Face Mismatch.\n") objdata['facesToFacets'] = [] break # map each of the face facets to the shape facets and make a list of shape facet indices that belong to this face facetList = [] for ff in faceData[1]: # face facets found = False for sf in range( len(shapeData[1]) ): #shape facets if faceData[0][ff[0]] in shapeData[1][sf] and faceData[0][ff[1]] in shapeData[1][sf] and faceData[0][ff[2]] in shapeData[1][sf]: facetList.append(sf) found = True break if not found: break if not found: FreeCAD.Console.PrintMessage("Facet List Mismatch.\n") objdata['facesToFacets'] = [] break objdata['facesToFacets'].append( baseEncode(facetList) ) wires = [] # Add wires for f in objShape.Faces: for w in f.Wires: wo = Part.Wire(Part.__sortEdges__(w.Edges)) wire = [] for v in wo.discretize(QuasiDeflection = 0.005): wire.append( '{:.5f}'.format(v.x) ) # use strings to avoid 0.00001 written as 1e-05 wire.append( '{:.5f}'.format(v.y) ) wire.append( '{:.5f}'.format(v.z) ) wires.append( wire ) if not disableCompression: for w in range( len(wires) ): for wv in range( len(wires[w]) ): found = False for f in range( len(objdata['floats']) ): if objdata['floats'][f] == wires[w][wv]: wires[w][wv] = f found = True break if not found: objdata['floats'].append( wires[w][wv] ) wires[w][wv] = len(objdata['floats'])-1 wires[w] = baseEncode(wires[w]) objdata['wires'] = wires vIndex = {} verts = [] for p in range( len(mesh.Points) ): vIndex[ mesh.Points[p].Index ] = p verts.append( '{:.5f}'.format(mesh.Points[p].Vector.x) ) verts.append( '{:.5f}'.format(mesh.Points[p].Vector.y) ) verts.append( '{:.5f}'.format(mesh.Points[p].Vector.z) ) # create floats list to compress verts and wires being written into the JS if not disableCompression: for v in range( len(verts) ): found = False for f in range( len(objdata['floats']) ): if objdata['floats'][f] == verts[v]: verts[v] = f found = True break if not found: objdata['floats'].append( verts[v] ) verts[v] = len(objdata['floats'])-1 objdata['verts'] = baseEncode(verts) facets = [] for f in mesh.Facets: for i in f.PointIndices: facets.append( vIndex[i] ) objdata['facets'] = baseEncode(facets) # compress floats if not disableCompression: # use ratio of 7x base13 to 4x base90 because 13^7 ~ 90^4 fullstr = json.dumps(objdata['floats'], separators=(',', ':')) fullstr = fullstr.replace('[', '').replace(']', '').replace('"', '') floatStr = '' baseFloatCt = len(baseFloat) baseCt = len(base) for fs in range( 0, len(fullstr), 7 ): # chunks of 7 chars, skip the first one str7 = fullstr[fs:(fs+7)] quotient = 0 for s in range( len(str7) ): quotient += baseFloat.find(str7[s]) * pow(baseFloatCt, (6-s)) for v in range(4): floatStr += base[ quotient % baseCt ] quotient = int(quotient / baseCt) objdata['floats'] = floatStr data['objects'].append( objdata ) html = getHTMLTemplate() html = html.replace('$pagetitle',FreeCAD.ActiveDocument.Label) version = FreeCAD.Version() html = html.replace('$version',version[0] + '.' + version[1] + '.' + version[2]) # Remove data compression in JS data['compressed'] = not disableCompression data['base'] = base data['baseFloat'] = baseFloat html = html.replace('$data', json.dumps(data, separators=(',', ':')) ) # Shape Data if six.PY2: outfile = pythonopen(filename, "wb") else: outfile = pythonopen(filename, "w") outfile.write( html ) outfile.close() FreeCAD.Console.PrintMessage( translate("Arch", "Successfully written") + ' ' + filename + "\n" ) def baseEncode( arr ): """Compresses an array of ints into a base90 string""" global disableCompression, base if disableCompression: return arr if len(arr) == 0: return '' longest = 0 output = [] baseCt = len(base) for v in range( len(arr) ): buffer = '' quotient = arr[v] while True: buffer += base[ quotient % baseCt ] quotient = int(quotient / baseCt) if quotient == 0: break output.append( buffer ) if len(buffer) > longest: longest = len(buffer) output = [('{:>'+str(longest)+'}').format(x) for x in output] # pad each element return str(longest) + ('').join(output)