pyodide/app-demo

This commit is contained in:
Thomas Krijnen
2025-01-12 14:45:25 +01:00
parent 0eb987fc07
commit 6aec2f7a02
6 changed files with 993 additions and 0 deletions
+3
View File
@@ -20,3 +20,6 @@
[submodule "src/ifcopenshell-python/ifcopenshell/simple_spf"]
path = src/ifcopenshell-python/ifcopenshell/simple_spf
url = https://github.com/IfcOpenShell/step-file-parser
[submodule "src/pyodide/demo-app/wheels"]
path = src/pyodide/demo-app/wheels
url = https://github.com/IfcOpenShell/wasm-wheels
+339
View File
@@ -0,0 +1,339 @@
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.api
import ifcopenshell.util.unit
import numpy as np
import propertygroups
O = 0.0, 0.0, 0.0
X = 1.0, 0.0, 0.0
Y = 0.0, 1.0, 0.0
Z = 0.0, 0.0, 1.0
class Context:
model = None
body = None
storey = None
def __init__(self):
self._create_empty_model()
def clear(self):
self._create_empty_model()
def open(self, file_content):
self.model = ifcopenshell.file.from_string(file_content)
body = [ctx for ctx in self.model.by_type('IfcGeometricRepresentationContext') if ctx.ContextIdentifier == 'Body']
if body:
self.body = body[0]
else:
context = ifcopenshell.api.run("context.add_context", self.model, context_type="Model")
self.body = ifcopenshell.api.run(
"context.add_context",
self.model,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=context,
)
axis = [ctx for ctx in self.model.by_type('IfcGeometricRepresentationContext') if ctx.ContextIdentifier == 'Axis']
if body:
self.axis = axis[0]
else:
context = ifcopenshell.api.run("context.add_context", self.model, context_type="Model")
self.axis = ifcopenshell.api.run(
"context.add_context",
self.model,
context_type="Model",
context_identifier="Axis",
target_view="GRAPH_VIEW",
parent=context,
)
self.storey = self.model.by_type('IfcBuildingStorey')[0]
def _create_empty_model(self):
# Create a blank model
self.model = ifcopenshell.file()
# All projects must have one IFC Project element
project = ifcopenshell.api.run(
"root.create_entity", self.model, ifc_class="IfcProject", name="My Project"
)
# Geometry is optional in IFC, but because we want to use geometry in this example, let's define units
# Assigning without arguments defaults to metric units
ifcopenshell.api.run("unit.assign_unit", self.model)
# Let's create a modeling geometry context, so we can store 3D geometry (note: IFC supports 2D too!)
context = ifcopenshell.api.run("context.add_context", self.model, context_type="Model")
# In particular, in this example we want to store the 3D "body" geometry of objects, i.e. the body shape
self.body = ifcopenshell.api.run(
"context.add_context",
self.model,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=context,
)
self.axis = ifcopenshell.api.run(
"context.add_context",
self.model,
context_type="Model",
context_identifier="Axis",
target_view="GRAPH_VIEW",
parent=context,
)
# Create a site, building, and storey. Many hierarchies are possible.
site = ifcopenshell.api.run(
"root.create_entity", self.model, ifc_class="IfcSite", name="My Site"
)
building = ifcopenshell.api.run(
"root.create_entity", self.model, ifc_class="IfcBuilding", name="Building A"
)
self.storey = ifcopenshell.api.run(
"root.create_entity",
self.model,
ifc_class="IfcBuildingStorey",
name="Ground Floor",
)
# Since the site is our top level location, assign it to the project
# Then place our building on the site, and our storey in the building
ifcopenshell.api.run(
"aggregate.assign_object",
self.model,
relating_object=project,
products=[site],
)
ifcopenshell.api.run(
"aggregate.assign_object",
self.model,
relating_object=site,
products=[building],
)
ifcopenshell.api.run(
"aggregate.assign_object",
self.model,
relating_object=building,
products=[self.storey],
)
def create_2pt_wall(
self, p1, p2, elevation, height, thickness, container, wall_type=None
):
p1 = np.array([p1[0], p1[1]])
p2 = np.array([p2[0], p2[1]])
wall = ifcopenshell.api.run("root.create_entity", self.model, ifc_class="IfcWall")
length = float(np.linalg.norm(p2 - p1))
representation = ifcopenshell.api.run(
"geometry.add_wall_representation",
self.model,
context=self.body,
length=length,
height=height,
thickness=thickness,
)
ifcopenshell.api.run(
"geometry.assign_representation",
self.model,
product=wall,
representation=representation,
)
representation = ifcopenshell.api.run(
"geometry.add_axis_representation",
self.model,
context=self.axis,
axis=[(0.0, 0.0), (length, 0.0)],
)
ifcopenshell.api.run(
"geometry.assign_representation",
self.model,
product=wall,
representation=representation,
)
v = p2 - p1
v = np.divide(v, float(np.linalg.norm(v)), casting="unsafe")
matrix = np.array(
[
[v[0], -v[1], 0, p1[0]],
[v[1], v[0], 0, p1[1]],
[0, 0, 1, elevation],
[0, 0, 0, 1],
]
)
ifcopenshell.api.run("geometry.edit_object_placement", self.model, product=wall, matrix=matrix)
ifcopenshell.api.run(
"spatial.assign_container",
self.model,
relating_structure=container,
products=[wall],
)
if wall_type:
ifcopenshell.api.run(
"type.assign_type",
self.model,
related_object=wall,
relating_type=wall_type,
)
return wall
def get_element(self, guid):
return self.model[guid]
def get_model(self):
return self.model
def create_fill(self, ty, pt, wall):
if isinstance(wall, str):
wall = self.model[wall]
if not wall.is_a('IfcWall'):
raise ValueError("Only 'wall' hosts are supported")
if ty == 'door':
props = propertygroups.BIMDoorProperties()
elif ty == 'window':
props = propertygroups.BIMWindowProperties()
else:
raise ValueError("Only 'door' or 'window' fills are supported")
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.model)
body = ifcopenshell.util.representation.get_context(
self.model, "Model", "Body", "MODEL_VIEW"
)
representation_data = props.to_dict(si_conversion=si_conversion)
representation_data["context"] = body
door_representation = ifcopenshell.api.run(
f"geometry.add_{ty}_representation", self.model, **representation_data
)
door = ifcopenshell.api.run(
"root.create_entity", self.model, ifc_class=f"ifc{ty}"
)
door.OverallWidth = props.overall_width / si_conversion
door.OverallHeight = props.overall_height / si_conversion
ifcopenshell.api.run(
"geometry.assign_representation",
self.model,
product=door,
representation=door_representation,
)
ifcopenshell.api.run(
"spatial.assign_container",
self.model,
relating_structure=self.storey,
products=[door],
)
r = [
r
for r in wall.Representation.Representations
if r.RepresentationIdentifier == "Axis"
]
if not r:
raise ValueError("Axis representation is needed")
r = r[0]
axis_geometry = ifcopenshell.geom.create_shape(
ifcopenshell.geom.settings(
DIMENSIONALITY=ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS,
USE_WORLD_COORDS=True,
),
wall,
r,
)
vs = np.array(axis_geometry.geometry.verts).reshape((-1, 3))
es = np.array(axis_geometry.geometry.edges).reshape((-1, 2))
A, B = vs[es[0]]
v = B - A
P = np.zeros(3)
P[0 : len(pt)] = pt
AP = P - A
AP_dot_v = np.dot(AP, v)
v_dot_v = np.dot(v, v)
t = AP_dot_v / v_dot_v * np.linalg.norm(v) / si_conversion
opening = ifcopenshell.api.run(
"root.create_entity",
self.model,
ifc_class="IfcOpeningElement",
predefined_type="OPENING",
name="Opening",
)
position_3d = None
if self.model.schema == "IFC2X3":
position_3d = self.model.createIfcAxis2Placement2D(
self.model.createIfcCartesianPoint([0.0, 0.0, 0.0])
)
position_2d = self.model.createIfcAxis2Placement2D(
self.model.createIfcCartesianPoint([door.OverallWidth / 2.0, 0.0])
)
opening.Representation = self.model.createIfcProductDefinitionShape(
Representations=[
self.model.createIfcRepresentation(
body,
"Body",
"SweptSolid",
Items=[
self.model.createIfcExtrudedAreaSolid(
self.model.createIfcRectangleProfileDef(
"AREA",
None,
position_2d,
door.OverallWidth,
1.2 / si_conversion,
),
position_3d,
self.model.createIfcDirection((0.0, 0.0, 1.0)),
door.OverallHeight,
)
],
)
]
)
ifcopenshell.api.run(
"void.add_opening", self.model, opening=opening, element=wall
)
ifcopenshell.api.run(
"void.add_filling", self.model, opening=opening, element=door
)
z_offsets = {
'door': 0,
'window': 1
}
opening.ObjectPlacement = self.model.createIfcLocalPlacement(
wall.ObjectPlacement,
self.model.createIfcAxis2Placement3D(
self.model.createIfcCartesianPoint((float(t), 0.0, z_offsets[ty] / si_conversion))
),
)
door.ObjectPlacement = self.model.createIfcLocalPlacement(
opening.ObjectPlacement,
self.model.createIfcAxis2Placement3D(
self.model.createIfcCartesianPoint((0.0, 0.0, 0.0))
),
)
return door
def to_obj_file(self, fn):
st = ifcopenshell.geom.settings(USE_WORLD_COORDS=True, WELD_VERTICES=False)
it = ifcopenshell.geom.iterator(st, self.model, exclude=("IfcOpeningElement",))
sr = ifcopenshell.geom.serializers.obj(
fn, fn + ".mtl", st, ifcopenshell.geom.serializer_settings()
)
if it.initialize():
for el in ifcopenshell.geom.consume_iterator(it):
sr.write(el)
sr.finalize()
if __name__ == "__main__":
m = Context()
w1 = m.create_2pt_wall((0.0, 0.0), (4.0, 0.0), 0.0, 3.0, 0.1, m.storey)
w2 = m.create_2pt_wall((1.0, 3.0), (1.0, 0.0), 0.0, 3.0, 0.1, m.storey)
m.create_fill('door', [2, 0.0], w1)
m.create_fill('window', [1, 1.5], w2)
m.model.write("out.ifc")
m.to_obj_file("out.obj")
+405
View File
@@ -0,0 +1,405 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,200,0,0" />
</head>
<body class='loading'>
<div class='menu'>
<ul>
<li class="material-symbols-outlined">note_add</li>
<li class="material-symbols-outlined">folder_open</li>
<li class="material-symbols-outlined">save</li>
</ul>
<div id='branding'>
<b>IfcOpenShell</b> WebAssembly
</div>
</div>
<div class='toolbar'>
<ul>
<li class="material-symbols-outlined active">arrow_selector_tool</li>
<li class="material-symbols-outlined">diagonal_line</li>
<li class="material-symbols-outlined">door_open</li>
<li class="material-symbols-outlined">window</li>
</ul>
</div>
<div class='main'>
<input type="file" id="modelupload" style="display:none" />
<div class='msg'>
<div id='status1'>Loading...</div>
<div id='status2'></div>
</div>
</div>
<script type="text/javascript">
document.querySelector("#status2").innerHTML = "Fetching pyodide";
</script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{"imports": {"three": "https://unpkg.com/three@0.141.0/build/three.module.js",
"OrbitControls": "https://unpkg.com/three@0.141.0/examples/jsm/controls/OrbitControls.js"}}
</script>
<script type="module">
import * as THREE from 'three';
import {
OrbitControls
} from 'OrbitControls';
THREE.Object3D.DefaultUp = new THREE.Vector3(0, 0, 1);
let pyodide = null;
let previousPoint = null;
let objectMapping = {};
function performDownload(filename, text) {
let element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
async function main() {
document.querySelector("#status2").innerHTML = "Initializing pyodide";
pyodide = await loadPyodide();
document.querySelector("#status2").innerHTML = "Loading dependencies";
await pyodide.loadPackage("micropip");
await pyodide.loadPackage("numpy");
const micropip = pyodide.pyimport("micropip");
await micropip.install("typing-extensions");
document.querySelector("#status2").innerHTML = "Loading IfcOpenShell";
await micropip.install("wheels/ifcopenshell-0.8.1+latest-cp312-cp312-emscripten_3_1_58_wasm32.whl");
document.body.className = '';
let ifcopenshell = pyodide.pyimport('ifcopenshell');
let ifcopenshell_geom = pyodide.pyimport('ifcopenshell.geom');
let s = ifcopenshell_geom.settings();
s.set(s.WELD_VERTICES, false);
// Load custom Python modules
await pyodide.runPythonAsync(`
from pyodide.http import pyfetch
for fn in ['context', 'propertygroups']:
response = await pyfetch(f"./{fn}.py")
with open(f"{fn}.py", "wb") as f:
f.write(await response.bytes())`)
// Initialize model context and make accessibly to JS
pyodide.runPython(`
from context import Context
import numpy as np
modelObject = Context()`);
let modelObject = pyodide.globals.get('modelObject').toJs();
// Menu and toolbar init
let mouseMode = 0;
let buttons = Array.from(document.querySelectorAll('.toolbar li'));
buttons.forEach((el, i) => {
el.onclick = (e) => {
buttons.forEach(el => el.classList.remove('active'));
mouseMode = i;
previousPoint = null;
e.stopPropagation();
el.classList.add('active');
};
});
let newfile = () => {
modelObject.clear();
clearScene();
};
let open = async () => {
let uploadField = document.querySelector('#modelupload');
let input = document.createElement('input');
input.type = 'file';
input.onchange = async () => {
clearScene();
let file = input.files[0];
let contents = await file.text();
modelObject.open(contents);
loadScene();
initCamera();
};
input.click();
};
let save = () => {
performDownload('model.ifc', modelObject.get_model().to_string());
};
let commands = Array.from(document.querySelectorAll('.menu li'));
commands.forEach((el, i) => {
el.onclick = [newfile, open, save][i];
});
let clearScene = () => {
const lights = [];
scene.traverse((obj) => {
if (obj.type.endsWith('Light')) {
lights.push(obj);
}
});
scene.children = lights;
}
// Init ThreeJS
let d = document.querySelector('.main');
const renderer = new THREE.WebGLRenderer();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, d.offsetWidth / d.offsetHeight, 1, 1000);
const controls = new OrbitControls(camera, renderer.domElement);
controls.mouseButtons = {
MIDDLE: THREE.MOUSE.ROTATE,
RIGHT: THREE.MOUSE.PAN
}
renderer.setSize(d.offsetWidth, d.offsetHeight);
d.appendChild(renderer.domElement);
renderer.setClearColor(0x000000, 0);
let light = new THREE.DirectionalLight(0xFFFFFF);
light.position.set(20, 10, 30);
scene.add(light);
light = new THREE.DirectionalLight(0xFFFFFF, 0.8);
light.position.set(-10, 1, -30);
scene.add(light);
scene.add(new THREE.AmbientLight(0x404050));
let lbm = new THREE.LineBasicMaterial({
color: 0x222222
});
// Generate ThreeJS mesh from an IfcOpenShell python geometry object
let last_geometries = null;
function generateMesh(last_mesh_id, obj) {
let geometries;
if (last_mesh_id == obj.geometry.id) {
geometries = last_geometries;
} else {
geometries = [];
let materials = obj.geometry.materials.toJs().map(e => new THREE.MeshLambertMaterial({
color: new THREE.Color(...e.diffuse.components.toJs()),
opacity: 1.0 - e.transparency,
transparent: e.transparency > 1.e-5,
side: THREE.DoubleSide,
polygonOffset: true,
polygonOffsetFactor: 1.0,
}));
let mapping = {};
obj.geometry.material_ids.toJs().forEach((i, idx) => {
mapping[i] = mapping[i] || []
mapping[i].push(idx);
});
let vs = new Float32Array(obj.geometry.verts.toJs());
let ns = new Float32Array(obj.geometry.normals.toJs());
let es = obj.geometry.edges.toJs();
let fs = obj.geometry.faces.toJs();
// Default material
let offset = 0;
if (mapping[-1]) {
materials.unshift(new THREE.MeshLambertMaterial({
color: new THREE.Color(0.6, 0.6, 0.6),
side: THREE.DoubleSide
}));
offset = 1;
}
materials.forEach((m, mi) => {
let geometry = new THREE.BufferGeometry();
geometry.setIndex(mapping[mi - offset].flatMap(i => [fs[3 * i + 0], fs[3 * i + 1], fs[3 * i + 2]]));
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vs, 3));
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(ns, 3));
geometries.push([geometry, m]);
});
let geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(vs, 3));
geometry.setIndex(es);
geometries.push([geometry, lbm]);
last_mesh_id = obj.geometry.id;
}
for (let gm of geometries) {
let [g, mat] = gm;
let object;
if (g.attributes.normal) {
object = new THREE.Mesh(g, mat);
} else {
object = new THREE.LineSegments(g, lbm);
}
let matrix = new THREE.Matrix4();
const m = obj.transformation.data().components.toJs();
matrix.set(
m[0][0], m[0][1], m[0][2], m[0][3],
m[1][0], m[1][1], m[1][2], m[1][3],
m[2][0], m[2][1], m[2][2], m[2][3],
m[3][0], m[3][1], m[3][2], m[3][3]
);
object.matrixAutoUpdate = false;
object.matrix = matrix;
scene.add(object);
objectMapping[object.uuid] = obj.guid;
}
}
function initCamera() {
let boundingBox = new THREE.Box3();
boundingBox.setFromObject(scene);
let center = new THREE.Vector3();
boundingBox.getCenter(center);
controls.target = center;
let viewDistance = boundingBox.isEmpty() ? 100. : boundingBox.getSize(new THREE.Vector3()).length() * 2.;
camera.position.copy(center.clone().add(
new THREE.Vector3(0.25, 1.0, 0.5).normalize().multiplyScalar(viewDistance)
));
camera.near = viewDistance / 100;
camera.far = viewDistance * 100;
controls.update();
camera.updateProjectionMatrix();
camera.updateMatrixWorld();
controls.update();
}
initCamera();
function loadScene() {
document.body.className = 'loading';
document.querySelector("#status2").innerHTML = "Generating geometry";
let ifc = modelObject.get_model();
let it = ifcopenshell_geom.iterator.callKwargs({
'settings': s,
'file_or_filename': ifc,
'exclude': ['IfcSpace', 'IfcOpeningElement'],
'geometry_library': 'hybrid-cgal-simple-opencascade'
});
let last_mesh_id = null;
if (it.initialize()) {
while (true) {
let obj = it.get();
// obj.type appears to be overwritten by pyodide, returning the typename of the C++ class?
let ty = ifc.by_id(obj.id).is_a()
generateMesh(last_mesh_id, obj);
if (!it.next()) {
break;
}
}
}
document.body.className = '';
}
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
}
document.addEventListener('click', onMouseClick);
function getPoint(event) {
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
let rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / (rect.width)) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / (rect.height)) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
if (mouseMode != 1) {
// Select, Add Window/Door
const nonLineObjects = [];
scene.traverse((obj) => {
if (obj.geometry && obj.geometry.attributes.normal) {
nonLineObjects.push(obj);
}
});
let objs = raycaster.intersectObjects(nonLineObjects, false);
if (objs.length) {
let {
point,
object
} = objs[0];
return {
point,
object
};
} else {
return null;
}
} else {
// Polygonal wall
let plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0); // flat ground plane
let point = new THREE.Vector3();
return raycaster.ray.intersectPlane(plane, point);
}
}
function addObjToScene(obj) {
let last_mesh_id = null;
generateMesh(last_mesh_id, obj);
}
function createShape(el) {
addObjToScene(ifcopenshell_geom.create_shape.callKwargs({
'settings': s,
'inst': el,
'geometry_library': 'hybrid-cgal-simple-opencascade'
}));
}
function onMouseClick(event) {
let newPoint = getPoint(event);
if (mouseMode == 1) {
if (previousPoint != null) {
createShape(modelObject.create_2pt_wall([previousPoint.x, previousPoint.y], [newPoint.x, newPoint.y], 0, 3., 0.2, modelObject.storey));
}
previousPoint = newPoint;
}
if (mouseMode == 2 || mouseMode == 3) {
if (newPoint !== null) {
let {
point,
object
} = newPoint;
const guid = objectMapping[object.uuid];
const toRemove = [];
scene.traverse((obj) => {
if (objectMapping[obj.uuid] == guid) {
toRemove.push(obj);
}
});
toRemove.forEach(obj => obj.removeFromParent());
createShape(modelObject.create_fill(mouseMode == 2 ? 'door' : 'window', [point.x, point.y, point.z], guid));
createShape(modelObject.get_element(guid));
}
}
}
render();
}
main();
</script>
</body>
</html>
+152
View File
@@ -0,0 +1,152 @@
from dataclasses import dataclass, field
# From: src\bonsai\bonsai\bim\module\model\prop.py
# Adapted to use dataclasses instead of bpy props
@dataclass
class BIMWindowProperties:
non_si_units_props = ("is_editing", "window_type")
window_types = (
("SINGLE_PANEL", "SINGLE_PANEL", ""),
("DOUBLE_PANEL_HORIZONTAL", "DOUBLE_PANEL_HORIZONTAL", ""),
("DOUBLE_PANEL_VERTICAL", "DOUBLE_PANEL_VERTICAL", ""),
("TRIPLE_PANEL_BOTTOM", "TRIPLE_PANEL_BOTTOM", ""),
("TRIPLE_PANEL_TOP", "TRIPLE_PANEL_TOP", ""),
("TRIPLE_PANEL_LEFT", "TRIPLE_PANEL_LEFT", ""),
("TRIPLE_PANEL_RIGHT", "TRIPLE_PANEL_RIGHT", ""),
("TRIPLE_PANEL_HORIZONTAL", "TRIPLE_PANEL_HORIZONTAL", ""),
("TRIPLE_PANEL_VERTICAL", "TRIPLE_PANEL_VERTICAL", ""),
)
# number of panels and default mullion/transom values
window_types_panels = {
"SINGLE_PANEL": (1, ((0, 0 ), (0, 0 ))),
"DOUBLE_PANEL_HORIZONTAL": (2, ((0, 0 ), (0.45, 0 ))),
"DOUBLE_PANEL_VERTICAL": (2, ((0.3, 0 ), (0, 0 ))),
"TRIPLE_PANEL_BOTTOM": (3, ((0.3, 0 ), (0.45, 0 ))),
"TRIPLE_PANEL_TOP": (3, ((0.3, 0 ), (0.45, 0 ))),
"TRIPLE_PANEL_LEFT": (3, ((0.3, 0 ), (0.45, 0 ))),
"TRIPLE_PANEL_RIGHT": (3, ((0.3, 0 ), (0.45, 0 ))),
"TRIPLE_PANEL_HORIZONTAL": (3, ((0, 0 ), (0.3, 0.6))),
"TRIPLE_PANEL_VERTICAL": (3, ((0.2, 0.4), (0, 0 ))),
}
is_editing: bool = False
window_type: str = "SINGLE_PANEL"
overall_height: float = 0.9
overall_width: float = 0.6
# lining properties
lining_depth: float = 0.050
lining_thickness: float = 0.050
lining_offset: float = 0.050
lining_to_panel_offset_x: float = 0.025
lining_to_panel_offset_y: float = 0.025
mullion_thickness: float = 0.050
first_mullion_offset: float = 0.3
second_mullion_offset: float = 0.45
transom_thickness: float = 0.050
first_transom_offset: float = 0.3
second_transom_offset: float = 0.6
# panel properties
frame_depth: list = field(default_factory = lambda: [0.035] * 3)
frame_thickness: list = field(default_factory = lambda: [0.035] * 3)
def to_dict(self, si_conversion=1.):
di = {
"partition_type": self.window_type,
"overall_height": self.overall_height / si_conversion,
"overall_width": self.overall_width / si_conversion,
"lining_properties": {
"LiningDepth": self.lining_depth / si_conversion,
"LiningThickness": self.lining_thickness / si_conversion,
"LiningOffset": self.lining_offset / si_conversion,
"LiningToPanelOffsetX": self.lining_to_panel_offset_x / si_conversion,
"LiningToPanelOffsetY": self.lining_to_panel_offset_y / si_conversion,
"MullionThickness": self.mullion_thickness / si_conversion,
"FirstMullionOffset": self.first_mullion_offset / si_conversion,
"SecondMullionOffset": self.second_mullion_offset / si_conversion,
"TransomThickness": self.transom_thickness / si_conversion,
"FirstTransomOffset": self.first_transom_offset / si_conversion,
"SecondTransomOffset": self.second_transom_offset / si_conversion,
},
"panel_properties": [],
}
number_of_panels, panels_data = self.window_types_panels[self.window_type]
for panel_i in range(number_of_panels):
panel_data = {
"FrameDepth": self.frame_depth[panel_i] / si_conversion,
"FrameThickness": self.frame_thickness[panel_i] / si_conversion,
}
di["panel_properties"].append(panel_data)
return di
@dataclass
class BIMDoorProperties:
non_si_units_props = ("is_editing", "door_type", "panel_width_ratio")
door_types = (
("SINGLE_SWING_LEFT", "SINGLE_SWING_LEFT", ""),
("SINGLE_SWING_RIGHT", "SINGLE_SWING_RIGHT", ""),
("DOUBLE_SWING_LEFT", "DOUBLE_SWING_LEFT", ""),
("DOUBLE_SWING_RIGHT", "DOUBLE_SWING_RIGHT", ""),
("DOUBLE_DOOR_SINGLE_SWING", "DOUBLE_DOOR_SINGLE_SWING", ""),
("SLIDING_TO_LEFT", "SLIDING_TO_LEFT", ""),
("SLIDING_TO_RIGHT", "SLIDING_TO_RIGHT", ""),
("DOUBLE_DOOR_SLIDING", "DOUBLE_DOOR_SLIDING", ""),
)
is_editing: bool = False
door_type: str = "SINGLE_SWING_LEFT"
overall_height: float = 2.0
overall_width: float = 0.9
# lining properties
lining_depth: float = 0.050
lining_thickness: float = 0.050
lining_offset: float = 0.0
lining_to_panel_offset_x: float = 0.025
lining_to_panel_offset_y: float = 0.025
transom_thickness: float = 0.000
transom_offset: float = 1.525
casing_thickness: float = 0.075
casing_depth: float = 0.005
threshold_thickness: float = 0.025
threshold_depth: float = 0.1
threshold_offset: float = 0.000
# panel properties
panel_depth: float = 0.035
panel_width_ratio: float = 1.0
frame_thickness: float = 0.035
frame_depth: float = 0.035
def to_dict(self, si_conversion=1.):
return {
"operation_type": self.door_type,
"overall_height": self.overall_height / si_conversion,
"overall_width": self.overall_width / si_conversion,
"lining_properties": {
"LiningDepth": self.lining_depth / si_conversion,
"LiningThickness": self.lining_thickness / si_conversion,
"LiningOffset": self.lining_offset / si_conversion,
"LiningToPanelOffsetX": self.lining_to_panel_offset_x / si_conversion,
"LiningToPanelOffsetY": self.lining_to_panel_offset_y / si_conversion,
"TransomThickness": self.transom_thickness / si_conversion,
"TransomOffset": self.transom_offset / si_conversion,
"CasingThickness": self.casing_thickness / si_conversion,
"CasingDepth": self.casing_depth / si_conversion,
"ThresholdThickness": self.threshold_thickness / si_conversion,
"ThresholdDepth": self.threshold_depth / si_conversion,
"ThresholdOffset": self.threshold_offset / si_conversion,
},
"panel_properties": {
"PanelDepth": self.panel_depth / si_conversion,
"PanelWidth": self.panel_width_ratio,
"FrameDepth": self.frame_depth / si_conversion,
"FrameThickness": self.frame_thickness / si_conversion,
},
}
+93
View File
@@ -0,0 +1,93 @@
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
width: 100%;
height: 100vh;
display: grid;
grid-template-columns: 80px 1fr;
grid-template-rows: 80px 1fr;
grid-template-areas:
"menu menu"
"toolbar main";
}
.msg {
padding-top: 25vh;
text-align: center;
}
.msg {
display: none;
}
body.loading .msg {
display: block;
}
form {
display: inline-block;
padding: 1vw 5vw;
border: solid 1px #eee;
border-radius: 1vw;
}
form div {
margin-bottom: 3vw;
}
canvas {
display: block;
}
#status1 {
font-size: 150%;
font-weight: bold;
}
#branding {
position: absolute;
top: 16px;
right: 16px;
text-align: right;
}
input[type='button'] {
padding: 8px 64px;
margin-top: 3vw;
}
ul {
background: #fafafa;
}
ul, ul li {
padding: 0;
margin: 0;
}
ul li {
display: inline-block;
border: solid 1px #ccc;
cursor: pointer;
width: 64px;
height: 64px;
padding: 16px;
border-radius: 8px;
background: #eee;
font-size: 32px !important;
margin: 8px;
}
.tools li {
display: block;
float: left;
}
ul li:hover {
border-color: #aaa;
background: #ddd;
}
.menu {
grid-area: menu;
}
.toolbar {
grid-area: toolbar;
}
.toolbar li.active {
border-color: #888;
background: #ccc;
}
.main {
grid-area: main;
}