Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: vanilla ts example for wind prediction #35

Merged
merged 2 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions analyses/predictive-wind/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
20 changes: 20 additions & 0 deletions analyses/predictive-wind/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Predictive wind example</title>
</head>
<body>
<div style="display: flex; flex-direction: column">
<h1>Predictive wind example</h1>
<select name="comfortScale" id="comfortScale">
<option value="lawson_lddc">lawson_lddc</option>
<option value="davenport">davenport</option>
<option value="nen8100">nen8100</option>
</select>
<button id="predictButton">Click here, then scene position</button>
<div id="info"></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions analyses/predictive-wind/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "predictive-wind",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"forma-embedded-view-sdk": "^0.54.1",
"three": "^0.157.0",
"three-mesh-bvh": "^0.6.8"
},
"devDependencies": {
"typescript": "^5.2.2",
"vite": "^5.1.4",
"@types/three": "^0.157.0"
}
}
18 changes: 18 additions & 0 deletions analyses/predictive-wind/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Vanilla TS Example for Wind Prediction

This directory is a simple example of how to do wind predictions from an embedded view.

- get desired position using `Forma.designTool.getPoint()`
- Generate height maps using raycasting in `generateHeightMaps`
- Perform predition using `Forma.predictiveAnalysis.predictWind`
- Draw results to scene using `Forma.terrain.groundTexture.add`

There has been done simple sanity check of comparing results from this approach with the built in feature, but make no promises that the exact same geometry is included in the height map.

## Usage

`yarn && yarn dev` will run the extension on `http://localhost:5173`.

Configure your extension to load `localhost:5173`, and open the extension.

See https://aps.autodesk.com/en/docs/forma/v1/overview/getting-started/ for more information on how to getting started.
138 changes: 138 additions & 0 deletions analyses/predictive-wind/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Forma } from "forma-embedded-view-sdk/auto";
import * as THREE from "three";
import { generateHeightMaps } from "./raycastProcessor";
import { computeBoundsTree, disposeBoundsTree } from "three-mesh-bvh";
import { PredictiveAnalysisGroundGrid } from "forma-embedded-view-sdk/predictive-analysis";

// Speed up raycasting using https://github.com/gkjohnson/three-mesh-bvh
// @ts-ignore
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
// @ts-ignore
THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;

export async function predictWind() {
const infoElement = document.getElementById("info") as HTMLDivElement;
infoElement.innerHTML = "Click on the map to predict wind";
const position = await Forma.designTool.getPoint();
if (!position) {
console.log("need a position");
return;
}
infoElement.innerHTML = `Predicting wind at ${position.x.toFixed(
2
)}, ${position.y.toFixed(2)}. It might take a second or two...`;

const [windParameters, terrainScene, allScene] = await Promise.all([
Forma.predictiveAnalysis.getWindParameters(),
getSceneWithTerrain(),
getSceneWithoutVirtual(),
]);

const heightMaps = generateHeightMaps(terrainScene, allScene, [
position.x,
position.y,
]);

const comfortScale = (
document.getElementById("comfortScale") as HTMLSelectElement
).value as "lawson_lddc" | "davenport" | "nen8100";

const predictionResult = await Forma.predictiveAnalysis.predictWind({
heightMaps: heightMaps,
windRose: {
data: windParameters.data,
height: windParameters.height,
},
type: "comfort",
roughness: windParameters.roughness,
comfortScale: comfortScale,
});

const canvas = gridToCanvas(predictionResult);
Forma.terrain.groundTexture.add({
name: "Wind Prediction",
canvas: canvas,
position,
scale: predictionResult.scale,
});
Forma.colorbar.add({
colors: ["#B2F8DA", "#55DCA2", "#FED52A", "#FFA900", "#FF463A"],
labels: ["Sitting", "Standing", "Strolling", "Walking", "Uncomfortable"],
labelPosition: "center",
});
infoElement.innerHTML = "Wind prediction added to the map";
}

const button = document.getElementById("predictButton");
if (button) {
button.onclick = predictWind;
}

const colorScale = ["#B2F8DA", "#55DCA2", "#FED52A", "#FFA900", "#FF463A"];

function gridToCanvas({ grid, width, height }: PredictiveAnalysisGroundGrid) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
canvas.height = height;
canvas.width = width;

for (let index = 0; index < grid.length; index++) {
const column = Math.floor(index % canvas.width);
const row = Math.floor(index / canvas.width);

if (!isNaN(grid[index])) {
const v = grid[index];
const colorIndex = Math.round(v);
ctx.fillStyle = colorScale[colorIndex];
ctx.fillRect(column, row, 1, 1);
}
}
return canvas;
}

async function getSceneWithTerrain() {
const terrainScene = new THREE.Scene();
const terrainPath = await Forma.geometry.getPathsByCategory({
category: "terrain",
});
const terrainVertexPositions = await Forma.geometry.getTriangles({
path: terrainPath[0],
});
const terrainGeometry = new THREE.BufferGeometry();
terrainGeometry.setAttribute(
"position",
new THREE.BufferAttribute(terrainVertexPositions, 3)
);
// @ts-ignore
terrainGeometry.computeBoundsTree();
terrainScene.add(
new THREE.Mesh(
terrainGeometry,
new THREE.MeshBasicMaterial({ color: 0xff0000 })
)
);
return terrainScene;
}

async function getSceneWithoutVirtual() {
const virtualPaths = await Forma.geometry.getPathsForVirtualElements();
const allScene = new THREE.Scene();
const allGeometryVertexPositions = await Forma.geometry.getTriangles({
path: "root",
excludedPaths: virtualPaths,
});
const allGeometry = new THREE.BufferGeometry();
allGeometry.setAttribute(
"position",
new THREE.BufferAttribute(allGeometryVertexPositions, 3)
);
// @ts-ignore
allGeometry.computeBoundsTree();
allScene.add(
new THREE.Mesh(
allGeometry,
new THREE.MeshBasicMaterial({ color: 0xff0000 })
)
);
return allScene;
}
70 changes: 70 additions & 0 deletions analyses/predictive-wind/src/raycastProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { HeightMaps } from "forma-embedded-view-sdk/predictive-analysis";
import * as THREE from "three";
import { acceleratedRaycast } from "three-mesh-bvh";

THREE.Mesh.prototype.raycast = acceleratedRaycast;
const raycaster = new THREE.Raycaster();
// For this analysis we only need the first hit, which is faster to compute
// @ts-ignore
raycaster.firstHitOnly = true;

function getMinMax(array: number[]) {
return array.reduce(
([min, max], val) => [Math.min(min, val), Math.max(max, val)],
[Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]
);
}

/**
* Simple approach to generates height maps using raycasting in three.js
* @param terrainScene a THREE scene containing only the terrain the terrain
* @param allScene a THREE scene containing terrain and volumes
* @param centerPosition The center position you want height maps for
* @returns Column major height maps with 500x500 grid points and a resolution of 1.5m.
*/
export function generateHeightMaps(
terrainScene: THREE.Scene,
allScene: THREE.Scene,
centerPosition: [number, number]
): HeightMaps {
console.time("generateHeightMaps");
const direction = new THREE.Vector3(0, 0, -1);
//@ts-ignore
const terrainResult = [];
const allResult = [];
for (let y = 249.5; y > -250.5; y--) {
for (let x = -249.5; x < 250.5; x++) {
const origin = new THREE.Vector3(
x * 1.5 + centerPosition[0],
y * 1.5 + centerPosition[1],
10000
);
raycaster.set(origin, direction);
const terrainIntersection = raycaster.intersectObjects(
terrainScene.children
)[0];
const allIntersection = raycaster.intersectObjects(allScene.children)[0];

terrainResult.push(terrainIntersection.point.z);
allResult.push(allIntersection.point.z);
}
}
const allMinMax = getMinMax(allResult.concat(terrainResult));
const scaledTerrainResult = new Uint8Array(terrainResult.length);
const scaledAllResult = new Uint8Array(allResult.length);
for (let i = 0; i < allResult.length; i++) {
scaledTerrainResult[i] = Math.round(
((terrainResult[i] - allMinMax[0]) / (allMinMax[1] - allMinMax[0])) * 255
);
scaledAllResult[i] = Math.round(
((allResult[i] - allMinMax[0]) / (allMinMax[1] - allMinMax[0])) * 255
);
}
console.timeEnd("generateHeightMaps");
return {
terrainHeightArray: Array.from(scaledTerrainResult),
minHeight: allMinMax[0],
maxHeight: allMinMax[1],
buildingAndTerrainHeightArray: Array.from(scaledAllResult),
};
}
1 change: 1 addition & 0 deletions analyses/predictive-wind/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
23 changes: 23 additions & 0 deletions analyses/predictive-wind/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
Loading