<ul class="flex-container">
<li class="flex-item">
<p>1) Polygone initial</p>
<canvas id="initial2d"></canvas>
</li>
<li class="flex-item">
<p>2) Squelette droit du polygone</p>
<canvas id="canvas_skeleton"></canvas>
</li>
<li class="flex-item">
<p>3) Squelette droit après post-traitement</p>
<canvas id="canvas_post_process"></canvas>
</li>
<li class="flex-item">
<p>4) Calcul de Dmax = <b id="hmax">...</b> </p>
<canvas id="canvas_post_process_vertices"></canvas>
</li>
<li class="flex-item">
<p>5) Triangulation du squelette droit </p>
<canvas id="canvas_triangulated_skeleton"></canvas>
</li>
<li class="flex-item-three-js">
<p>6) Création toiture </p>
<canvas id="scene_roof"> </canvas>
</li>
<li class="flex-item-three-js">
<p>7) Création façade </p>
<canvas id="walls_scene"> </canvas>
</li>
<li class="flex-item-building-three-js">
<p>8) Bâtiment complet </p>
<canvas id="building_scene"> </canvas>
</li>
</ul>
.flex-container {
/* We first create a flex layout context */
display: flex;
/* Then we define the flow direction
and if we allow the items to wrap
* Remember this is the same as:
* flex-direction: row;
* flex-wrap: wrap;
*/
flex-flow: row wrap;
/* Then we define how is distributed the remaining space */
justify-content: space-around;
padding: 0;
margin: 0;
list-style: none;
}
.flex-item {
padding: 5px;
width: 32%;
height: 300px;
margin-top: 10px;
color: black;
font-weight: bold;
text-align: center;
border: 1px solid black;
canvas {
height: 250px;
width: 100%;
}
}
.flex-item-three-js {
padding: 5px;
width: 32%;
height: 300px;
margin-top: 10px;
color: black;
font-weight: bold;
text-align: center;
border: 1px solid black;
canvas {
height: 250px !important;
width: 100% !important;
}
}
.flex-item-building-three-js {
padding: 5px;
width: 100%;
height: 350px;
margin-top: 10px;
color: black;
font-weight: bold;
text-align: center;
border: 1px solid black;
canvas {
height: 300px !important;
width: 100% !important;
}
}
import {
Vector2,
Vector3,
Box3,
Mesh,
PerspectiveCamera,
PointsMaterial,
MeshBasicMaterial,
Scene,
Color,
DoubleSide,
WebGLRenderer,
BufferGeometry,
Points,
Float32BufferAttribute
} from "https://esm.sh/three";
import {
OrbitControls,
TextGeometry,
FontLoader
} from "https://esm.sh/three/addons";
import * as BufferGeometryUtils from "https://esm.sh/three/addons/utils/BufferGeometryUtils.js";
import straightSkeleton from "https://esm.sh/straight-skeleton";
import earcut from "https://esm.sh/earcut";
const initialPolygon = {
type: "Polygon",
coordinates: [
[
[259521.14076069795, 6253167.799861707],
[259606.34863939675, 6253192.28359733],
[259590.5611086523, 6253248.230426137],
[259505.31590718575, 6253223.7466905145],
[259521.14076069795, 6253167.799861707]
]
]
};
const roofMinHeight = 48;
const roofMaxHeight = 58;
const roofHeight = 10;
const minHeight = 31;
/////////////// Affichage du polygone initial + construction du skeleton droit et son affichage //////////////////:
const drawInitial2d = (skeletonBox) => {
// 2D canvas
const initial2d = document.getElementById("initial2d");
const initialCtx = initial2d.getContext("2d");
initial2d.width = initial2d.clientWidth * window.devicePixelRatio;
initial2d.height = initial2d.clientHeight * window.devicePixelRatio;
initialCtx.fillStyle = "#eee";
initialCtx.fillRect(0, 0, initial2d.width, initial2d.height);
const padding = 15 * window.devicePixelRatio;
const scale = Math.min(
(initial2d.width - padding * 2) / (skeletonBox.maxX - skeletonBox.minX),
(initial2d.height - padding * 2) / (skeletonBox.maxY - skeletonBox.minY)
);
const offsetX =
(initial2d.width - (skeletonBox.maxX - skeletonBox.minX) * scale) / 2;
const offsetY =
(initial2d.height - (skeletonBox.maxY - skeletonBox.minY) * scale) / 2;
initialCtx.strokeStyle = "#000";
initialCtx.lineWidth = window.devicePixelRatio;
initialCtx.fillStyle = "#ffb6e9";
for (const polygon of initialPolygon.coordinates) {
initialCtx.beginPath();
for (let i = 0; i < polygon.length; i++) {
const vertex = polygon[i];
const x = (vertex[0] - skeletonBox.minX) * scale + offsetX;
const y = (vertex[1] - skeletonBox.minY) * scale + offsetY;
if (i === 0) {
initialCtx.moveTo(x, y);
} else {
initialCtx.lineTo(x, y);
}
}
initialCtx.closePath();
initialCtx.stroke();
initialCtx.fill();
}
};
const draw2d = (
skeletonBox,
skeleton,
cansvas_id,
draw_triangle = false,
burst_geometry = false,
display_vertices = false
) => {
// 2D canvas
const canvas2d = document.getElementById(cansvas_id);
const ctx = canvas2d.getContext("2d");
canvas2d.width = canvas2d.clientWidth * window.devicePixelRatio;
canvas2d.height = canvas2d.clientHeight * window.devicePixelRatio;
ctx.fillStyle = "#eee";
ctx.fillRect(0, 0, canvas2d.width, canvas2d.height);
const padding = 15 * window.devicePixelRatio;
const scale = Math.min(
(canvas2d.width - padding * 2) / (skeletonBox.maxX - skeletonBox.minX),
(canvas2d.height - padding * 2) / (skeletonBox.maxY - skeletonBox.minY)
);
const offsetX =
(canvas2d.width - (skeletonBox.maxX - skeletonBox.minX) * scale) / 2;
const offsetY =
(canvas2d.height - (skeletonBox.maxY - skeletonBox.minY) * scale) / 2;
ctx.strokeStyle = "#000";
ctx.lineWidth = window.devicePixelRatio;
ctx.fillStyle = "#ffb6e9";
for (const polygon of skeleton.polygons) {
ctx.beginPath();
if (polygon.length == 4) {
ctx.fillStyle = "green";
} else {
ctx.fillStyle = "#ffb6e9";
}
const vertices = [];
if (Array.isArray(polygon) == false) {
for (let i = 0; i < polygon.vertices.length; i++) {
const vertex = polygon.vertices[i];
const x = (vertex.x - skeletonBox.minX) * scale + offsetX;
let y = (vertex.y - skeletonBox.minY) * scale + offsetY;
if (burst_geometry) {
let isFirstPolygon =
skeleton.polygons
.filter((p) => p.vertices.length > 0)
.indexOf(polygon) == 0;
if (isFirstPolygon) {
y -= 20;
}
}
let position = "";
if (i == 0) {
position = "start";
} else if (i == polygon.vertices.length - 1) {
position = "end";
}
vertices.push([position, x, y]);
if (draw_triangle) {
if (i % 3 === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
} else {
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
}
} else {
for (let i = 0; i < polygon.length; i++) {
const vertex = skeleton.vertices[polygon[i]];
const x = (vertex[0] - skeletonBox.minX) * scale + offsetX;
const y = (vertex[1] - skeletonBox.minY) * scale + offsetY;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
}
ctx.closePath();
ctx.fill();
ctx.stroke();
if (burst_geometry) {
for (const vertice of vertices) {
ctx.beginPath();
ctx.fillStyle = "black";
ctx.font = "25px serif";
ctx.fillText(vertice[0], vertice[1], vertice[2]);
ctx.fillStyle = "red";
ctx.arc(vertice[1], vertice[2], 5, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
}
}
};
straightSkeleton.SkeletonBuilder.init().then(() => {
// Construction du skeleton
const skeleton = straightSkeleton.SkeletonBuilder.buildFromGeoJSONPolygon(
initialPolygon
);
// Construction de l'étendue de la géométrie afin de centrer la géométrie dans la vue
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const vertex of skeleton.vertices) {
minX = Math.min(minX, vertex[0]);
minY = Math.min(minY, vertex[1]);
maxX = Math.max(maxX, vertex[0]);
maxY = Math.max(maxY, vertex[1]);
}
const skeletonBox = { minX, minY, maxX, maxY };
drawInitial2d(skeletonBox);
draw2d(skeletonBox, skeleton, "canvas_skeleton");
postProcessSkeleton(skeleton, skeletonBox);
});
/////////////// Post traitement du sekeleton droit + affichage ///////////////
export class StraightSkeletonResult {
vertices: Vector2[];
polygons: {
vertices: Vector2[];
edgeStart: Vector2;
edgeEnd: Vector2;
}[];
constructor(source) {
this.vertices = source.vertices.map((v) => new Vector2(v[0], v[1]));
this.polygons = source.polygons.map((p) => {
const vertices = p.map((v) => this.vertices[v]);
return {
vertices: vertices,
edgeStart: vertices[vertices.length - 1],
edgeEnd: vertices[0]
};
});
}
}
const postProcessSkeleton = (skeleton, skeletonBox) => {
const skeletonResult = new StraightSkeletonResult(skeleton);
for (let i = 0; i < skeletonResult.polygons.length; i++) {
const polygon = skeletonResult.polygons[i];
// Pour les polygone roses (ceux dont on doit conserver uniquement les sommets de fins et de debut)
// On recherchera les polygones rouges du skeleton adjacents à leurs extrémités
if (polygon.vertices.length === 3) {
// Polygone de notre skeleton adjacent au sommet du debut
const prevPolygon = skeletonResult.polygons.find(
(p) => p.edgeEnd.equals(polygon.edgeStart) && p.vertices.length > 3
);
// Polygone de notre skeleton adjacent au sommet de fin
const nextPolygon = skeletonResult.polygons.find(
(p) => p.edgeStart.equals(polygon.edgeEnd) && p.vertices.length > 3
);
if (prevPolygon && nextPolygon) {
// Projection des polygones rouges adjacents à notre polygone rose
// Le seul sommet de notre polygone rose qui est ni sa fin, ni son debut
const extrudedPoint = polygon.vertices.find((p) => {
return !p.equals(polygon.edgeStart) && !p.equals(polygon.edgeEnd);
});
// Le sommet de nos polygones rouges qui sont à projeter
const prevPolygonExtrudedPoint = prevPolygon.vertices.find((v) =>
v.equals(extrudedPoint)
);
const nextPolygonExtrudedPoint = nextPolygon.vertices.find((v) =>
v.equals(extrudedPoint)
);
// Le sommet situé au milieu du segment[sommet debut, sommet fin] de notre polygone rose
// Qui correspond au sommet souhaité pour notre projection
const middle = new Vector2()
.addVectors(polygon.edgeStart, polygon.edgeEnd)
.multiplyScalar(0.5);
prevPolygonExtrudedPoint.x = nextPolygonExtrudedPoint.x = middle.x;
prevPolygonExtrudedPoint.y = nextPolygonExtrudedPoint.y = middle.y;
// Projection faite, on conserve uniquement les sommets de debut et fin de notre polygone rose
polygon.vertices = [];
}
}
}
draw2d(skeletonBox, skeletonResult, "canvas_post_process");
evaluateHmax(skeletonResult, skeletonBox);
};
/////////////// Evaluation du Dmax et visualisation des sommets ///////////////
const _signedDistanceToLine = function (point, line) {
const lineVector = new Vector2().subVectors(line[1], line[0]);
const pointVector = new Vector2().subVectors(point, line[0]);
const cross = lineVector.x * pointVector.y - lineVector.y * pointVector.x;
const lineLength = lineVector.length();
return cross / lineLength;
};
const evaluateHmax = (skeletonPostProcessingResult, skeletonBox) => {
const SkeletonHeights = (function () {
let heights = [];
for (const polygon of skeletonPostProcessingResult.polygons) {
const edgeLine = [polygon.edgeStart, polygon.edgeEnd];
for (const vertex of polygon.vertices) {
const dst = _signedDistanceToLine(vertex, edgeLine);
heights.push(dst);
}
}
return heights;
})();
const hMax = Math.max(SkeletonHeights);
document.getElementById("hmax").firstChild.textContent = hMax;
draw2d(
skeletonBox,
skeletonPostProcessingResult,
"canvas_post_process_vertices",
false,
true,
true
);
triangulateSkeleton(skeletonPostProcessingResult, skeletonBox, hMax);
};
/////////////// Triangulation du skeleton droit + affichage ///////////////
const triangulatedPolygon: {
polygons: {
vertices: Vector2[];
}[];
} = {
polygons: []
};
const triangulateSkeleton = (
skeletonPostProcessingResult,
skeletonBox,
hMax
) => {
for (const polygon of skeletonPostProcessingResult.polygons) {
const polygonVertices: number[] = [];
const polygonTriangleVertices: Vector2[] = [];
triangulatedPolygon.polygons.push({ vertices: polygonTriangleVertices });
for (const vertex of polygon.vertices) {
polygonVertices.push(vertex.x, vertex.y);
}
const triangles = earcut(polygonVertices).reverse();
for (let i = 0; i < triangles.length; i++) {
const index = triangles[i];
const x = polygonVertices[index * 2];
const y = polygonVertices[index * 2 + 1];
const vertex = new Vector2(x, y);
polygonTriangleVertices.push(vertex);
}
}
draw2d(
skeletonBox,
triangulatedPolygon,
"canvas_triangulated_skeleton",
true
);
addZtoVerticesToCreateRoof(skeletonPostProcessingResult, skeletonBox, hMax);
};
/////////////// Affectation des altitudes pour création de la toiture et son affichage ///////////////
const addZtoVerticesToCreateRoof = (
skeletonPostProcessingResult,
skeletonBox,
hMax
) => {
const roofPositions: Vector3[] = [];
for (const polygon of skeletonPostProcessingResult.polygons) {
const polygonVertices: number[] = [];
const edgeLine = [
new Vector2(polygon.edgeStart.x, polygon.edgeStart.y),
new Vector2(polygon.edgeEnd.x, polygon.edgeEnd.y)
];
for (const vertex of polygon.vertices) {
polygonVertices.push(vertex.x, vertex.y);
}
const triangles = earcut(polygonVertices).reverse();
for (let i = 0; i < triangles.length; i++) {
const index = triangles[i];
const x = polygonVertices[index * 2];
const y = polygonVertices[index * 2 + 1];
const vertex = new Vector2(x, y);
// distance maximale entre le point "vertex" et le vecteur [sommet debut, sommet fin] "edgeLine"
const dst = _signedDistanceToLine(vertex, edgeLine);
// Interpolation de l'altitude du point "vertex"
const vertexZ = roofMinHeight + (roofHeight * dst) / hMax;
roofPositions.push(new Vector3(x, y, vertexZ));
}
}
displayRoof(skeletonPostProcessingResult, skeletonBox, hMax, roofPositions);
};
const displayRoof = (
skeletonPostProcessingResult,
skeletonBox,
hMax,
positions
) => {
let camera, scene, renderer, controls;
const loader = new FontLoader();
////// Roof polygons
const material = new MeshBasicMaterial({
color: "#ffb6e9",
side: DoubleSide,
wireframe: false
});
const roofGeometry = new BufferGeometry();
roofGeometry.setFromPoints(positions);
const roofMesh = new Mesh(roofGeometry, material);
const scene_canvas = document.getElementById("scene_roof");
const center = new Vector3(
skeletonBox.minX + (skeletonBox.maxX - skeletonBox.minX) / 2,
skeletonBox.minY + (skeletonBox.maxY - skeletonBox.minY) / 2,
roofMinHeight + roofHeight / 2
);
function init() {
renderer = new WebGLRenderer({ canvas: scene_canvas });
renderer.setSize(scene_canvas.clientWidth, scene_canvas.clientHeight);
scene = new Scene();
scene.background = new Color("white");
camera = new PerspectiveCamera(
30,
scene_canvas.clientWidth / scene_canvas.clientHeight,
1,
10000
);
// Our up axes here is the Z
camera.up.set(0, 0, 1);
controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(skeletonBox.maxX, skeletonBox.minY, minHeight * 4);
camera.lookAt(center);
controls.target.copy(center);
controls.update();
scene.add(roofMesh);
window.addEventListener("resize", onWindowResize);
}
function onWindowResize() {
camera.aspect = scene_canvas.clientWidth / scene_canvas.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(scene_canvas.clientWidth, scene_canvas.clientHeight);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
init();
animate();
// display Z labels
loader.load(
"https://esm.sh/three@0.172.0/examples/fonts/helvetiker_regular.typeface.json",
function (font) {
for (let index = 0; index < positions.length; index++) {
const roofVertice = positions[index];
const labelMesh = getPointLabelMesh(
font,
Math.ceil(roofVertice.z) + " m"
);
labelMesh.position.set(roofVertice.x, roofVertice.y, roofVertice.z);
scene.add(labelMesh);
//console.log(labelMesh, roofVertice);
}
}
);
const getPointLabelMesh = (font, label) => {
const geometry = new TextGeometry(label, {
font: font,
size: 2,
depth: 0.1
});
const labelMesh = new Mesh(
geometry,
new MeshBasicMaterial({
color: "black",
side: DoubleSide
})
);
labelMesh.rotation.y = Math.PI / 2;
labelMesh.rotation.z = Math.PI / 2;
return labelMesh;
};
displayWalls(skeletonPostProcessingResult, skeletonBox, hMax, roofGeometry);
};
/////////////// Modélisation des façades et leurs affichages ///////////////
function createWallTriangles(positions: Array<Vector3>) {
const postionsResult = positions.slice();
for (let index = 0; index < positions.length; index++) {
const A = positions[index];
const B = positions[index + 1] ? positions[index + 1] : positions[0];
// Triangle ABA'
postionsResult.push(A); // A
postionsResult.push(B); // B
postionsResult.push(new Vector3(A.x, A.y, minHeight)); // A'
// Triangle A',B,B'
postionsResult.push(new Vector3(A.x, A.y, minHeight)); // A'
postionsResult.push(B); // B
postionsResult.push(new Vector3(B.x, B.y, minHeight)); // B'
}
return postionsResult;
}
const displayWalls = (
skeletonPostProcessingResult,
skeletonBox,
Hmax,
roofGeometry
) => {
const vertices: Vector3[] = [];
let camera, scene, renderer, controls;
for (const polygon of skeletonPostProcessingResult.polygons) {
const edgeLine = [polygon.edgeStart, polygon.edgeEnd];
// les polygones avec uniquement 2 sommets
if (polygon.vertices.length == 0) {
const middle = new Vector2()
.addVectors(polygon.edgeStart, polygon.edgeEnd)
.multiplyScalar(0.5);
const startDst = _signedDistanceToLine(polygon.edgeStart, edgeLine);
const endDst = _signedDistanceToLine(polygon.edgeEnd, edgeLine);
// A
vertices.push(
new Vector3(
polygon.edgeStart.x,
polygon.edgeStart.y,
roofMinHeight + (roofHeight * startDst) / Hmax
)
);
// B
vertices.push(new Vector3(middle.x, middle.y, roofMaxHeight));
// C
vertices.push(
new Vector3(
polygon.edgeEnd.x,
polygon.edgeEnd.y,
roofMinHeight + (roofHeight * endDst) / Hmax
)
);
}
}
const wallTrianglesVertices = createWallTriangles(vertices);
//const material = new PointsMaterial({ color: 0x888888, size: 10 });
const material = new MeshBasicMaterial({
color: "#ffb6e9",
side: DoubleSide
});
const wallGeometry = new BufferGeometry();
wallGeometry.setFromPoints(wallTrianglesVertices);
const wallMesh = new Mesh(wallGeometry, material);
const center = new Vector3(
skeletonBox.minX + (skeletonBox.maxX - skeletonBox.minX) / 2,
skeletonBox.minY + (skeletonBox.maxY - skeletonBox.minY) / 2,
minHeight + roofHeight / 2
);
const walls_scene = document.getElementById("walls_scene");
function init() {
renderer = new WebGLRenderer({ canvas: walls_scene });
renderer.setSize(walls_scene.clientWidth, walls_scene.clientHeight);
scene = new Scene();
scene.background = new Color("white");
camera = new PerspectiveCamera(
70,
walls_scene.clientWidth / walls_scene.clientHeight,
1,
10000
);
// Our up axes here is the Z
camera.up.set(0, 0, 1);
controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(skeletonBox.maxX, skeletonBox.maxY, roofMaxHeight * 2);
camera.lookAt(center);
controls.target.copy(center);
controls.update();
scene.add(wallMesh);
window.addEventListener("resize", onWindowResize);
}
function onWindowResize() {
camera.aspect = walls_scene.clientWidth / walls_scene.clientHeight;
renderer.setSize(walls_scene.clientWidth, walls_scene.clientHeight);
camera.updateProjectionMatrix();
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
init();
animate();
displayBuilding(roofGeometry, wallGeometry, skeletonBox);
};
/////////////// Réprésentation du bâtiment complet ///////////////
const displayBuilding = (roofGeometry, wallGeometry, skeletonBox) => {
let camera, scene, renderer, controls;
const buildingGeometry = BufferGeometryUtils.mergeGeometries(
[roofGeometry, wallGeometry],
true
);
const roofMaterial = new MeshBasicMaterial({
color: "green",
side: DoubleSide
});
const wallMaterial = new MeshBasicMaterial({
color: "#ffb6e9",
side: DoubleSide
});
const center = new Vector3(
skeletonBox.minX + (skeletonBox.maxX - skeletonBox.minX) / 2,
skeletonBox.minY + (skeletonBox.maxY - skeletonBox.minY) / 2,
minHeight + roofHeight / 2
);
const buildingMesh = new Mesh(buildingGeometry, [roofMaterial, wallMaterial]);
const building_scene = document.getElementById("building_scene");
function init() {
renderer = new WebGLRenderer({ canvas: building_scene });
renderer.setSize(building_scene.clientWidth, building_scene.clientHeight);
scene = new Scene();
scene.background = new Color("white");
camera = new PerspectiveCamera(
70,
building_scene.clientWidth / building_scene.clientHeight,
1,
10000
);
// Our up axes here is the Z
camera.up.set(0, 0, 1);
controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(skeletonBox.maxX, skeletonBox.maxY, roofMaxHeight * 2);
camera.lookAt(center);
controls.target.copy(center);
controls.update();
scene.add(buildingMesh);
window.addEventListener("resize", onWindowResize);
}
function onWindowResize() {
camera.aspect = building_scene.clientWidth / building_scene.clientHeight;
renderer.setSize(building_scene.clientWidth, building_scene.clientHeight);
camera.updateProjectionMatrix();
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
init();
animate();
};
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.