awatif-ui
Version:
Awatif User Interface
290 lines (244 loc) • 8.2 kB
text/typescript
import * as THREE from "three";
import van, { State } from "vanjs-core";
import { Node, Mesh } from "awatif-fem";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import {
Settings,
SettingsObj,
getDefaultSettings,
getSettings,
} from "./settings/getSettings";
import { nodes } from "./objects/nodes";
import { elements } from "./objects/elements";
import { grid } from "./objects/grid";
import { supports } from "./objects/supports";
import { loads } from "./objects/loads";
import { nodesIndexes } from "./objects/nodesIndexes";
import { elementsIndexes } from "./objects/elementsIndexes";
import { axes } from "./objects/axes";
import { orientations } from "./objects/orientations";
import { frameResults } from "./objects/frameResults";
import { nodeResults } from "./objects/nodeResults";
import { drawing, Drawing } from "./drawing/drawing";
import { shellResults } from "./objects/shellResults";
import "./styles.css";
import { getLegend } from "../color-map/getLegend";
export function getViewer({
mesh,
settingsObj,
drawingObj,
objects3D,
solids,
}: {
mesh?: Mesh;
settingsObj?: SettingsObj;
drawingObj?: Drawing;
objects3D?: State<THREE.Object3D[]>;
solids?: State<THREE.Object3D[]>;
}): HTMLDivElement {
// init
THREE.Object3D.DEFAULT_UP = new THREE.Vector3(0, 0, 1);
const viewerElm = document.createElement("div");
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
1,
0.1,
2 * 1e6 // supported view till 1e6
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
const controls = new OrbitControls(camera, renderer.domElement);
const settings = getDefaultSettings(settingsObj);
const derivedDisplayScale = van.derive(() =>
settings.displayScale.val === 0
? 1
: settings.displayScale.val > 0
? settings.displayScale.val
: -1 / settings.displayScale.val
);
const derivedNodes = deriveNodes(mesh, settings);
const gridObj = grid(settings.gridSize.rawVal);
// update
viewerElm.appendChild(getSettings(settings, mesh, solids));
viewerElm.setAttribute("id", "viewer");
viewerElm.appendChild(renderer.domElement);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x000000, 1);
const gridSize = settings.gridSize.rawVal;
const z2fit = gridSize * 0.5 + (gridSize * 0.5) / Math.tan(45 * 0.5);
camera.position.set(0.5 * gridSize, 0.8 * -z2fit, 0.5 * gridSize);
controls.target.set(0.5 * gridSize, 0.5 * gridSize, 0);
controls.minDistance = 1;
controls.maxDistance = z2fit * 2.5;
controls.zoomSpeed = 10;
controls.update();
scene.add(gridObj, axes(settings.gridSize.rawVal, settings.flipAxes.rawVal));
// Events
// on size change
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.target?.clientWidth;
const height = entry.target?.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
viewerRender();
}
});
resizeObserver.observe(viewerElm);
// on controls change
controls.addEventListener("change", viewerRender);
// on mesh or settings change: render
van.derive(() => {
mesh?.nodes?.val;
mesh?.elements?.val;
mesh?.nodeInputs?.val;
mesh?.elementInputs?.val;
mesh?.deformOutputs?.val;
mesh?.analyzeOutputs?.val;
settings.displayScale.val;
settings.nodes.val;
settings.elements.val;
settings.nodesIndexes.val;
settings.elementsIndexes.val;
settings.orientations.val;
settings.supports.val;
settings.loads.val;
settings.deformedShape.val;
settings.nodeResults.val;
settings.frameResults.val;
settings.shellResults.val;
setTimeout(viewerRender); // setTimeout to ensure render is called after all updates are done in that event tick
});
// Object's functions (Actions)
function viewerRender() {
renderer.render(scene, camera);
}
// Optional inputs
if (mesh) {
// 3D objects
scene.add(
nodes(settings, derivedNodes, derivedDisplayScale),
elements(mesh, settings, derivedNodes),
nodesIndexes(settings, derivedNodes, derivedDisplayScale),
elementsIndexes(mesh, settings, derivedNodes, derivedDisplayScale),
supports(mesh, settings, derivedNodes, derivedDisplayScale),
loads(mesh, settings, derivedNodes, derivedDisplayScale),
orientations(mesh, settings, derivedNodes, derivedDisplayScale),
nodeResults(mesh, settings, derivedNodes, derivedDisplayScale),
frameResults(mesh, settings, derivedNodes, derivedDisplayScale)
);
// Color map
const colorMapValues = getColorMapValues(mesh, settings);
const shellResultsObj = shellResults(
mesh,
settings,
derivedNodes,
colorMapValues
);
const legend = getLegend(colorMapValues);
scene.add(shellResultsObj);
viewerElm.appendChild(legend);
van.derive(() => {
legend.hidden = settings.shellResults.val == "none";
shellResultsObj.visible = settings.shellResults.val != "none";
});
}
if (solids) {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const light1 = new THREE.DirectionalLight(0xffffff, 0.5);
light1.position.set(30, 25, -10);
light1.shadow.mapSize.width = 1024;
light1.shadow.mapSize.height = 1024;
scene.add(light1);
const d = 10;
light1.shadow.camera.left = -d;
light1.shadow.camera.right = d;
light1.shadow.camera.top = d;
light1.shadow.camera.bottom = -d;
light1.shadow.camera.far = 1000;
const light2 = new THREE.DirectionalLight(0xffffff, 0.5);
light2.color.setHSL(11, 43, 96);
light2.position.set(-10, 0, 30);
scene.add(light2);
// Events: on solids change add/remove objects from the scene
van.derive(() => {
if (!solids?.val.length) return;
scene.remove(...solids.oldVal);
scene.add(...solids.rawVal);
viewerRender();
});
// Events: on solids settings change update visibility
van.derive(() => {
solids.rawVal.forEach((solid) => (solid.visible = settings.solids.val));
viewerRender();
});
}
if (objects3D) {
// Events: on objects3D change add/remove objects from the scene
van.derive(() => {
if (!objects3D?.val.length) return;
scene.remove(...objects3D.oldVal);
scene.add(...objects3D.rawVal);
viewerRender();
});
}
if (drawingObj)
drawing({
drawingObj,
gridObj,
scene,
camera,
controls,
gridSize,
derivedDisplayScale,
viewerRender,
});
return viewerElm;
}
// Utils
function deriveNodes(
mesh: Mesh | undefined,
settings: Settings
): Mesh["nodes"] {
return van.derive(() => {
if (!settings.deformedShape.val) return mesh?.nodes?.val ?? [];
return (
mesh?.nodes?.val.map((node, index) => {
const d = mesh?.deformOutputs?.val.deformations
?.get(index)
?.slice(0, 3) ?? [0, 0, 0];
return node.map((n, i) => n + d[i]) as Node;
}) ?? []
);
});
}
function getColorMapValues(mesh: Mesh, settings: Settings): State<number[]> {
// Init
const colorMapValues = van.state([]);
enum ResultType {
bendingXX = "bendingXX",
bendingYY = "bendingYY",
bendingXY = "bendingXY",
displacementZ = "displacementZ",
}
// Events
// On resultMapper, nodes, settings.shellResults change: get new values
van.derive(() => {
const resultMapper = {
[ResultType.bendingXX]: [mesh.analyzeOutputs?.val.bendingXX, 0],
[ResultType.bendingYY]: [mesh.analyzeOutputs?.val.bendingYY, 0],
[ResultType.bendingXY]: [mesh.analyzeOutputs?.val.bendingXY, 0],
[ResultType.displacementZ]: [mesh.deformOutputs?.val.deformations, 2],
};
const values = [];
mesh.nodes.val.forEach((_, i) => {
const resultMap = resultMapper[settings.shellResults.val];
if (!resultMap) return;
values.push(resultMap[0].get(i)[resultMap[1]]);
});
colorMapValues.val = values;
});
return colorMapValues;
}