Codesandbox is being lame.
paste this into your objViewer.ts:
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
import { MutableRefObject } from "react";
import * as THREE from "three";
import { DragControls } from "three/examples/jsm/controls/DragControls";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import {
CSS2DObject,
CSS2DRenderer,
} from "three/examples/jsm/renderers/CSS2DRenderer";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type]
export function NewObjViewer(
el: any,
objUrl: any,
mtlUrl: any,
linesGroupRef: MutableRefObject<THREE.Group<THREE.Object3DEventMap>>,
textMeshes: { current: any[] },
dragRef: any,
dragControls: any,
element?: any,
projectPoints?: any,
popupRef?: any,
t?: any,
fetchData?: any,
BackendApis?: any
) {
let scene: any, renderer: any;
let controls: any;
let drawingLine = false;
let maxWidthMode = false;
const measurementLabels: any = {};
const lineId = 0;
let line: any;
init();
function init() {
let camera: any;
let object: any;
const raycaster = new THREE.Raycaster();
const labelRenderer = new CSS2DRenderer();
let isShiftPressed = false;
const editModeRadio = document.createElement("input");
const createModeRadio = document.createElement("input");
const popupContainer = document.createElement("div");
popupContainer.className = "popup-container";
popupContainer.style.position = "absolute";
popupContainer.style.bottom = "5px";
popupContainer.style.right = "10px";
popupContainer.style.backgroundColor = "#fff";
popupContainer.style.border = "1px solid #ccc";
popupContainer.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.5)";
popupContainer.style.borderRadius = "10px";
popupContainer.style.width = "300px"; // Set a fixed width for better aesthetics
popupContainer.style.display = "flex";
popupContainer.style.flexDirection = "column";
popupContainer.style.alignItems = "center"; // Center content horizontally
popupContainer.style.justifyContent = "center";
popupContainer.style.gap = "5px";
popupContainer.style.zIndex = "999";
popupContainer.style.maxHeight = "300px";
popupContainer.style.overflowY = "auto";
// Create the popup content
const popupContent = document.createElement("div");
popupContent.className = "popup-content";
popupContent.style.display = "flex";
popupContent.style.flexDirection = "column";
popupContent.style.alignItems = "center";
popupContent.style.padding = "0 0 10px 0";
popupContent.style.gap = "10px";
popupContent.style.width = "100%";
popupContent.style.position = "relative";
// Create the heading
const dragButton = document.createElement("img");
dragButton.src = "DragIcon";
dragButton.alt = "DragIcon";
dragButton.id = "DragIcon";
dragButton.style.cssText =
"height: 25px; width: 25px; margin: 5px; cursor: pointer; position: relative;";
const heading = document.createElement("div");
heading.style.display = "flex";
heading.style.alignItems = "center";
heading.style.gap = "10px";
heading.style.position = "relative";
const header = document.createElement("h2");
header.innerText = "Measurement Details";
header.style.color = "#333"; // header color
header.style.fontSize = "16px";
header.style.fontWeight = "bold";
const closeButton = document.createElement("img");
closeButton.src = "CloseIcon";
closeButton.alt = "CloseIcon";
closeButton.id = "CloseIcon";
closeButton.style.cssText =
"height: 25px; width: 25px; margin: 5px; cursor: pointer; position: absolute; right:5px; top:10px;";
popupContainer.appendChild(dragButton);
popupContainer.appendChild(header);
popupContainer.appendChild(closeButton);
closeButton.addEventListener("click", () => {
el.removeChild(popupContainer);
});
let points: any = [];
let outLines: any = [];
let lines: any = [];
const distanceObjects: any = [];
const polygons: any = [];
// eslint-disable-next-line prefer-const
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
20
);
// camera.position.set(0, 0, 1.5);
// eslint-disable-next-line prefer-const
let mode = "";
scene = new THREE.Scene();
const ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 15);
pointLight.position.z = 3;
scene.add(pointLight);
//scene.add(polygons); //THRAX:
scene.add(camera);
//scene.add(points); //THRAX:
//scene.add(outLines); //THRAX:
scene.add(linesGroupRef.current);
//scene.add(textMeshes.current); //THRAX:
//scene.add(dragControls.current); //THRAX:
//scene.add(dragRef.current); //THRAX:
const handleDragStart: any = () => {
controls.enabled = false; // Disable OrbitControls when drag starts
};
const handleDragEnd = () => {
controls.enabled = true; // Enable OrbitControls when drag ends
updateLineGeometry();
calculateAndShowPopup(polygons);
};
const calculateDistance = (
point1: { distanceTo: (arg0: any) => any },
point2: any
) => {
return point1.distanceTo(point2);
};
const calculatePolygonArea = (points: any) => {
// Calculate the area of the polygon formed by the points using the shoelace formula
let area = 0;
const n = points.length;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
area += points[i].x * points[j].y;
area -= points[j].x * points[i].y;
}
area = Math.abs(area) / 2.0;
return area;
};
const calculatePolygonPerimeter = (points: any) => {
// Calculate the perimeter of the polygon formed by the points
let perimeter = 0;
const n = points.length;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
perimeter += calculateDistance(points[i], points[j]);
}
return perimeter;
};
const onDocumentMouseMove = (event: any) => {
event.preventDefault();
if (drawingLine) {
const intersects: any = getIntersections(event);
if (intersects.length > 0) {
const positions = line.geometry.attributes.position.array;
const v0 = new THREE.Vector3(
positions[0],
positions[1],
positions[2]
);
const v1 = new THREE.Vector3(
intersects[0].point.x,
intersects[0].point.y,
intersects[0].point.z
);
positions[3] = intersects[0].point.x;
positions[4] = intersects[0].point.y;
positions[5] = intersects[0].point.z;
line.geometry.attributes.position.needsUpdate = true;
const distance = v0.distanceTo(v1);
measurementLabels[lineId].element.innerText =
distance.toFixed(2) + "m";
measurementLabels[lineId].position.lerpVectors(v0, v1, 0.5);
}
return;
}
if (mode === "Edit mode" && dragControls.current) {
dragControls.current.addEventListener("dragstart", handleDragStart);
dragControls.current.addEventListener("dragend", handleDragEnd);
dragControls.current.addEventListener("drag", updateLineGeometry);
}
};
const onDocumentMouseUp = () => {
if (mode === "Edit mode" && dragControls.current) {
dragControls.current.removeEventListener("dragstart", handleDragStart);
dragControls.current.removeEventListener("dragend", handleDragEnd);
dragControls.current.removeEventListener("drag", updateLineGeometry);
}
};
function getIntersections(event: any) {
const rect = el.getBoundingClientRect();
// Calculate normalized device coordinates (NDC)
const x = ((event.clientX - rect.left) / el.clientWidth) * 2 - 1;
const y = -((event.clientY - rect.top) / el.clientHeight) * 2 + 1;
// Check if the mouse coordinates are within the bounds of the 3D viewer
const mouse = new THREE.Vector2(x, y);
raycaster.setFromCamera(mouse, camera);
// Find intersections with the mesh
const intersects = raycaster.intersectObjects(scene.children, true);
return intersects;
}
const updateLineGeometry = () => {
linesGroupRef.current.clear();
lines.splice(0, lines.length);
lines = [];
if (polygons.length > 0) {
polygons.forEach((el: any, index: number) => {
const points1 = el.map((point: { position: any }) => point.position);
if (points1.length > 1) {
for (let i = 0; i < points1.length; i++) {
createLine(
points1[i],
points1[(i + 1) % points1.length],
true,
points1,
index
);
}
}
});
}
if (points.length) {
const points1 = points.map(
(point: { position: any }) => point.position
);
if (points1.length > 1) {
for (let i = 0; i < points1.length; i++) {
createLine(
points1[i],
points1[(i + 1) % points1.length],
false,
points1,
polygons.length
);
}
}
}
render();
};
const handleUndo = () => {
drawingLine = false;
scene.remove(measurementLabels[lineId]);
if (line) {
scene.remove(line);
}
if (points.length > 0) {
const removedPoint = points.pop(); // Remove the last point from the points array
scene.remove(removedPoint); // Remove the point from the scene
updateLineGeometry(); // Update the line geometry after removing the point
}
};
const saveFile = async (dataUrl: any) => {
console.log(dataUrl, "dataUrl");
const formData = new FormData();
formData.append("type", "savedProject");
// Decode Base64 to binary
// eslint-disable-next-line @typescript-eslint/no-floating-promises
fetch(dataUrl)
.then(async (res) => await res.blob())
.then(async (blob) => {
const file = new File([blob], "model.png ", { type: "image/png" });
formData.append("file_upload", file);
try {
await fetchData({
...BackendApis.uploadImages,
data: formData,
headers: {
"Content-Type": "multipart/form-data;",
},
});
} catch (error) {
console.log(error);
}
});
};
const strDownloadMime = "image/octet-stream";
function handleScreenShot() {
let imgData;
try {
const strMime = "image/png";
imgData = renderer.domElement.toDataURL(strMime);
saveFile(imgData);
} catch (e) {
console.log(e);
}
}
const findMiddlePoint = (points: any[]) => {
if (points.length === 0) {
console.error("No points provided.");
return null;
}
// Calculate the average x, y, and z coordinates
const sum = points.reduce(
(acc, point) => ({
x: acc.x + point.x,
y: acc.y + point.y,
z: acc.z + point.z,
}),
{ x: 0, y: 0, z: 0 }
);
const average = {
x: sum.x / points.length,
y: sum.y / points.length,
z: sum.z / points.length,
};
return average;
};
const shapeMeshes: any[] = [];
const polygonLabelsArray: any = []; // Array to store labels for each polygon
const createLabels = (polygonIndex: number, pointsPositions: any) => {
const distanceLabel = document.createElement("div");
distanceLabel.className = "measurementLabel";
distanceLabel.style.fontWeight = "bold";
distanceLabel.style.color = "#fff";
distanceLabel.style.borderRadius = "10px";
distanceLabel.style.padding = "3px 7px";
distanceLabel.style.background = "rgba(0, 0, 0, 0.60)";
distanceLabel.style.zIndex = "999999";
const areaLabel = document.createElement("div");
areaLabel.className = "measurementLabel";
areaLabel.innerText = `${"Area"}: ${calculatePolygonArea(
pointsPositions
).toFixed(2)} m²`;
const perimeterLabel = document.createElement("div");
perimeterLabel.className = "measurementLabel";
perimeterLabel.style.fontWeight = "bold";
perimeterLabel.innerText = `${"Perimeter"}: ${calculatePolygonPerimeter(
pointsPositions
).toFixed(2)} m`;
distanceLabel.appendChild(areaLabel);
distanceLabel.appendChild(perimeterLabel);
const middlePoint: any = findMiddlePoint(pointsPositions);
const distanceLabelObject = new CSS2DObject(distanceLabel);
distanceLabelObject.position.copy(middlePoint);
distanceObjects.push(distanceLabelObject);
// scene.add(distanceLabelObject);
// Ensure there's an entry for the polygon in the array
if (!polygonLabelsArray[polygonIndex]) {
polygonLabelsArray[polygonIndex] = {};
}
// Store labels for the polygon in the array
polygonLabelsArray[polygonIndex] = {
areaLabel,
perimeterLabel,
distanceLabelObject,
};
};
const createLine = (
point1: any,
point2: any,
closedLoop = false,
points?: any,
polygonIndex?: any
) => {
const pointsPositions: any = points;
console.log(pointsPositions, " aaaaaaaaaaaaaaa");
const distanceLabel = document.createElement("div");
distanceLabel.className = "measurementLabel";
distanceLabel.style.fontWeight = "bold";
distanceLabel.style.color = "#fff";
distanceLabel.style.borderRadius = "10px";
distanceLabel.style.padding = "3px 7px";
distanceLabel.style.background = "rgba(0, 0, 0, 0.60)";
distanceLabel.style.zIndex = "999999";
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(pointsPositions.length * 3);
for (let i = 0; i < pointsPositions.length; i++) {
positions[i * 3] = pointsPositions[i].x;
positions[i * 3 + 1] = pointsPositions[i].y;
positions[i * 3 + 2] = pointsPositions[i].z;
}
geometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
geometry.setFromPoints(pointsPositions);
const indexArray: any = [];
if (closedLoop && pointsPositions.length > 2) {
// Close the loop by connecting the last point to the first point
for (let i = 0; i < pointsPositions.length; i++) {
indexArray.push(i, (i + 1) % pointsPositions.length);
}
geometry.setIndex(indexArray);
if (!polygonLabelsArray[polygonIndex]) {
createLabels(polygonIndex, pointsPositions);
} else {
// Update labels during editing
const labels = polygonLabelsArray[polygonIndex];
labels.areaLabel.innerText = `"Area": ${calculatePolygonArea(
pointsPositions
).toFixed(2)} m²`;
labels.perimeterLabel.innerText = `
"Perimeter"
: ${calculatePolygonPerimeter(pointsPositions).toFixed(2)} m`;
}
const removedShapeMesh = shapeMeshes.splice(polygonIndex, 1)[0];
scene.remove(removedShapeMesh);
console.log(pointsPositions, "pointPosition");
let points2d = pointsPositions.map(
(v3) => new THREE.Vector2(v3.x, v3.z)
);
const shapePositions = new THREE.Shape(
//THRAX: Map these to 2d points for the shape generator....
points2d
);
shapePositions.autoClose = true;
const geomShape = new THREE.ShapeGeometry(shapePositions);
const pos = geomShape.attributes.position;
//Figure out which points in the path map to the points in the geometry...
let pointMap = [];
for (let i = 0; i < pos.count; i++) {
let p = new THREE.Vector2(pos.getX(i), pos.getY(i));
let nearestDist = Infinity;
let nearestIndex;
for (let j = 0; j < points2d.length; j++) {
//pos.setZ(i, -pointsPositions[pos.count - 1 - i].y);
let d = points2d[j].distanceTo(p);
if (d < nearestDist) {
nearestIndex = j;
nearestDist = d;
}
}
pointMap.push(nearestIndex);
}
console.log(pointMap);
//THRAX: this hack wont work because these points dont neccesarily have a relationship to pointsPositions anymore..
// but it works for the flat case
console.log(pos.array, pointsPositions);
for (let i = 0; i < pos.count; i++)
pos.setZ(i, -pointsPositions[pos.count - 1 - i].y);
//THRAX: Instead use the pointMap we created to find the original height...
for (let i = 0; i < pos.count; i++)
pos.setZ(i, -pointsPositions[pointMap[i]].y);
//THRAX:Instead we probably need to match them back to the original point somehow..
pos.needsUpdate = true;
geomShape.computeVertexNormals();
const matShape = new THREE.MeshBasicMaterial({
color: 0x22d94a,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.2,
depthTest: false,
});
const shape = new THREE.Mesh(geomShape, matShape);
//Set the y position of the shape mesh to the height of the first point..
//shape.position.y = pointsPositions[0].y;
//Rotate the shape so it lies on the X/Z plane...
shape.rotation.x = Math.PI * 0.5;
scene.add(shape);
shapeMeshes.push(shape);
shapeMeshes.splice(polygonIndex, 0, shape);
} else {
scene.remove(shapeMeshes[polygonIndex]);
scene.remove(distanceObjects[polygonIndex]);
scene.remove(polygonLabelsArray[polygonIndex]);
shapeMeshes.splice(polygonIndex, 1);
distanceObjects.splice(polygonIndex, 1);
polygonLabelsArray.splice(polygonIndex, 1);
}
const material = new THREE.LineBasicMaterial({
color: 0x22d94a,
linewidth: 3,
depthTest: false,
transparent: false,
opacity: 1,
});
const line = new THREE.Line(geometry, material);
line.frustumCulled = false;
line.userData = { point1, point2 };
linesGroupRef.current.add(line);
lines.push(line);
const distance = point1.distanceTo(point2);
distanceLabel.innerText = `${distance.toFixed(2)}m`;
const distanceLabelObject = new CSS2DObject(distanceLabel);
// Set the position of the label to the midpoint between point1 and point2
const midPoint = new THREE.Vector3().copy(point1).lerp(point2, 0.25);
distanceLabelObject.position.copy(midPoint);
linesGroupRef.current.add(distanceLabelObject);
render();
};
let outlineSphere: any;
const pointMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
depthTest: false,
transparent: true,
opacity: 1,
});
let clickTimer: any;
const DOUBLE_CLICK_THRESHOLD = 300;
el.addEventListener("click", onClick);
function onClick(event: any) {
clearTimeout(clickTimer);
clickTimer = setTimeout(() => {
// Single click logic
handleMouseDownDocument(event);
}, DOUBLE_CLICK_THRESHOLD);
}
const sphereGeometry = new THREE.SphereGeometry(0.1, 32, 32);
const suggestedPoint = new THREE.Mesh(sphereGeometry, pointMaterial);
suggestedPoint.visible = false;
const handleHover = (event: any) => {
const rect = el.getBoundingClientRect();
const x = ((event.clientX - rect.left) / el.clientWidth) * 2 - 1;
const y = -((event.clientY - rect.top) / el.clientHeight) * 2 + 1;
const mouse = new THREE.Vector2(x, y);
raycaster.setFromCamera(mouse, camera);
const points = polygons.flat(2);
const intersects = raycaster.intersectObjects(points, true);
if (intersects.length > 0) {
if (outlineSphere) {
scene.remove(outlineSphere);
outlineSphere = null; // Reset the variable
}
const currentHoveredPoint: any = intersects[0].object;
const outlineMaterial = new THREE.LineBasicMaterial({
color: 0xffa500,
linewidth: 2,
depthTest: false,
});
// Create the outline outlineSphere
const outlineGeometry = new THREE.EdgesGeometry(
new THREE.SphereGeometry(0.1, 32, 32)
);
outlineSphere = new THREE.LineSegments(
outlineGeometry,
outlineMaterial
);
outlineSphere.position.copy(currentHoveredPoint.position);
currentHoveredPoint.scale.set(1.4, 1.4, 1.4);
scene.add(outlineSphere);
outlineSphere.scale.set(1.7, 1.7, 1.7);
} else {
if (outlineSphere) {
outlineSphere.scale.set(1, 1, 1);
scene.remove(outlineSphere);
outlineSphere = null; // Reset the variable
}
points.forEach((point: any) => {
point.scale.set(1, 1, 1); // Reset the size
});
}
const intersectsWithLines = raycaster.intersectObjects(lines, true);
if (intersectsWithLines?.length > 0) {
const line: any = intersectsWithLines[0]?.index;
const actualLine: any = intersectsWithLines[line / 2]?.object;
if (actualLine?.userData?.point1 && actualLine?.userData?.point2) {
const center = new THREE.Vector3()
.addVectors(actualLine.userData.point1, actualLine.userData.point2)
.multiplyScalar(0.5);
if (center) {
suggestedPoint.position.copy(center);
suggestedPoint.visible = true;
scene.add(suggestedPoint);
}
}
return;
}
scene.remove(suggestedPoint);
};
function onDoubleClick() {
clearTimeout(clickTimer);
drawingLine = false;
scene.remove(measurementLabels[lineId]);
if (line) {
scene.remove(line);
}
if (points.length > 2) {
polygons.push(points);
clearPoints();
updateLineGeometry();
calculateAndShowPopup(polygons);
handleModeChange("Edit mode");
createModeRadio.checked = false;
editModeRadio.checked = true;
}
}
el.addEventListener("dblclick", onDoubleClick);
el.addEventListener("mouseout", onMouseOut);
// Define the mouseout event handler
function onMouseOut() {
// Hide the suggested point
suggestedPoint.visible = false;
}
const handleRemoveClick = (clickedPoint: any) => {
const index = points.indexOf(clickedPoint);
scene.remove(clickedPoint);
if (index !== -1) {
points.splice(index, 1);
} else {
polygons.forEach((array: any[]) => {
const pointIndex = array.indexOf(clickedPoint);
if (pointIndex !== -1) {
array.splice(pointIndex, 1);
}
});
}
updateLineGeometry();
hidePopup();
};
const showPopup = (clientX: any, clientY: any, clickedPoint: any) => {
if (document.querySelector(".manage_point_popup")) {
document.body.removeChild(popupRef.current);
}
const popup = document.createElement("div");
popup.style.position = "absolute";
popup.className = "manage_point_popup";
popup.style.top = `${clientY}px`;
popup.style.left = `${clientX}px`;
popup.style.background = "#FFF";
popup.style.padding = "10px";
popup.style.border = "1px solid #ccc";
popup.style.borderRadius = "5px";
popup.style.boxShadow = "0px 4px 20px 0px rgba(0, 0, 0, 0.40)";
popupRef.current = popup;
const title = document.createElement("span");
title.textContent = "Point Options";
popup.appendChild(title);
const deleteIcon = document.createElement("img");
deleteIcon.src = "DeleteIcon";
deleteIcon.alt = "DeleteIcon";
deleteIcon.id = "DeleteIcon";
popup.appendChild(deleteIcon);
deleteIcon.addEventListener("click", () =>
handleRemoveClick(clickedPoint)
);
document.body.appendChild(popup);
};
const hidePopup = () => {
if (popupRef.current) {
document.body.removeChild(popupRef.current);
popupRef.current = null;
}
};
const clearPoints = () => {
points = [];
outLines = [];
};
const handleMouseDownDocument = (event: {
clientX: number;
clientY: number;
}) => {
drawingLine = false;
scene.remove(measurementLabels[lineId]);
if (line) {
scene.remove(line);
}
// const intersect = raycaster.intersectObjects(shapeMeshes, true);
// if (intersect.length > 0) {
// if (document.querySelector(".manage_point_popup")) {
// el.removeChild(popupRef.current);
// }
// const popup = document.createElement("div");
// popup.style.position = "absolute";
// popup.className = "manage_point_popup";
// // Calculate center coordinates of the screen
// const centerX = el.clientWidth / 2;
// const centerY = el.clientHeight / 2;
// // Set the popup position to the center of the screen
// popup.style.top = `${centerY}px`;
// popup.style.left = `${centerX}px`;
// popup.style.background = "#FFF";
// popup.style.padding = "10px";
// popup.style.border = "1px solid #ccc";
// popup.style.borderRadius = "5px";
// popup.style.boxShadow = "0px 4px 20px 0px rgba(0, 0, 0, 0.40)";
// popup.style.zIndex = "9999";
// popup.style.display = "flex";
// popup.style.gap = "15px";
// popupRef.current = popup;
// // Set the second element to be horizontally aligned to the right
// const solarPanelContainer = document.createElement("div");
// solarPanelContainer.style.display = "flex";
// solarPanelContainer.style.flexDirection = "column";
// solarPanelContainer.style.justifyContent = "center";
// solarPanelContainer.style.alignItems = "center";
// const solarPanel = document.createElement("img");
// solarPanel.src = BlueSolarPanel;
// solarPanel.alt = "SolarPanel";
// solarPanel.id = "SolarPanel";
// solarPanel.style.marginBottom = "10px";
// solarPanel.style.height = "24px";
// solarPanel.style.width = "23px";
// const solarpanelTitle = document.createElement("div");
// solarpanelTitle.textContent = "Add panel";
// solarpanelTitle.style.fontFamily = "Nunito";
// solarpanelTitle.style.fontSize = "16px";
// solarpanelTitle.style.fontWeight = "700";
// solarpanelTitle.style.color = "#2D4764";
// solarPanelContainer.appendChild(solarPanel);
// solarPanelContainer.appendChild(solarpanelTitle);
// // Set the second element to be horizontally aligned to the right
// const deleteIconContainer = document.createElement("div");
// deleteIconContainer.style.marginLeft = "auto";
// deleteIconContainer.style.display = "flex";
// deleteIconContainer.style.flexDirection = "column";
// deleteIconContainer.style.justifyContent = "center";
// deleteIconContainer.style.alignItems = "center";
// const deleteIcon1 = document.createElement("img");
// deleteIcon1.src = DeleteIcon;
// deleteIcon1.alt = "DeleteIcon";
// deleteIcon1.id = "DeleteIcon";
// deleteIcon1.style.marginBottom = "10px";
// deleteIcon1.style.height = "24px";
// deleteIcon1.style.width = "23px";
// const solarpanelDeleteTitle2 = document.createElement("div");
// solarpanelDeleteTitle2.textContent = "Remove";
// solarpanelDeleteTitle2.style.fontFamily = "Nunito";
// solarpanelDeleteTitle2.style.fontSize = "16px";
// solarpanelDeleteTitle2.style.fontWeight = "700";
// solarpanelDeleteTitle2.style.color = "#2D4764";
// deleteIconContainer.appendChild(deleteIcon1);
// deleteIconContainer.appendChild(solarpanelDeleteTitle2);
// popup.appendChild(solarPanelContainer);
// popup.appendChild(deleteIconContainer);
// el.appendChild(popup);
// // solarPanel.addEventListener("click", addSolarPanel);
// }
if (mode === "") return;
if (mode === "Edit mode") {
// Enable DragControls when clicking on a point in Edit mode
const dragablePoints = [...polygons.flat(2), ...points];
dragControls.current = new DragControls(
dragablePoints,
camera,
renderer.domElement
);
return;
} else {
dragControls.current = null;
}
const intersects: any = getIntersections(event);
if (intersects?.length > 0) {
const clickPoint = intersects[0].point;
const intersect = raycaster.intersectObjects(points, true);
if (intersect.length > 0) {
const clickedPoint = intersect[0].object;
const clickedPointIndex = points.indexOf(clickedPoint);
const pointsPosition = points.map(
(point: { position: any }) => point.position
);
if (clickedPointIndex === 0 && pointsPosition.length > 2) {
polygons.push(points);
clearPoints();
updateLineGeometry();
calculateAndShowPopup(polygons);
return;
}
return;
}
const intersectsWithLines = raycaster.intersectObjects(lines, true);
if (intersectsWithLines?.length > 0 && intersect.length === 0) {
const line: any = intersectsWithLines[0]?.index;
const actualLine: any = intersectsWithLines[line / 2]?.object;
if (actualLine?.userData?.point1 && actualLine?.userData?.point2) {
const center = new THREE.Vector3()
.addVectors(
actualLine.userData.point1,
actualLine.userData.point2
)
.multiplyScalar(0.5);
const allPoints = [...polygons.flat(2), ...points];
const pointAlreadyExists = allPoints.some((point: any) =>
point.position.equals(center)
);
if (!pointAlreadyExists) {
if (center) {
const newPointMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
depthTest: false,
transparent: true,
opacity: 1,
});
const newPointGeometry = new THREE.SphereGeometry(0.1, 32, 32);
const newPoint = new THREE.Mesh(
newPointGeometry,
newPointMaterial
);
newPoint.position.copy(center);
scene.add(newPoint);
const pointPositions = points.map((ele: any) => ele.position);
if (
pointPositions.includes(actualLine?.userData?.point1) &&
pointPositions.includes(actualLine?.userData?.point2)
) {
const indexToInsertAfter = pointPositions.indexOf(
actualLine?.userData?.point1
); // Find the index of the element after which you want to insert
if (indexToInsertAfter !== -1) {
points.splice(indexToInsertAfter + 1, 0, newPoint);
}
} else {
polygons.forEach((array: any[]) => {
const pointPositions = array.map(
(ele: any) => ele.position
);
const pointIndex = pointPositions.indexOf(
actualLine?.userData?.point1
);
if (pointIndex !== -1) {
array.splice(pointIndex + 1, 0, newPoint);
}
});
}
updateLineGeometry();
calculateAndShowPopup(polygons);
render();
}
}
}
return;
}
createSpherePointWithStroke(clickPoint, 0.1, 0xffffff);
if (!drawingLine) {
// start the line
const points: any = [];
points.push(intersects[0].point);
points.push(intersects[0].point.clone());
const geometry = new THREE.BufferGeometry().setFromPoints(points);
line = new THREE.Line(
geometry,
new THREE.LineBasicMaterial({
color: 0x22d94a,
linewidth: 3,
depthTest: false,
transparent: false,
opacity: 1,
})
);
line.frustumCulled = false;
scene.add(line);
const distanceLabel = document.createElement("div");
distanceLabel.className = "measurementLabel";
distanceLabel.style.fontWeight = "bold";
distanceLabel.style.color = "#fff";
distanceLabel.style.borderRadius = "10px";
distanceLabel.style.padding = "3px 7px";
distanceLabel.style.background = "rgba(0, 0, 0, 0.60)";
distanceLabel.style.zIndex = "999999";
distanceLabel.innerText = "0.0m";
const measurementLabel = new CSS2DObject(distanceLabel);
measurementLabel.position.copy(intersects[0].point);
measurementLabels[lineId] = measurementLabel;
scene.add(measurementLabels[lineId]);
drawingLine = true;
}
}
};
const handleDeletePoints = (event: {
clientX: number;
clientY: number;
}) => {
const dragablePoints = [...polygons.flat(2), ...points];
if (mode === "Create mode" || mode === "Edit mode") return;
const intersect = raycaster.intersectObjects(dragablePoints, true);
if (intersect.length > 0) {
const clickedPoint = intersect[0].object;
showPopup(event.clientX, event.clientY, clickedPoint);
}
};
el.addEventListener("pointermove", handleHover, false);
el.addEventListener("click", handleDeletePoints);
function createSpherePointWithStroke(
position: THREE.Vector3,
size: number | undefined,
mainColor: number
) {
const pointMaterial = new THREE.MeshBasicMaterial({
color: mainColor,
depthTest: false,
transparent: true,
opacity: 1,
});
const sphereGeometry = new THREE.SphereGeometry(size, 32, 32);
const mainSphere = new THREE.Mesh(sphereGeometry, pointMaterial);
mainSphere.position.copy(position);
points.push(mainSphere);
updateLineGeometry();
scene.add(mainSphere);
}
const toggleFullScreen = () => {
const element = renderer.domElement;
if (document.fullscreenElement) {
// If already in full-screen mode, exit full-screen
if (document.exitFullscreen) {
void document.exitFullscreen();
}
} else {
// Request full-screen for the renderer's DOM element
if (element.requestFullscreen) {
el.requestFullscreen();
}
}
};
const handleModeChange = (newMode: string) => {
mode = newMode;
};
const createRadioContainer = () => {
const radioContainer = document?.createElement("div");
radioContainer.className = "radio-container";
radioContainer.style.cssText =
"position: absolute; top: 10px; left: 50%; transform: translateX(-50%); color:white; border-radius: 20px; background: rgba(0, 0, 0, 0.60); padding: 10px;";
createModeRadio.type = "radio";
createModeRadio.name = "modeRadio";
createModeRadio.value = "Create mode";
createModeRadio.defaultChecked = true;
createModeRadio.style.color = "black";
createModeRadio.style.position = "relative";
createModeRadio.style.top = "1px";
createModeRadio.addEventListener("change", () =>
handleModeChange("Create mode")
);
const createModeLabel = document.createElement("label");
createModeLabel.innerHTML = "Create mode";
createModeLabel.style.marginRight = "10px";
createModeLabel.style.marginLeft = "3px";
createModeLabel.style.color = "white";
createModeLabel.addEventListener("click", () => {
handleModeChange("Create mode");
createModeRadio.checked = true;
editModeRadio.checked = false;
});
const verticalLine = document.createElement("span");
verticalLine.style.borderLeft = "1px solid white";
verticalLine.style.height = "20px";
verticalLine.style.marginLeft = "10px";
createModeLabel.appendChild(verticalLine);
editModeRadio.type = "radio";
editModeRadio.name = "modeRadio";
editModeRadio.value = "Edit mode";
editModeRadio.style.color = "black";
editModeRadio.style.position = "relative";
editModeRadio.style.top = "1px";
editModeRadio.addEventListener("change", () =>
handleModeChange("Edit mode")
);
const editModeLabel = document.createElement("label");
editModeLabel.innerHTML = "Edit mode";
editModeLabel.style.marginLeft = "3px";
editModeLabel.style.color = "white";
editModeLabel.addEventListener("click", () => {
handleModeChange("Edit mode");
editModeRadio.checked = true;
createModeRadio.checked = false;
});
radioContainer.appendChild(createModeRadio);
radioContainer.appendChild(createModeLabel);
radioContainer.appendChild(editModeRadio);
radioContainer.appendChild(editModeLabel);
el.appendChild(radioContainer);
handleModeChange("Create mode");
};
const changeIconColor = (
createModeButton: any,
icon: any,
isResetIcon: boolean
) => {
if (isResetIcon) {
createModeButton.src = icon;
createModeButton.classList.remove("activeIcon");
return;
}
createModeButton.className = "activeIcon";
};
const resetIcons = (key: any) => {
panelIcons.forEach((icon) => {
const iconElement = document.getElementById(icon.key);
if (iconElement && icon.key !== key) {
changeIconColor(
iconElement,
icon.key === "planet" ? "Planet" : icon.image,
true
); // Assume White is a constant representing the white color
}
});
};
const handlePaning = (event: any) => {
if (isShiftPressed && event.button === 0) {
controls.enablePan = true;
controls.update();
}
};
const handleClick: any = (key: string, createModeButton: any, e: any) => {
// Clear existing mode-related elements
// e.stopPropagation();
const existingRadioContainer = element?.querySelector(".radio-container");
resetIcons(key);
if (existingRadioContainer) {
mode = "";
existingRadioContainer.remove();
}
if (key === "home") {
changeIconColor(createModeButton, "HomeYellow", false);
fitCameraToCenteredObject(camera, object);
}
if (key === "light") {
changeIconColor(createModeButton, "Light", false);
controls.mouseButtons.LEFT = THREE.MOUSE.PAN;
controls.update();
}
if (key === "planet") {
changeIconColor(createModeButton, "PlanetYellow", false);
}
if (key === "drone") {
changeIconColor(createModeButton, "DroneYellow", false);
}
if (key === "polygon") {
changeIconColor(createModeButton, "MesureYellow", false);
const measurementLabels =
document.querySelectorAll(".measurementLabels");
measurementLabels.forEach((label: any) => {
label.style.display = "block";
});
createRadioContainer();
}
if (key === "solarpanel") {
changeIconColor(createModeButton, "SolarPanelYellow", false);
const measurementLabels =
document.querySelectorAll(".measurementLabels");
measurementLabels.forEach((label: any) => {
label.style.display = "none";
});
}
if (key === "FullScreenIcon") {
toggleFullScreen();
}
if (key === "MaxWidth") {
maxWidthMode = !maxWidthMode;
}
if (key === "popup") {
changeIconColor(createModeButton, "PopupYellow", false);
el.appendChild(popupContainer);
calculateAndShowPopup(polygons);
}
if (key === "ZoomIn") {
camera.position.z -= 0.5;
camera.updateProjectionMatrix();
}
if (key === "ZoomOut") {
camera.position.z += 0.5;
camera.updateProjectionMatrix();
}
if (key !== "polygon") {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
mode === "";
}
if (key !== "light") {
controls.mouseButtons.LEFT = THREE.MOUSE.ROTATE;
controls.enableRotate = true;
controls.update();
}
};
function loadModel() {
// object.position.y = 0;
// object.position.z = bakeModelImages ? -2.2 : -1;
// object.scale.setScalar(0.1);
// Add the object to the scene
scene.add(object);
el.addEventListener("mousedown", onClick);
document.addEventListener("mousemove", onDocumentMouseMove, false);
document.addEventListener("mouseup", onDocumentMouseUp, false);
render();
}
const manager = new THREE.LoadingManager(loadModel);
const panelIcons = [
{
key: "home",
image: "Home",
msg: "Initial View",
hoverImage: "HomeYellow",
},
// {
// key: "drone",
// image: Drone,
// msg: "Coming Soon",
// hoverImage: DroneYellow,
// },
{
key: "polygon",
image: "Polygon",
msg: "Coming Soon",
hoverImage: "MesureYellow",
},
];
const rightPanelIcons = [];
function onProgress(xhr: {
lengthComputable: any;
loaded: number;
total: number;
}) {
if (xhr.lengthComputable) {
const percentComplete = (xhr.loaded / xhr.total) * 100;
if (element) {
// Check if the loader container already exists, and update its content
let loaderContainer = element.querySelector(".loader-container");
if (!loaderContainer) {
loaderContainer = document.createElement("div");
loaderContainer.className = "loader-container";
loaderContainer.style.cssText =
"position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);";
element.appendChild(loaderContainer);
}
// Update the loader content
loaderContainer.innerHTML = `
<img src="${"Loader"}" alt="loader" style="height: 25px; width: 25px; margin: 5px; cursor: pointer;">
`;
if (percentComplete === 100) {
// Remove the loader container
setTimeout(() => {
loaderContainer.remove();
if (el) {
const modeButtonContainer = document.createElement("div");
modeButtonContainer.className = "left-icons";
modeButtonContainer.style.cssText =
"position: absolute; top: 10px; transform: translateY(70%); left:10px; background: #2d4764; padding: 10px 5px; border-radius: 20px;";
panelIcons.map((ele) => {
const createModeButton = document.createElement("img");
createModeButton.src = ele.image;
createModeButton.alt = ele.key;
createModeButton.id = ele.key;
createModeButton.className =
ele.key === "planet" ? "activeIcon" : "";
createModeButton.style.cssText =
"height: 25px; width: 25px; margin: 5px; cursor: pointer;";
createModeButton.addEventListener("click", (e) =>
handleClick(ele.key, createModeButton, e)
);
return modeButtonContainer.appendChild(createModeButton);
});
el.appendChild(modeButtonContainer);
const modeButtonContainer2 = document.createElement("div");
modeButtonContainer2.className = "right-icons";
modeButtonContainer2.style.cssText =
"position: absolute; top: 10px; right:10px; background: #2d4764; padding: 10px 5px; border-radius: 20px;";
rightPanelIcons.map((ele: any) => {
const createModeButton = document.createElement("img");
createModeButton.src = ele.image;
createModeButton.alt = ele.key;
createModeButton.style.cssText =
"height: 25px; width: 25px; margin: 10px; cursor: pointer;";
createModeButton.addEventListener("click", (e) =>
handleClick(ele.key, createModeButton, e)
);
return modeButtonContainer2.appendChild(createModeButton);
});
const undoButton = document.createElement("img");
undoButton.src = "Undo";
undoButton.alt = "Undo";
undoButton.style.cssText =
"height: 20px; width: 20px; margin: 10px; cursor: pointer;";
undoButton.addEventListener("click", handleUndo);
// modeButtonContainer2.appendChild(undoButton);
const screenShotButton = document.createElement("img");
screenShotButton.className = "screenshot";
screenShotButton.src = "Capture";
screenShotButton.alt = "Capture";
screenShotButton.style.cssText =
"position: absolute; height: 65px; width: 65px; cursor: pointer; top: 10px; right:70px; border-radius: 20px;";
screenShotButton.addEventListener("click", handleScreenShot);
// el.appendChild(screenShotButton);
el.appendChild(modeButtonContainer2);
labelRenderer.setSize(el.clientWidth, el.clientHeight);
labelRenderer.domElement.style.position = "absolute";
labelRenderer.domElement.style.top = "0px";
labelRenderer.domElement.style.pointerEvents = "none";
labelRenderer.domElement.style.height = `${el.clientHeight}px`;
labelRenderer.domElement.className = "measurementLabels";
el.appendChild(labelRenderer.domElement);
}
}, 3000);
}
}
}
}
function onError() {}
const calculateAndShowPopup = (polygons: any) => {
const element = document.querySelector(".popup-content");
if (element) {
element.innerHTML = "";
}
polygons.map((ele: any, index: any) => {
const points: any = ele.map(
(point: { position: any }) => point.position
);
const area = calculatePolygonArea(points);
const perimeter = calculatePolygonPerimeter(points);
// Create the popup container
const heading = document.createElement("div");
heading.style.display = "flex";
heading.style.alignItems = "center";
heading.style.gap = "10px";
heading.style.position = "relative";
const header = document.createElement("h2");
header.innerText = `${"Measurement"} ${index + 1}`;
header.style.color = "#333"; // header color
header.style.fontSize = "16px";
header.style.fontWeight = "bold";
heading.appendChild(header);
const downIcon = document.createElement("img");
downIcon.src = "DownIcon";
downIcon.alt = "DownIcon";
downIcon.id = "DownIcon";
downIcon.style.cssText =
"height: 25px; width: 25px; margin: 5px; cursor: pointer;";
const upIcon = document.createElement("img");
upIcon.src = "UpIcon";
upIcon.alt = "UpIcon";
upIcon.id = "UpIcon";
upIcon.style.cssText =
"height: 25px; width: 25px; margin: 5px; cursor: pointer;";
const deleteIcon = document.createElement("img");
deleteIcon.src = "DeleteIcon";
deleteIcon.alt = "DeleteIcon";
deleteIcon.id = "DeleteIcon";
deleteIcon.style.cssText =
"height: 20px; width: 20px; margin: 5px; cursor: pointer; margin-left:10px";
heading.appendChild(downIcon);
heading.appendChild(deleteIcon);
// Display distances between consecutive points
const distancesParagraph = document.createElement("p");
distancesParagraph.innerHTML = `<strong>${"Distances"}:</strong>`;
distancesParagraph.id = `${index + 1}`;
points.forEach((point: any, index: any) => {
const nextIndex = (index + 1) % points.length; // Use modulo to connect last point to the first point
const distance = calculateDistance(point, points[nextIndex]);
distancesParagraph.innerHTML += `<br>${"Point"} ${
index + 1
} ${"to Point"} ${nextIndex + 1}: ${distance.toFixed(2)} ${"meters"}`;
});
const areaParagraph = document.createElement("p");
areaParagraph.innerHTML = `<strong>${"Area"}:</strong> ${area.toFixed(
2
)} ${"square meters"}`;
const perimeterParagraph = document.createElement("p");
perimeterParagraph.innerHTML = `<strong>${"Perimeter"}:</strong> ${perimeter.toFixed(
2
)} ${"meters"}`;
// Create the close button
let measurementsVisible = true;
const measurementsContainer = document.createElement("div");
measurementsContainer.style.display = "flex";
measurementsContainer.style.flexDirection = "column";
// Append distances, area, and perimeter to the measurements container
measurementsContainer.appendChild(distancesParagraph);
measurementsContainer.appendChild(areaParagraph);
measurementsContainer.appendChild(perimeterParagraph);
popupContent.appendChild(heading);
popupContent.appendChild(measurementsContainer);
popupContainer.appendChild(popupContent);
deleteIcon.addEventListener("click", () => {
scene.remove(shapeMeshes[index]);
scene.remove(distanceObjects[index]);
scene.remove(polygonLabelsArray[index]);
polygons[index].forEach((e: any) => {
scene.remove(e);
});
shapeMeshes.splice(index, 1);
distanceObjects.splice(index, 1);
polygonLabelsArray.splice(index, 1);
polygons.splice(index, 1);
updateLineGeometry();
calculateAndShowPopup(polygons);
});
downIcon.addEventListener("click", (event) => {
event.stopPropagation();
// Toggle visibility state
measurementsVisible = !measurementsVisible;
// Toggle the display style of the measurements container
heading.removeChild(downIcon);
heading.removeChild(deleteIcon);
heading.appendChild(measurementsVisible ? upIcon : downIcon);
heading.appendChild(deleteIcon);
measurementsContainer.style.display = measurementsVisible
? "flex"
: "none";
});
});
};
// el.appendChild(popupContainer);
const fitCameraToCenteredObject = (camera: any, object: any) => {
const boundingBox = new THREE.Box3();
boundingBox.setFromObject(object);
const center = boundingBox.getCenter(new THREE.Vector3());
const size = boundingBox.getSize(new THREE.Vector3());
const maxSize = Math.max(size.x, size.y, size.z);
const fov = camera.fov * (Math.PI / 180);
const cameraZ = Math.abs(maxSize / 2 / Math.tan(fov / 2));
camera.position.copy(center);
camera.position.z += cameraZ;
camera.near = maxSize / 100;
camera.far = maxSize * 3;
camera.updateProjectionMatrix();
};
const mtlLoader = new MTLLoader(manager);
mtlLoader.load(mtlUrl, (materials: { preload: () => void }) => {
materials.preload();
const loader = new OBJLoader(manager);
loader.setMaterials(materials as any).load(
objUrl,
function (obj: any) {
obj.traverse(function (child: any) {
if (child instanceof THREE.Mesh) {
child.material.wireframe = false;
}
});
object = obj;
fitCameraToCenteredObject(camera, obj);
},
onProgress,
onError
);
});
const animate = () => {
requestAnimationFrame(animate);
controls.update();
render();
};
renderer = new THREE.WebGLRenderer({
antialias: true,
preserveDrawingBuffer: true,
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000);
el.appendChild(renderer.domElement);
controls = new OrbitControls(camera, renderer.domElement);
controls.enablePan = false;
controls.autoRotate = false;
controls.mouseButtons.RIGHT = THREE.MOUSE.ROTATE;
controls.mouseButtons.LEFT = THREE.MOUSE.ROTATE;
controls.addEventListener("change", render);
// Event listener for keydown to detect the shift key
document.addEventListener("keydown", (event: any) => {
console.log(event.key, "keydown");
if (event.key === "Shift") {
isShiftPressed = true;
}
if (event.key === "Escape") {
void onDoubleClick();
}
});
// Event listener for keyup to detect when the shift key is released
document.addEventListener("keyup", (event: any) => {
if (event.key === "Shift") {
isShiftPressed = false;
}
});
document.addEventListener("pointerdown", handlePaning);
animate();
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.setSize(el.clientWidth, el.clientHeight);
}
function render() {
labelRenderer.render(scene, camera);
renderer.render(scene, camera);
}
const setDefaultValue = () => {
if (projectPoints.length > 0) {
projectPoints?.forEach((points: any) => {
if (points?.length > 0) {
const singlePolygonPoints = points?.map((point: any) => {
const geometry1 = new THREE.SphereGeometry(0.1, 32, 32);
const material1 = new THREE.MeshBasicMaterial({
color: 0xffffff,
depthTest: false,
transparent: true,
opacity: 1,
});
const sphere1 = new THREE.Mesh(geometry1, material1);
sphere1.position.copy(
new THREE.Vector3(point.x, point.y, point.z)
);
scene.add(sphere1);
return sphere1;
});
polygons.push(singlePolygonPoints);
updateLineGeometry();
}
});
}
};
setDefaultValue();
window.addEventListener("resize", onWindowResize, false);
return () => {
if (element) {
element.remove();
}
el.removeChild(renderer.domElement);
el.removeEventListener("mousedown", onClick);
document.removeEventListener("mousemove", onDocumentMouseMove);
document.removeEventListener("mouseup", onDocumentMouseUp);
if (mode === "Edit mode" && dragControls.current) {
dragControls.current.dispose();
}
};
}
}