commit 0dbe610d6804907d80618a6e30a1450e78d3794d Author: Morgan 'ARR\!' Allen Date: Tue Mar 21 16:42:22 2023 -0700 its a start diff --git a/README.md b/README.md new file mode 100644 index 0000000..507df24 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +Quick and dirty demo of using Flask and the Python FreeCAD modules to Spreadsheet based +variables and return the resulting model to a ThreeJS based viewer. + +# Quickstart +``` +git clone https://git.oit.cloud/morgan/freecad_webviewer_poc.git +cd freecad_webviewer_poc +python -m venv . +pip install flask +``` + +It is expected you have FreeCAD installed on your system and you might need to update line 2 +in app.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..e6bf919 --- /dev/null +++ b/app.py @@ -0,0 +1,90 @@ +import sys +sys.path.append('/home/morgan/devel/FreeCAD-build/lib') + +from flask import Flask, render_template, request, send_file +from hashlib import blake2b +import FreeCAD +import Part +import Import +import json +import time +from FreeCAD import Mesh +print(FreeCAD, Import, Part) + +app = Flask(__name__) +app.config['TEMPLATES_AUTO_RELOAD'] = True + +@app.route('/gltf/.gltf') +def gen_gltf(name): + file = './models/{}.FCStd'.format(name) + + doc = FreeCAD.openDocument(file) + body = doc.getObject('Body') + + sheet = doc.getObject('Spreadsheet') + + sheet.set('chole', request.args['count']) + doc.recompute() + + out_file = u'static/{}x{}.gltf'.format(name, request.args['count']) + print('writing to {}'.format(out_file)) + + Import.export([ body ], out_file) + + return send_file(out_file) + +@app.route('/stl/.stl') +def gen_stl(name): + file = './models/{}.FCStd'.format(name) + + print('loading ', file) + + doc = FreeCAD.openDocument(file) + print(doc) + #body = doc.getObject('Body') + # TODO support more types + + bodies = [body for body in doc.findObjects('PartDesign::Body') if body.Visibility] + + sheet = doc.getObject('Spreadsheet') + + for k in request.args: + v = request.args[k] + print(k, v) + try: + sheet.set(k, v) + except Exception as e: + print(e) + + doc.recompute() + arg_hash = blake2b(bytes(json.dumps(request.args), 'utf-8'), digest_size=10).hexdigest() + out_file = './stls/{}_{}.stl'.format(name, arg_hash) + Mesh.export(bodies, out_file) + + return send_file(out_file) + +@app.route('/upload', methods=['POST']) +def upload(): + f = request.files['upload'] + out_file = 'models/{}'.format(f.filename) + f.save(out_file) + + doc = FreeCAD.openDocument(out_file) + sheet = doc.getObject('Spreadsheet') + cells = [] + for cell in [cell for cell in sheet.getUsedCells() if sheet.getAlias(cell)]: + value = sheet.get(cell) + + if hasattr(value, 'Value'): value = value.Value + + cells.append({ + 'cell': cell, + 'alias': sheet.getAlias(cell), + 'value': value + }) + + return json.dumps(cells) + +@app.route("/") +def hello_world(): + return render_template('index.html') diff --git a/models/master_rail.FCStd b/models/master_rail.FCStd new file mode 100644 index 0000000..451b4d8 Binary files /dev/null and b/models/master_rail.FCStd differ diff --git a/static/FileLoader.js b/static/FileLoader.js new file mode 100644 index 0000000..5bcf34a --- /dev/null +++ b/static/FileLoader.js @@ -0,0 +1,66 @@ +class FileLoaderElement extends HTMLElement { + constructor() { + super(); + + this.addEventListener('dragover', this.onDragOver.bind(this)); + this.addEventListener('dragleave', this.onDragLeave.bind(this)); + this.addEventListener('drop', this.onDrop.bind(this)); + } + + onDrop(evt) { + evt.preventDefault(); + this.classList.remove('dragover'); + + var formData = new FormData(); + + [...evt.dataTransfer.items].forEach(function(item) { + console.log(item.getAsFile()); + formData.append('upload', item.getAsFile()); + }); + + var filename = evt.dataTransfer.files[0].name; + var self = this; + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/upload'); + xhr.send(formData); + xhr.addEventListener('load', function(evt) { + try { + var data = JSON.parse(xhr.responseText); + } catch(e) { + var data = {}; + } + + self.dispatchEvent(new CustomEvent("upload_complete", { + detail: { + data: data, + filename: filename + } + })); + }); + } + + onXHRComplete(evt) { + } + + onDragOver(evt) { + if(!evt.dataTransfer) return; + + if(Array.from(evt.dataTransfer.items).filter(function(el) { + if(el.type == 'application/x-extension-fcstd') return el; + }).length > 0) { + this.classList.add('dragover'); + evt.preventDefault(); + } + } + + onDragLeave(evt) { + this.classList.remove('dragover'); + } + + connectedCallback() { + this.innerHTML = '
Drop File Here
'; + } +}; + +customElements.define('file-loader', FileLoaderElement); +export { FileLoaderElement }; diff --git a/static/ThreeGuiElement.js b/static/ThreeGuiElement.js new file mode 100644 index 0000000..8fc351a --- /dev/null +++ b/static/ThreeGuiElement.js @@ -0,0 +1,39 @@ +import { GUI } from 'GUI'; + +export class ThreeGUIElement extends HTMLElement{ + constructor() { + super(); + this.GUI = new GUI(); + this.GUI.onChange(this.onGUIValueChange.bind(this)); + } + + onGUIValueChange(evt) { + this.dispatchEvent(new CustomEvent('value-change', { + detail: { + property: evt.property, + value: evt.value + } + })); + } + + reset() { + while(this.GUI.children.length > 0) { + this.GUI.children[0].destroy(); + } + } + + setElements(elements) { + var self = this; + this.elements = {}; + + var gui = this.GUI; + this.reset(); + elements.forEach(function(parameter) { + console.log(parameter) + self.elements[parameter.alias] = parameter.value; + gui.add(self.elements, parameter.alias, parameter.value); + }); + } +} + +customElements.define('three-gui', ThreeGUIElement); diff --git a/static/ThreeRendererElement.js b/static/ThreeRendererElement.js new file mode 100644 index 0000000..931b917 --- /dev/null +++ b/static/ThreeRendererElement.js @@ -0,0 +1,127 @@ +import * as THREE from 'three'; +import { STLLoader } from 'STLLoader'; +import { OrbitControls } from 'OrbitControls'; + +export class ThreeRendererElement extends HTMLElement { + constructor() { + super(); + + var scene = this.scene = new THREE.Scene(); + var camera = this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); + + var light = this.light = new THREE.AmbientLight(0xffffff, 1); + this.scene.add(light); + var point_light = this.point_light = new THREE.DirectionalLight(0xffffff, 0.5); + this.point_light.position.set(0, 0, 10); + this.point_light.target.position.set(-5, 0, 0); + this.scene.add(point_light); + this.scene.add(point_light.target); + + var renderer = this.renderer = new THREE.WebGLRenderer(); + var bb = this.parentElement.getBoundingClientRect(); + this.renderer.setSize(bb.width, bb.height); + this.controls = new OrbitControls(camera, renderer.domElement); + + this.geometry = new THREE.BoxGeometry(); + this.material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); + + this.scene.background = new THREE.Color(0xababab); + + this.camera.position.z = 5; + var group = this.group = new THREE.Group(); + //group.rotation.x = group.rotation.y = Math.PI / 2; + this.scene.add(group); + + // ground plane + const planeSize = 40; + const loader = new THREE.TextureLoader(); + const texture = loader.load('static/three.js-master/examples/textures/checker.png'); + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.magFilter = THREE.NearestFilter; + const repeats = planeSize / 2; + texture.repeat.set(repeats, repeats); + + const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize); + const planeMat = new THREE.MeshPhongMaterial({ + map: texture, + side: THREE.DoubleSide, + }); + const plane_mesh = new THREE.Mesh(planeGeo, planeMat); + window.plane_mesh = plane_mesh; + //plane_mesh.rotation.x = Math.PI * -.5; + plane_mesh.position.z = -10; + this.scene.add(plane_mesh); + + if(this.hasAttribute('src')) { + this.loadObject(this.getAttribute('src')); + } + + + // XXX applWHY? + this.do_animate = this.do_animate.bind(this); + this.do_animate(); + } + + loadObject(src) { + const loader = new STLLoader(); + const group = this.group; + + loader.load(src, function (geometry) { + group.clear(); + + const material = new THREE.MeshStandardMaterial({ + color: 0xababab, + metalness: 0.5, + specular: 0x111111, + shininess: 300 + }); + + const mesh = new THREE.Mesh(geometry, material); + + mesh.castShadow = true; + mesh.receiveShadow = true; + + const edges = new THREE.EdgesGeometry(geometry); + const lines = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ + color: 0xff0000 + })); + + geometry.center(); + edges.center(); + group.add(mesh); + group.add(lines); + }); + } + + set src(url) { + return this.setAttribute('src', url); + } + + get src() { + return this.getAttribute('src'); + } + + static get observedAttributes() { + return [ 'src' ]; + } + + do_animate() { + // TODO handle pausing + requestAnimationFrame(this.do_animate); + + this.renderer.render(this.scene, this.camera); + } + + attributeChangedCallback(name, old_value, value) { + if(name == 'src') { + this.loadObject(value); + } + } + + connectedCallback() { + this.appendChild(this.renderer.domElement); + } +} + +customElements.define('three-renderer', ThreeRendererElement); diff --git a/static/index.js b/static/index.js new file mode 100644 index 0000000..96269ce --- /dev/null +++ b/static/index.js @@ -0,0 +1,35 @@ +import './FileLoader.js'; +import './ThreeRendererElement.js'; +import './ThreeGuiElement.js'; + +const loadObject = (count) => { +} + +window.addEventListener("DOMContentLoaded", function() { + var uploader = document.querySelector('file-loader'); + var renderer = document.querySelector('three-renderer'); + var gui = document.querySelector('three-gui'); + + uploader.addEventListener('upload_complete', function(evt) { + console.log(evt, evt.detail); + renderer.setAttribute('src', `/stl/${evt.detail.filename.split('.')[0]}.stl`); + gui.setElements(evt.detail.data); + }); + + gui.addEventListener('value-change', function(evt) { + if(evt.detail) { + var src = renderer.src; + try { + var url = new URL(src); + } catch(e) { + var url = new URL(window.location) + url.pathname = src; + } + + url.searchParams.set(evt.detail.property, evt.detail.value); + + console.log(url) + renderer.src = url.href; + } + }); +}); diff --git a/static/main.css b/static/main.css new file mode 100644 index 0000000..ac3a415 --- /dev/null +++ b/static/main.css @@ -0,0 +1,19 @@ +html, body, content { + height: 100%; + width: 100%; +} + +content { + display: inline-block; +} + +file-loader { + background-color: #bab; + height: 40px; + position: absolute; + width: 100%; +} + +file-loader.dragover { + background-color: #d042d0; +} diff --git a/static/three.js b/static/three.js new file mode 160000 index 0000000..9b274fe --- /dev/null +++ b/static/three.js @@ -0,0 +1 @@ +Subproject commit 9b274fef1d2b9ba0fc47410d06593e1f34f4df0e diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..7c7392d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,30 @@ + + + + + FreeCAD Web Viewer + + + + + + + + + + + + +