diff --git a/src/Mod/Arch/importWebGL.py b/src/Mod/Arch/importWebGL.py index 3bc1f75c94..f8d2e27cea 100644 --- a/src/Mod/Arch/importWebGL.py +++ b/src/Mod/Arch/importWebGL.py @@ -1,5 +1,6 @@ #*************************************************************************** #* 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) * @@ -18,233 +19,792 @@ #* 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 +"""FreeCAD WebGL Exporter""" -options: importWebGL.wireframeStyle = "faceloop" (can also be "multimaterial" or None) -importWebGL.template = a complete html file, where $CameraData is a placeholder for the -FreeCAD camera, and $ObjectsData a placeholder for the FreeCAD objects. -importWebGL.linewidth = an integer, specifying the width of lines in "faceloop" mode""" - -import FreeCAD,Draft,Part,DraftGeomUtils +import FreeCAD,Mesh,Draft,Part,DraftGeomUtils,Arch,OfflineRenderingUtils,json,six if FreeCAD.GuiUp: import FreeCADGui from DraftTools import translate else: FreeCADGui = None - # \cond - def translate(ctxt,txt,utf8_decode=True): - return txt - # \endcond + def translate(ctxt, txt): return txt + +if open.__module__ in ['__builtin__','io']: pythonopen = open ## @package importWebGL # \ingroup ARCH -# \brief WebGL file format exporter +# \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. -tab = " " # the tab size -wireframeStyle = "faceloop" # this can be "faceloop", "multimaterial", or None -cameraPosition = None # set this to a tuple to change, for ex. (0,0,0) -linewidth = 1 -template = """ - - - FreeCAD model - +disableCompression = False # Compress object data before sending to JS +base = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!#$%&()*+-:;/=>?@[]^_,.{|}~`' # safe str chars for js in all cases +baseFloat = ',.-0123456789' - + + + """ - var camera, controls, scene, renderer; - - window.onload = function() { - - var SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight; - var VIEW_ANGLE = 35, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 0.1, FAR = 200000; - - renderer = new THREE.WebGLRenderer(); - renderer.setSize( SCREEN_WIDTH, SCREEN_HEIGHT ); - document.body.appendChild( renderer.domElement ); - - scene = new THREE.Scene(); - - camera = new THREE.PerspectiveCamera( - VIEW_ANGLE, // Field of view - ASPECT, // Aspect ratio - NEAR, // Near plane - FAR // Far plane - ); - $CameraData // placeholder for the FreeCAD camera - - controls = new THREE.TrackballControls( camera ); - controls.rotateSpeed = 1.0; - controls.zoomSpeed = 1.2; - controls.panSpeed = 0.8; - controls.noZoom = false; - controls.noPan = false; - controls.staticMoving = true; - controls.dynamicDampingFactor = 0.3; - controls.keys = [ 65, 83, 68 ]; - - $ObjectsData // placeholder for the FreeCAD objects - - var light = new THREE.PointLight( 0xFFFF00 ); - light.position.set( -10000, -10000, 10000 ); - scene.add( light ); - - renderer.render( scene, camera ); - - animate(); - }; - - function animate(){ - requestAnimationFrame( animate ); - render(); - }; - - function render(){ - controls.update(); - renderer.render( scene, camera ); - }; - - - -""" - - -if open.__module__ in ['__builtin__','io']: - pythonopen = open - -def export(exportList,filename,colors=None,camera=None): - "exports the given objects to an .html file" - - html = getHTML(exportList,colors,camera) - outfile = pythonopen(filename,"w") - outfile.write(html) - outfile.close() - FreeCAD.Console.PrintMessage(translate("Arch", "Successfully written", utf8_decode=True) + ' ' + filename + "\n") - -def getHTML(objectsList,colors=None,camera=None): - "returns the complete HTML code of a viewer for the given objects" - - # get objects data - objectsData = '' - for obj in objectsList: - colordata = None - if colors: - if obj.Name in colors: - colordata = colors[obj.Name] - objectsData += getObjectData(obj,color=colordata) - t = template.replace("$CameraData",getCameraData(camera)) - t = t.replace("$ObjectsData",objectsData) - return t - -def getCameraData(camera=None): - "returns the position and direction of the camera as three.js snippet" - - result = "" +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: - global cameraPosition - if isinstance(camera,str): - import OfflineRenderingUtils - camnode = OfflineRenderingUtils.getCoinCamera(camera) - cameraPosition = camnode.position.getValue().getValue() - elif hasattr(camera,"position"): - cameraPosition = camera.position.getValue().getValue() - if cameraPosition: - result += "camera.position.set("+str(cameraPosition[0])+","+str(cameraPosition[1])+","+str(cameraPosition[2])+");\n" - elif FreeCADGui: - # getting camera position - pos = FreeCADGui.ActiveDocument.ActiveView.viewPosition().Base - result += "camera.position.set( " - result += str(pos.x) + ", " - result += str(pos.y) + ", " - result += str(pos.z) + " );\n" + # 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: - result += "camera.position.set(0,0,1000);\n" - result += tab+"camera.lookAt( scene.position );\n"+tab - # print(result) - return result - -def getObjectData(obj,wireframeMode=wireframeStyle,color=None): - """returns the geometry data of an object as three.js snippet. - wireframeMode can be multimaterial, faceloop, or None""" - - result = "" - wires = [] - - if hasattr(obj,'Shape'): - fcmesh = obj.Shape.tessellate(0.1) - result = "var geom = new THREE.Geometry();\n" - # adding vertices data - for i in range(len(fcmesh[0])): - v = fcmesh[0][i] - result += tab+"var v"+str(i)+" = new THREE.Vector3("+str(v.x)+","+str(v.y)+","+str(v.z)+");\n" - result += tab+"console.log(geom.vertices)\n" - for i in range(len(fcmesh[0])): - result += tab+"geom.vertices.push(v"+str(i)+");\n" - # adding facets data - for f in fcmesh[1]: - result += tab+"geom.faces.push( new THREE.Face3"+str(f).replace("L","")+" );\n" - for f in obj.Shape.Faces: - for w in f.Wires: - wo = Part.Wire(Part.__sortEdges__(w.Edges)) - wires.append(wo.discretize(QuasiDeflection=0.1)) - - elif obj.isDerivedFrom("Mesh::Feature"): - mesh = obj.Mesh - result = "var geom = new THREE.Geometry();\n" - # adding vertices data - for p in mesh.Points: - v = p.Vector - i = p.Index - result += tab+"var v"+str(i)+" = new THREE.Vector3("+str(v.x)+","+str(v.y)+","+str(v.z)+");\n" - result += tab+"console.log(geom.vertices)\n" - for p in mesh.Points: - result += tab+"geom.vertices.push(v"+str(p.Index)+");\n" - # adding facets data + 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 Mismach.\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 Mismach.\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: - pointIndices = tuple([ int(i) for i in f.PointIndices ]) - result += tab+"geom.faces.push( new THREE.Face3"+str(pointIndices).replace("L","")+" );\n" + 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 + if disableCompression: html = html.replace('$disableCompression','true') + else: html = html.replace('$disableCompression','false') + + html = html.replace('$base', base) + html = html.replace('$float', 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" ) - if result: - # adding a base material - if color: - rgb = Draft.getrgb(color,testbw=False) - elif FreeCADGui: - col = obj.ViewObject.ShapeColor - rgb = Draft.getrgb(col,testbw=False) - else: - rgb = "#888888" # test color - result += tab+"var basematerial = new THREE.MeshBasicMaterial( { color: 0x"+str(rgb)[1:]+" } );\n" - #result += tab+"var basematerial = new THREE.MeshLambertMaterial( { color: 0x"+str(rgb)[1:]+" } );\n" - - if wireframeMode == "faceloop": - # adding the mesh to the scene with a wireframe copy - result += tab+"var mesh = new THREE.Mesh( geom, basematerial );\n" - result += tab+"scene.add( mesh );\n" - result += tab+"var linematerial = new THREE.LineBasicMaterial({linewidth: %d, color: 0x000000,});\n" % linewidth - for w in wires: - result += tab+"var wire = new THREE.Geometry();\n" - for p in w: - result += tab+"wire.vertices.push(new THREE.Vector3(" - result += str(p.x)+", "+str(p.y)+", "+str(p.z)+"));\n" - result += tab+"var line = new THREE.Line(wire, linematerial);\n" - result += tab+"scene.add(line);\n" - - elif wireframeMode == "multimaterial": - # adding a wireframe material - result += tab+"var wireframe = new THREE.MeshBasicMaterial( { color: " - result += "0x000000, wireframe: true, transparent: true } );\n" - result += tab+"var material = [ basematerial, wireframe ];\n" - result += tab+"var mesh = new THREE.SceneUtils.createMultiMaterialObject( geom, material );\n" - result += tab+"scene.add( mesh );\n"+tab - - else: - # adding the mesh to the scene with simple material - result += tab+"var mesh = new THREE.Mesh( geom, basematerial );\n" - result += tab+"scene.add( mesh );\n"+tab - - return result +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) \ No newline at end of file