import {
DirectionalLight,
Color,
PerspectiveCamera,
Scene,
WebGLRenderer,
BoxGeometry,
MeshPhongMaterial,
Mesh,
Group,
Box3,
OrthographicCamera,
WireframeGeometry,
LineSegments,
LineBasicMaterial,
Vector3,
} from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
/** Creates a Three.js scene for visualizing Lego mockups */
export class LegoMockupVisualizer {
/**
* Sets up a Three.js scene to visualize the lego mock up.
*
* @param {HTMLElement} domElement - HTML element that will be used as the container for the Three.js scene.
*/
constructor(domElement) {
/** @type {HTMLDivElement} */
this.domElement = domElement;
/** @type {Scene} */
this.scene = null;
/** @type {PerspectiveCamera} */
this.camera = null;
/** @type {OrbitControls} */
this.otbitControls = null;
/** @type {Scene} */
this.sceneCadastre = null;
/** @type {OrthographicCamera} */
this.cameraCadastre = null;
/** @type {Group<Object3DEventMap>} */
this.mockUpLego = null;
this.createTHREEScene();
}
/**
* Creates a three.js scene with a camera, renderer, lights, and orbit controls.
*/
createTHREEScene() {
this.scene = new Scene();
this.camera = new PerspectiveCamera(
75,
this.domElement.clientWidth / this.domElement.clientHeight,
0.1,
1000
);
this.scene.background = new Color('lightblue');
this.camera.position.set(20, 10, 20);
this.camera.lookAt(0, 2, 0);
const light = new DirectionalLight(0xffffff, 1);
light.position.set(-20, 20, 20);
const light2 = light.clone();
light2.position.set(20, 20, -20);
this.scene.add(light);
this.scene.add(light2);
const renderer = new WebGLRenderer({ antialias: true });
renderer.setSize(
this.domElement.clientWidth,
this.domElement.clientHeight,
false
);
renderer.domElement.style.height = '100%';
renderer.domElement.style.width = '100%';
this.domElement.appendChild(renderer.domElement);
new ResizeObserver(() => {
renderer.setSize(
this.domElement.clientWidth,
this.domElement.clientHeight,
false
);
renderer.render(this.scene, this.camera);
}).observe(this.domElement);
this.orbit = new OrbitControls(this.camera, renderer.domElement);
this.orbit.update();
this.orbit.addEventListener('change', () => {
renderer.render(this.scene, this.camera);
});
renderer.render(this.scene, this.camera);
}
/**
* Adds Lego blocks to threejs scene based on a given heightmap.
*
* @param {Array<Array<number>>} heightMap 2D array representing the height values of the terrain.
*/
addLegoPlateSimulation(heightMap) {
if (!heightMap.length) return;
const mockUpLego = new Group();
for (let j = 0; j < heightMap.length; j++) {
const heightMapX = heightMap[j];
for (let i = 0; i < heightMapX.length; i++) {
const value = heightMapX[i];
if (value != 0) {
const geometry = new BoxGeometry(1, value * 1.230769230769231, 1); // a lego brick is not a perfect cube. this number is calculated to have a dimension to a real lego
const material = new MeshPhongMaterial({ color: 'white' });
const cube = new Mesh(geometry, material);
cube.position.set(i, value / 2, -j);
mockUpLego.add(cube);
}
}
}
// merging all voxel to a single geometry
const geoms = [];
const meshes = [];
mockUpLego.updateMatrixWorld(true, true);
mockUpLego.children.forEach((object3D) => {
object3D.traverse(
(e) =>
e.isMesh &&
meshes.push(e) &&
geoms.push(
e.geometry.index ? e.geometry.toNonIndexed() : e.geometry().clone()
)
);
});
geoms.forEach((g, i) => g.applyMatrix4(meshes[i].matrixWorld));
const gg = BufferGeometryUtils.mergeGeometries(geoms, true);
gg.applyMatrix4(mockUpLego.children[0].matrix.clone().invert());
const m = new MeshPhongMaterial({ color: 'white' });
const mesh = new Mesh(gg, m);
mesh.position.set(0, 0, 0);
mesh.geometry.computeBoundingBox();
const geometry = new BoxGeometry(heightMap[0].length, 1, heightMap.length);
const material = new MeshPhongMaterial({ color: 'brown' });
const terrain = new Mesh(geometry, material);
const centroid = new Vector3(
(mesh.geometry.boundingBox.max.x - mesh.geometry.boundingBox.min.x) / 2,
mesh.geometry.boundingBox.min.y,
(mesh.geometry.boundingBox.max.z - mesh.geometry.boundingBox.min.z) / 2
);
terrain.position.set(
centroid.x + mesh.geometry.boundingBox.min.x,
centroid.y,
centroid.z + mesh.geometry.boundingBox.min.z
);
terrain.updateMatrix();
this.scene.add(terrain);
const targetPosition = new Box3()
.setFromObject(terrain.clone())
.getCenter(terrain.clone().position);
this.orbit.target.copy(targetPosition);
this.orbit.update();
this.mockUpLego = mockUpLego;
this.scene.add(mesh);
}
/**
* Generate cadastre image from lego mockup
*
* @param {Array<Array<number>>} heightMap 2D array representing the height values of the terrain.
*/
generateCadastre(heightMap) {
// Create cadastre scene
const rtScene = new Scene();
const xplates = heightMap[0].length / 32;
const yplates = heightMap.length / 32;
const frustumSize = 32;
const rtCamera = new OrthographicCamera(
-(frustumSize * xplates) / 2,
(frustumSize * xplates) / 2,
(frustumSize * yplates) / 2,
-(frustumSize * yplates) / 2,
1,
1000
);
rtScene.background = new Color('white');
rtCamera.position.set(
(heightMap[0].length - 1) / 2,
11,
-(heightMap.length - 1) / 2
);
rtCamera.lookAt(
(heightMap[0].length - 1) / 2,
0,
-(heightMap.length - 1) / 2
);
const light = new DirectionalLight(0xffffff, 1);
light.position.set(-20, 20, 20);
rtScene.add(light);
const cloneMockup = this.mockUpLego.clone();
rtScene.add(cloneMockup);
// create wireframe for better visualization
cloneMockup.children.forEach((mesh) => {
const wireframe = new WireframeGeometry(mesh.geometry);
const line = new LineSegments(
wireframe,
new LineBasicMaterial({ color: 'black' })
);
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
line.position.copy(mesh.position);
rtScene.add(line);
});
let scale = 1920;
const renderer = new WebGLRenderer({ antialias: true });
if (heightMap[0].length > heightMap.length)
scale /= heightMap[0].length / 32;
else scale /= heightMap.length / 32;
renderer.setSize(xplates * scale, yplates * scale, false);
renderer.render(rtScene, rtCamera);
// upload image
const strMime = 'image/jpeg';
const imgData = renderer.domElement.toDataURL(strMime);
const link = document.createElement('a');
document.body.appendChild(link);
link.download = 'calqueTemplate.jpg';
link.href = imgData;
link.click();
}
/**
* Clears the inner HTML of a DOM element and disposes of an orbit object.
*/
dispose() {
this.domElement.innerHTML = null;
this.orbit.dispose();
}
}