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