its a start
This commit is contained in:
commit
0dbe610d68
10 changed files with 420 additions and 0 deletions
13
README.md
Normal file
13
README.md
Normal file
|
@ -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
|
90
app.py
Normal file
90
app.py
Normal file
|
@ -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/<name>.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/<name>.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')
|
BIN
models/master_rail.FCStd
Normal file
BIN
models/master_rail.FCStd
Normal file
Binary file not shown.
66
static/FileLoader.js
Normal file
66
static/FileLoader.js
Normal file
|
@ -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 = '<center>Drop File Here<center>';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
customElements.define('file-loader', FileLoaderElement);
|
||||||
|
export { FileLoaderElement };
|
39
static/ThreeGuiElement.js
Normal file
39
static/ThreeGuiElement.js
Normal file
|
@ -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);
|
127
static/ThreeRendererElement.js
Normal file
127
static/ThreeRendererElement.js
Normal file
|
@ -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);
|
35
static/index.js
Normal file
35
static/index.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
19
static/main.css
Normal file
19
static/main.css
Normal file
|
@ -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;
|
||||||
|
}
|
1
static/three.js
Submodule
1
static/three.js
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 9b274fef1d2b9ba0fc47410d06593e1f34f4df0e
|
30
templates/index.html
Normal file
30
templates/index.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>FreeCAD Web Viewer</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; }
|
||||||
|
</style>
|
||||||
|
<link href="static/main.css" rel='stylesheet' type='text/css' />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type='importmap'>
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"three": "./static/three.js-master/src/Three.js",
|
||||||
|
"Loader": "./static/three.js-master/src/loaders/Loader.js",
|
||||||
|
"GUI": "./static/three.js-master/examples/jsm/libs/lil-gui.module.min.js",
|
||||||
|
"OrbitControls": "./static/three.js-master/examples/jsm/controls/OrbitControls.js",
|
||||||
|
"STLLoader": "./static/three.js-master/examples/jsm/loaders/STLLoader.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<content>
|
||||||
|
<file-loader> </file-loader>
|
||||||
|
<three-renderer src='static/master_railx10.stl'></three-renderer>
|
||||||
|
<three-gui></three-gui>
|
||||||
|
</content>
|
||||||
|
<script src='./static/index.js' type='module'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue