-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: vanilla ts example for wind prediction
This branch 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` I've done simple sanity check of comparing this approach with the one done by the built in feature, but make no promises that I've included the exact same geometry in the height map.
- Loading branch information
1 parent
caef0ec
commit d2cdba0
Showing
9 changed files
with
658 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
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 | ||
); | ||
console.log(x, y); | ||
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), | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/// <reference types="vite/client" /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
Oops, something went wrong.