created new window

This commit is contained in:
2025-10-13 10:44:17 +05:30
parent 5fb88849aa
commit e00bccb357
14 changed files with 944 additions and 426 deletions

48
app/package-lock.json generated
View File

@@ -2029,7 +2029,7 @@
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"devOptional": true,
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
@@ -2041,7 +2041,7 @@
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
@@ -4192,6 +4192,26 @@
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "5.17.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
@@ -4303,25 +4323,25 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"dev": true
"devOptional": true
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true
"devOptional": true
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true
"devOptional": true
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true
"devOptional": true
},
"node_modules/@turf/along": {
"version": "7.2.0",
@@ -9093,7 +9113,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
"devOptional": true
},
"node_modules/cross-env": {
"version": "7.0.3",
@@ -9970,7 +9990,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=0.3.1"
}
@@ -15354,7 +15374,7 @@
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
"devOptional": true
},
"node_modules/makeerror": {
"version": "1.0.12",
@@ -20908,7 +20928,7 @@
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -20951,7 +20971,7 @@
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"devOptional": true,
"dependencies": {
"acorn": "^8.11.0"
},
@@ -20963,7 +20983,7 @@
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
"devOptional": true
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
@@ -21459,7 +21479,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
"devOptional": true
},
"node_modules/v8-to-istanbul": {
"version": "8.1.1",
@@ -22518,7 +22538,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=6"
}

View File

@@ -1,4 +1,9 @@
import { useCompareProductDataStore, useLoadingProgress, useIsComparing } from "../../../store/builder/store";
import {
useCompareProductDataStore,
useLoadingProgress,
useIsComparing,
useCreateNewWindow,
} from "../../../store/builder/store";
import { useSimulationState } from "../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
import { useEffect, useState } from "react";
@@ -11,7 +16,8 @@ import { useSimulationManager } from "../../../store/rough/useSimulationManagerS
import { useParams } from "react-router-dom";
import { validateSimulationDataApi } from "../../../services/simulation/comparison/validateSimulationDataApi";
import { calculateSimulationData } from "./functions/calculateSimulationData";
import NewWindowScene from "../../ui/compareVersion/NewWindowScene";
import Button from "../../ui/compareVersion/Button";
type AssetData = {
activeTime: number;
idleTime: number;
@@ -41,7 +47,11 @@ export interface CompareProduct {
//shiftsPerDay: number;
};
}
export const createCompareProduct = (productUuid: string, productName: string, assets: AssetData[]): CompareProduct => ({
export const createCompareProduct = (
productUuid: string,
productName: string,
assets: AssetData[]
): CompareProduct => ({
productUuid,
productName,
simulationData: calculateSimulationData(assets),
@@ -61,8 +71,10 @@ function ComparisonScene() {
const { setCompareProductsData } = useCompareProductDataStore();
const [shouldShowComparisonResult, setShouldShowComparisonResult] = useState(false);
const { addSimulationRecord } = useSimulationManager();
useEffect(() => {});
const { createNewWindow } = useCreateNewWindow();
useEffect(() => {
console.log("comparisonScene: ", comparisonScene);
}, [comparisonScene]);
const handleSelectVersion = (option: string) => {
const version = versionHistory.find((version) => version.versionName === option);
@@ -77,12 +89,22 @@ function ComparisonScene() {
echo.log(getData.message);
const getSimulate = getData?.data?.existingSimulatedData;
if (!getSimulate) return;
if (!selectedVersion?.versionId || !projectId || getSimulate === undefined || !selectedProduct.productUuid) {
if (
!selectedVersion?.versionId ||
!projectId ||
getSimulate === undefined ||
!selectedProduct.productUuid
) {
echo.warn("No prebacked Data found");
alert("Please run the simulation before comparing.");
return;
}
addSimulationRecord(projectId, selectedVersion?.versionId || "", selectedProduct.productUuid || "", getSimulate.data);
addSimulationRecord(
projectId,
selectedVersion?.versionId || "",
selectedProduct.productUuid || "",
getSimulate.data
);
});
}
};
@@ -105,7 +127,12 @@ function ComparisonScene() {
echo.warn(getData.message);
const getSimulate = getData?.data?.existingSimulatedData;
if (!getSimulate) return;
addSimulationRecord(projectId, selectedVersion?.versionId || "", product.productUuid || "", getSimulate.data);
addSimulationRecord(
projectId,
selectedVersion?.versionId || "",
product.productUuid || "",
getSimulate.data
);
});
setComparisonState(data);
}
@@ -113,12 +140,32 @@ function ComparisonScene() {
useEffect(() => {
if (mainScene && comparisonScene && selectedVersion) {
const mainVersion = useSimulationManager.getState().getProductById(projectId, mainScene.version.versionUuid, mainScene.product.productUuid);
const mainVersion = useSimulationManager
.getState()
.getProductById(
projectId,
mainScene.version.versionUuid,
mainScene.product.productUuid
);
const compareVersion = useSimulationManager.getState().getProductById(projectId, comparisonScene.version.versionUuid, comparisonScene.product.productUuid);
const compareVersion = useSimulationManager
.getState()
.getProductById(
projectId,
comparisonScene.version.versionUuid,
comparisonScene.product.productUuid
);
const mainVompareversion = createCompareProduct(mainVersion?.productId ?? "", mainScene.product.productName, mainVersion?.simulateData || []);
const compareProduct2 = createCompareProduct(compareVersion?.productId ?? "", comparisonScene.product.productName, compareVersion?.simulateData || []);
const mainVompareversion = createCompareProduct(
mainVersion?.productId ?? "",
mainScene.product.productName,
mainVersion?.simulateData || []
);
const compareProduct2 = createCompareProduct(
compareVersion?.productId ?? "",
comparisonScene.product.productName,
compareVersion?.simulateData || []
);
const comparedArray = [mainVompareversion, compareProduct2];
@@ -131,7 +178,14 @@ function ComparisonScene() {
} else {
setShouldShowComparisonResult(false);
}
}, [mainScene, comparisonScene, selectedVersion, projectId, setCompareProductsData, simulationRecords]);
}, [
mainScene,
comparisonScene,
selectedVersion,
projectId,
setCompareProductsData,
simulationRecords,
]);
return (
<>
@@ -154,7 +208,20 @@ function ComparisonScene() {
/>
</div>
)}
<CompareLayOut />
{selectedVersion?.versionId && (
<div
style={{
position: "absolute",
top: "10px",
right: "900px",
zIndex: 10,
}}
>
<Button />
</div>
)}
{<CompareLayOut />}
{createNewWindow && <NewWindowScene />}
{shouldShowComparisonResult && !loadingProgress && <ComparisonResult />}
</>
)}

View File

@@ -2,182 +2,382 @@ import React, { ReactNode, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
type NewWindowProps = {
children: ReactNode;
title?: string;
width?: number;
height?: number;
left?: number;
top?: number;
center?: boolean;
features?: Partial<{
toolbar: boolean;
menubar: boolean;
scrollbars: boolean;
resizable: boolean;
location: boolean;
status: boolean;
}>;
onClose?: () => void;
copyStyles?: boolean;
noopener?: boolean;
className?: string;
theme?: string | null;
children: ReactNode;
title?: string;
width?: number;
height?: number;
left?: number;
top?: number;
center?: boolean;
features?: Partial<{
toolbar: boolean;
menubar: boolean;
scrollbars: boolean;
resizable: boolean;
location: boolean;
status: boolean;
}>;
onClose?: () => void;
copyStyles?: boolean;
noopener?: boolean;
className?: string;
theme?: string | null;
};
// export const RenderInNewWindow: React.FC<NewWindowProps> = ({
// children,
// title = "New Window",
// width = 900,
// height = 700,
// left,
// top,
// center = true,
// features,
// onClose,
// copyStyles = true,
// noopener = true,
// className,
// theme = localStorage.getItem("theme") ?? "light",
// }) => {
// const [mounted, setMounted] = useState(false);
// const childWindowRef = useRef<Window | null>(null);
// const containerElRef = useRef<HTMLDivElement | null>(null);
// useEffect(() => {
// if (typeof window === "undefined") return;
// const screenLeft = window.screenLeft ?? window.screenX ?? 0;
// const screenTop = window.screenTop ?? window.screenY ?? 0;
// const availWidth = window.outerWidth ?? window.innerWidth;
// const availHeight = window.outerHeight ?? window.innerHeight;
// const finalLeft =
// center && availWidth ? Math.max(0, screenLeft + (availWidth - width) / 2) : left ?? 100;
// const finalTop =
// center && availHeight
// ? Math.max(0, screenTop + (availHeight - height) / 2)
// : top ?? 100;
// const baseFeatures = [
// `width=${Math.floor(width)}`,
// `height=${Math.floor(height)}`,
// `left=${Math.floor(finalLeft)}`,
// `top=${Math.floor(finalTop)}`,
// ];
// const featureFlags = features ?? {
// toolbar: false,
// menubar: false,
// scrollbars: true,
// resizable: true,
// location: false,
// status: false,
// };
// Object.entries(featureFlags).forEach(([k, v]) =>
// baseFeatures.push(`${k}=${v ? "yes" : "no"}`)
// );
// const newWin = window.open("", "_blank", baseFeatures.join(","));
// if (!newWin) {
// console.warn("Popup blocked or failed to open window.");
// onClose?.();
// return;
// }
// if (noopener) {
// try {
// newWin.opener = null;
// } catch {}
// }
// newWin.document.open();
// newWin.document.write(`<!doctype html>
// <html data-theme=${theme}>
// <head>
// <meta charset="utf-8" />
// <title>${title}</title>
// <meta name="viewport" content="width=device-width, initial-scale=1" />
// </head>
// <body style="margin:0;"></body>
// </html>`);
// newWin.document.close();
// if (copyStyles) {
// const head = newWin.document.head;
// Array.from(document.styleSheets).forEach((styleSheet) => {
// try {
// if ((styleSheet as CSSStyleSheet).cssRules) {
// const newStyleEl = newWin.document.createElement("style");
// const rules = Array.from((styleSheet as CSSStyleSheet).cssRules).map(
// (r) => r.cssText
// );
// newStyleEl.appendChild(newWin.document.createTextNode(rules.join("\n")));
// head.appendChild(newStyleEl);
// }
// } catch {
// const ownerNode = styleSheet.ownerNode as HTMLElement | null;
// if (ownerNode && ownerNode.tagName === "LINK") {
// const link = ownerNode as HTMLLinkElement;
// const newLink = newWin.document.createElement("link");
// newLink.rel = link.rel;
// newLink.href = link.href;
// newLink.media = link.media;
// head.appendChild(newLink);
// }
// }
// });
// }
// const container = newWin.document.createElement("div");
// if (className) container.className = className;
// newWin.document.body.appendChild(container);
// newWin.document.title = title;
// // Handle child window close
// const handleChildUnload = () => {
// onClose?.();
// };
// newWin.addEventListener("beforeunload", handleChildUnload);
// // 👇 Handle parent refresh/close → auto close child
// const handleParentUnload = () => {
// try {
// newWin.close();
// } catch {}
// };
// window.addEventListener("beforeunload", handleParentUnload);
// childWindowRef.current = newWin;
// containerElRef.current = container;
// setMounted(true);
// return () => {
// newWin.removeEventListener("beforeunload", handleChildUnload);
// window.removeEventListener("beforeunload", handleParentUnload);
// try {
// newWin.close();
// } catch {}
// childWindowRef.current = null;
// containerElRef.current = null;
// };
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, []);
// useEffect(() => {
// const w = childWindowRef.current;
// if (w && !w.closed) {
// w.document.title = title;
// }
// }, [title]);
// if (!mounted || !containerElRef.current) return null;
// return createPortal(children, containerElRef.current);
// };
export const RenderInNewWindow: React.FC<NewWindowProps> = ({
children,
title = "New Window",
width = 900,
height = 700,
left,
top,
center = true,
features,
onClose,
copyStyles = true,
noopener = true,
className,
theme = localStorage.getItem('theme') ?? 'light',
children,
title = "3D Viewer",
width = 900,
height = 700,
left,
top,
center = true,
features,
onClose,
copyStyles = true,
noopener = true,
className,
theme = localStorage.getItem("theme") ?? "light",
}) => {
const [mounted, setMounted] = useState(false);
const childWindowRef = useRef<Window | null>(null);
const containerElRef = useRef<HTMLDivElement | null>(null);
const [mounted, setMounted] = useState(false);
const childWindowRef = useRef<Window | null>(null);
const containerElRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (typeof window === "undefined") return;
useEffect(() => {
if (typeof window === "undefined") return;
const screenLeft = window.screenLeft ?? window.screenX ?? 0;
const screenTop = window.screenTop ?? window.screenY ?? 0;
const availWidth = window.outerWidth ?? window.innerWidth;
const availHeight = window.outerHeight ?? window.innerHeight;
const screenLeft = window.screenLeft ?? window.screenX ?? 0;
const screenTop = window.screenTop ?? window.screenY ?? 0;
const availWidth = window.outerWidth ?? window.innerWidth;
const availHeight = window.outerHeight ?? window.innerHeight;
const finalLeft =
center && availWidth
? Math.max(0, screenLeft + (availWidth - width) / 2)
: left ?? 100;
const finalLeft =
center && availWidth ? Math.max(0, screenLeft + (availWidth - width) / 2) : left ?? 100;
const finalTop =
center && availHeight
? Math.max(0, screenTop + (availHeight - height) / 2)
: top ?? 100;
const finalTop =
center && availHeight
? Math.max(0, screenTop + (availHeight - height) / 2)
: top ?? 100;
const baseFeatures = [
`width=${Math.floor(width)}`,
`height=${Math.floor(height)}`,
`left=${Math.floor(finalLeft)}`,
`top=${Math.floor(finalTop)}`,
];
const baseFeatures = [
`width=${Math.floor(width)}`,
`height=${Math.floor(height)}`,
`left=${Math.floor(finalLeft)}`,
`top=${Math.floor(finalTop)}`,
];
const featureFlags = features ?? {
toolbar: false,
menubar: false,
scrollbars: true,
resizable: true,
location: false,
status: false,
};
const featureFlags = features ?? {
toolbar: false,
menubar: false,
scrollbars: true,
resizable: true,
location: false,
status: false,
};
Object.entries(featureFlags).forEach(([k, v]) =>
baseFeatures.push(`${k}=${v ? "yes" : "no"}`)
);
Object.entries(featureFlags).forEach(([k, v]) =>
baseFeatures.push(`${k}=${v ? "yes" : "no"}`)
);
const newWin = window.open("", "_blank", baseFeatures.join(","));
if (!newWin) {
console.warn("Popup blocked or failed to open window.");
onClose?.();
return;
}
const newWin = window.open("", "_blank", baseFeatures.join(","));
if (!newWin) {
console.warn("Popup blocked or failed to open window.");
onClose?.();
return;
}
if (noopener) {
try {
newWin.opener = null;
} catch {}
}
if (noopener) {
try {
newWin.opener = null;
} catch {}
}
newWin.document.open();
newWin.document.write(`<!doctype html>
newWin.document.open();
newWin.document.write(`<!doctype html>
<html data-theme=${theme}>
<head>
<meta charset="utf-8" />
<title>${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
width: 100vw;
height: 100vh;
}
#three-container {
width: 100%;
height: 100%;
}
</style>
</head>
<body style="margin:0;"></body>
<body>
<div id="three-container"></div>
</body>
</html>`);
newWin.document.close();
newWin.document.close();
if (copyStyles) {
const head = newWin.document.head;
Array.from(document.styleSheets).forEach((styleSheet) => {
try {
if ((styleSheet as CSSStyleSheet).cssRules) {
const newStyleEl = newWin.document.createElement("style");
const rules = Array.from(
(styleSheet as CSSStyleSheet).cssRules
).map((r) => r.cssText);
newStyleEl.appendChild(
newWin.document.createTextNode(rules.join("\n"))
);
head.appendChild(newStyleEl);
}
} catch {
const ownerNode = styleSheet.ownerNode as HTMLElement | null;
if (ownerNode && ownerNode.tagName === "LINK") {
const link = ownerNode as HTMLLinkElement;
const newLink = newWin.document.createElement("link");
newLink.rel = link.rel;
newLink.href = link.href;
newLink.media = link.media;
head.appendChild(newLink);
}
if (copyStyles) {
const head = newWin.document.head;
Array.from(document.styleSheets).forEach((styleSheet) => {
try {
if ((styleSheet as CSSStyleSheet).cssRules) {
const newStyleEl = newWin.document.createElement("style");
const rules = Array.from((styleSheet as CSSStyleSheet).cssRules).map(
(r) => r.cssText
);
newStyleEl.appendChild(newWin.document.createTextNode(rules.join("\n")));
head.appendChild(newStyleEl);
}
} catch {
const ownerNode = styleSheet.ownerNode as HTMLElement | null;
if (ownerNode && ownerNode.tagName === "LINK") {
const link = ownerNode as HTMLLinkElement;
const newLink = newWin.document.createElement("link");
newLink.rel = link.rel;
newLink.href = link.href;
newLink.media = link.media;
head.appendChild(newLink);
}
}
});
}
});
}
const container = newWin.document.createElement("div");
if (className) container.className = className;
newWin.document.body.appendChild(container);
const container = newWin.document.getElementById(
"three-container"
) as HTMLDivElement | null;
if (!container) return;
newWin.document.title = title;
if (className) container.className = className;
// Handle child window close
const handleChildUnload = () => {
onClose?.();
};
newWin.addEventListener("beforeunload", handleChildUnload);
newWin.document.title = title;
// 👇 Handle parent refresh/close → auto close child
const handleParentUnload = () => {
try {
newWin.close();
} catch {}
};
window.addEventListener("beforeunload", handleParentUnload);
// ✅ CRITICAL FIX: Wait for window to be fully ready then trigger resize
const initializeWindow = () => {
// Force maximize handling
setTimeout(() => {
// Get actual window dimensions (might be maximized)
const actualWidth = newWin.innerWidth;
const actualHeight = newWin.innerHeight;
childWindowRef.current = newWin;
containerElRef.current = container;
setMounted(true);
console.log("Window dimensions:", actualWidth, actualHeight);
return () => {
newWin.removeEventListener("beforeunload", handleChildUnload);
window.removeEventListener("beforeunload", handleParentUnload);
try {
newWin.close();
} catch {}
childWindowRef.current = null;
containerElRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Trigger resize event for Three.js
newWin.dispatchEvent(new Event("resize"));
useEffect(() => {
const w = childWindowRef.current;
if (w && !w.closed) {
w.document.title = title;
}
}, [title]);
// Additional safety: trigger again after a short delay
setTimeout(() => {
newWin.dispatchEvent(new Event("resize"));
}, 200);
}, 100);
};
if (!mounted || !containerElRef.current) return null;
// Handle both load and focus events
newWin.addEventListener("load", initializeWindow);
newWin.addEventListener("focus", initializeWindow);
return createPortal(children, containerElRef.current);
// Also initialize immediately if window is already loaded
if (newWin.document.readyState === "complete") {
initializeWindow();
} else {
newWin.addEventListener("DOMContentLoaded", initializeWindow);
}
// Handle child window close
const handleChildUnload = () => {
onClose?.();
};
newWin.addEventListener("beforeunload", handleChildUnload);
// Handle parent refresh/close → auto close child
const handleParentUnload = () => {
try {
newWin.close();
} catch {}
};
window.addEventListener("beforeunload", handleParentUnload);
childWindowRef.current = newWin;
containerElRef.current = container;
setMounted(true);
return () => {
newWin.removeEventListener("load", initializeWindow);
newWin.removeEventListener("focus", initializeWindow);
newWin.removeEventListener("DOMContentLoaded", initializeWindow);
newWin.removeEventListener("beforeunload", handleChildUnload);
window.removeEventListener("beforeunload", handleParentUnload);
try {
newWin.close();
} catch {}
childWindowRef.current = null;
containerElRef.current = null;
};
}, []);
useEffect(() => {
const w = childWindowRef.current;
if (w && !w.closed) {
w.document.title = title;
}
}, [title]);
if (!mounted || !containerElRef.current) return null;
return createPortal(children, containerElRef.current);
};

View File

@@ -9,14 +9,18 @@ interface LoadingPageProps {
}
const LoadingPage: React.FC<LoadingPageProps> = ({ progress }) => {
console.log('progress: ', progress);
const { projectName } = useProjectName();
const { comparisonScene } = useSimulationState();
const validatedProgress = Math.min(100, Math.max(0, progress));
console.log("comparisonScene: ", comparisonScene);
return (
<RenderOverlay>
<div className={`loading-wrapper ${comparisonScene != null ? "comparisionLoading" : ""}`}>
<div
className={`loading-wrapper ${comparisonScene != null ? "comparisionLoading" : ""}`}
>
<div className="loading-container">
<div className="project-name">{projectName}</div>
<div className="loading-hero-container">

View File

@@ -0,0 +1,47 @@
import React from "react";
import {
useCreateNewWindow,
useIsComparing,
useLoadingProgress,
} from "../../../store/builder/store";
import { useSimulationState } from "../../../store/simulation/useSimulationStore";
const Button = () => {
const { isComparing, setIsComparing } = useIsComparing();
const { createNewWindow, setCreateNewWindow } = useCreateNewWindow();
const { setLoadingProgress } = useLoadingProgress();
const { clearComparisonState } = useSimulationState();
const handleExit = () => {
setIsComparing(false);
setCreateNewWindow(false);
setLoadingProgress(0);
clearComparisonState();
};
const handleOpenInNewWindow = () => {
// 🧹 Immediately reset any loading or scene state
setLoadingProgress(0);
setCreateNewWindow(true);
};
return (
<div
style={{
position: "absolute",
top: "10px",
left: "10px",
zIndex: 1000,
display: "flex",
gap: "10px",
}}
>
{isComparing && <button onClick={handleExit}>Exit</button>}
{isComparing && !createNewWindow && (
<button onClick={handleOpenInNewWindow}>Open in New Window</button>
)}
</div>
);
};
export default Button;

View File

@@ -1,7 +1,11 @@
import { useParams } from "react-router-dom";
import React, { useState, useRef, useEffect, Suspense } from "react";
import { CompareLayoutIcon, LayoutIcon, ResizerIcon } from "../../icons/SimulationIcons";
import { useLoadingProgress, useIsComparing } from "../../../store/builder/store";
import {
useLoadingProgress,
useIsComparing,
useCreateNewWindow,
} from "../../../store/builder/store";
import { useSimulationState } from "../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
import { useSceneContext } from "../../../modules/scene/sceneContext";
@@ -13,11 +17,18 @@ import useRestStates from "../../../hooks/useResetStates";
import { getVersionHistoryApi } from "../../../services/factoryBuilder/versionControl/getVersionHistoryApi";
import { validateSimulationDataApi } from "../../../services/simulation/comparison/validateSimulationDataApi";
import Button from "./Button";
const CompareLayOut = () => {
const { clearComparisonState, comparisonScene, setComparisonState } = useSimulationState();
const { versionStore } = useSceneContext();
const { versionHistory, selectedVersion, setSelectedVersion, clearSelectedVersion, setVersions } = versionStore();
const {
versionHistory,
selectedVersion,
setSelectedVersion,
clearSelectedVersion,
setVersions,
} = versionStore();
const { setLoadingProgress } = useLoadingProgress();
const [width, setWidth] = useState("50vw");
const [isResizing, setIsResizing] = useState(false);
@@ -30,6 +41,7 @@ const CompareLayOut = () => {
const { setIsPlaying } = usePlayButtonStore();
const { projectId } = useParams();
const { resetStates } = useRestStates();
const { createNewWindow } = useCreateNewWindow();
useEffect(() => {
return () => {
resetStates();
@@ -162,8 +174,8 @@ const CompareLayOut = () => {
setLoadingProgress(1);
const singleData = {
projectId: projectId,
versionId: version.versionId,
productUuid: data[0].productUuid,
versionId: version.versionId,
productUuid: data[0].productUuid,
};
validateSimulationDataApi(singleData).then((getData) => {
@@ -178,59 +190,77 @@ const CompareLayOut = () => {
};
return (
<div className={`compareLayOut-wrapper ${width === "0px" ? "closed" : ""}`} ref={wrapperRef} style={{ width }}>
{loadingProgress === 0 && selectedVersion?.versionId && (
<button title="resize-canvas" id="compare-resize-slider-btn" className="resizer" onMouseDown={handleStartResizing}>
<ResizerIcon />
</button>
)}
<div className="chooseLayout-container">
{selectedVersion?.versionId && (
<div className="compare-layout-canvas-container">
<Suspense fallback={null}>
<Scene layout="Comparison Layout" />
</Suspense>
</div>
)}
{width !== "0px" &&
!selectedVersion?.versionId && ( // Show only if no layout selected
<div className="chooseLayout-wrapper">
<div className="icon">
<CompareLayoutIcon />
<>
{!createNewWindow && (
<div
className={`compareLayOut-wrapper ${width === "0px" ? "closed" : ""}`}
ref={wrapperRef}
style={{ width }}
>
{loadingProgress === 0 && selectedVersion?.versionId && (
<button
title="resize-canvas"
id="compare-resize-slider-btn"
className="resizer"
onMouseDown={handleStartResizing}
>
<ResizerIcon />
</button>
)}
<div className="chooseLayout-container">
{selectedVersion?.versionId && (
<div className="compare-layout-canvas-container">
<Suspense fallback={null}>
<Scene layout="Comparison Layout" />
</Suspense>
</div>
<div className="value">Choose Version to compare</div>
<button className="selectLayout" onClick={() => setShowLayoutDropdown(!showLayoutDropdown)}>
Select Version
</button>
)}
{showLayoutDropdown && (
<div className="displayLayouts-container">
<div className="header">Versions</div>
<Search onChange={() => {}} />
<div className="layouts-container">
{versionHistory.map((version) => (
<button
key={version.versionId}
className="layout-wrapper"
onClick={() => {
handleSelectLayout(version);
setShowLayoutDropdown(false);
}}
>
<LayoutIcon />
<div className="layout">{version.versionName}</div>
</button>
))}
{width !== "0px" &&
!selectedVersion?.versionId && ( // Show only if no layout selected
<div className="chooseLayout-wrapper">
<div className="icon">
<CompareLayoutIcon />
</div>
<div className="value">Choose Version to compare</div>
<button
className="selectLayout"
onClick={() => setShowLayoutDropdown(!showLayoutDropdown)}
>
Select Version
</button>
{showLayoutDropdown && (
<div className="displayLayouts-container">
<div className="header">Versions</div>
<Search onChange={() => {}} />
<div className="layouts-container">
{versionHistory.map((version) => (
<button
key={version.versionId}
className="layout-wrapper"
onClick={() => {
handleSelectLayout(version);
setShowLayoutDropdown(false);
}}
>
<LayoutIcon />
<div className="layout">
{version.versionName}
</div>
</button>
))}
</div>
</div>
)}
</div>
)}
</div>
)}
{/* Always show after layout is selected */}
</div>
</div>
{/* Always show after layout is selected */}
</div>
</div>
)}
</>
);
};

View File

@@ -0,0 +1,44 @@
import React, { Suspense, useEffect, useState } from "react";
import { RenderInNewWindow } from "../../templates/CreateNewWindow";
import { useSceneContext } from "../../../modules/scene/sceneContext";
import { useCreateNewWindow, useLoadingProgress } from "../../../store/builder/store";
import Scene from "../../../modules/scene/scene";
import ComparisonResult from "./ComparisonResult";
import Button from "./Button";
const NewWindowScene = () => {
const { versionStore } = useSceneContext();
const { selectedVersion } = versionStore();
const { setCreateNewWindow } = useCreateNewWindow();
const { loadingProgress } = useLoadingProgress();
return (
<>
{selectedVersion?.versionId && (
<div style={{ width: "100%", height: "100%" }}>
<Suspense fallback={null}>
<RenderInNewWindow
title="3D Viewer"
onClose={() => setCreateNewWindow(false)}
>
<div
style={{
position: "absolute",
top: "10px",
right: "900px",
zIndex: 1000,
}}
>
<Button />
</div>
<Scene layout="Comparison Layout" />
{!loadingProgress && <ComparisonResult />}
</RenderInNewWindow>
</Suspense>
</div>
)}
</>
);
};
export default NewWindowScene;

View File

@@ -1,185 +1,164 @@
import React, { useEffect, useState } from "react";
import {
LogListIcon,
CloseIcon,
ExpandIcon2,
} from "../../icons/ExportCommonIcons"; // Adjust path as needed
import { LogListIcon, CloseIcon, ExpandIcon2 } from "../../icons/ExportCommonIcons"; // Adjust path as needed
import { LogEntry, useLogger } from "./LoggerContext";
import { GetLogIcon } from "../../footer/getLogIcons";
import { RenderInNewWindow } from "../../templates/CreateNewWindow";
// --- Logs Component ---
type LogsProps = {
selectedTab: "all" | "info" | "warning" | "error";
setSelectedTab: (tab: "all" | "info" | "warning" | "error") => void;
clear: () => void;
filteredLogs: LogEntry[];
formatTimestamp: (date: Date) => string;
selectedTab: "all" | "info" | "warning" | "error";
setSelectedTab: (tab: "all" | "info" | "warning" | "error") => void;
clear: () => void;
filteredLogs: LogEntry[];
formatTimestamp: (date: Date) => string;
};
const Logs: React.FC<LogsProps> = ({
selectedTab,
setSelectedTab,
clear,
filteredLogs,
formatTimestamp,
selectedTab,
setSelectedTab,
clear,
filteredLogs,
formatTimestamp,
}) => {
return (
<>
<div className="log-nav-container">
<div className="log-nav-wrapper">
{["all", "info", "warning", "error"].map((type) => (
<button
id="log-type"
title="log-type"
key={type}
className={`log-nav ${selectedTab === type ? "active" : ""}`}
onClick={() => setSelectedTab(type as any)}
>
{`${type.charAt(0).toUpperCase() + type.slice(1)} Logs`}
</button>
))}
</div>
<button
id="clean-btn"
title="clear-btn"
className="clear-button"
onClick={clear}
>
clear
</button>
</div>
{/* Log Entries */}
<div className="log-entry-wrapper">
{filteredLogs.length > 0 ? (
filteredLogs.map((log) => (
<div key={log.id} className={`log-entry ${log.type}`}>
<div className="log-icon">{GetLogIcon(log.type)}</div>
<div className="log-entry-message-container">
<div className="log-entry-message">{log.message}</div>
<div className="message-time">
{formatTimestamp(log.timestamp)}
return (
<>
<div className="log-nav-container">
<div className="log-nav-wrapper">
{["all", "info", "warning", "error"].map((type) => (
<button
id="log-type"
title="log-type"
key={type}
className={`log-nav ${selectedTab === type ? "active" : ""}`}
onClick={() => setSelectedTab(type as any)}
>
{`${type.charAt(0).toUpperCase() + type.slice(1)} Logs`}
</button>
))}
</div>
</div>
<button id="clean-btn" title="clear-btn" className="clear-button" onClick={clear}>
clear
</button>
</div>
))
) : (
<div className="no-log">
There are no logs to display at the moment.
</div>
)}
</div>
</>
);
{/* Log Entries */}
<div className="log-entry-wrapper">
{filteredLogs.length > 0 ? (
filteredLogs.map((log) => (
<div key={log.id} className={`log-entry ${log.type}`}>
<div className="log-icon">{GetLogIcon(log.type)}</div>
<div className="log-entry-message-container">
<div className="log-entry-message">{log.message}</div>
<div className="message-time">{formatTimestamp(log.timestamp)}</div>
</div>
</div>
))
) : (
<div className="no-log">There are no logs to display at the moment.</div>
)}
</div>
</>
);
};
// --- LogList Component ---
const LogList: React.FC = () => {
const {
logs,
clear,
setIsLogListVisible,
isLogListVisible,
selectedTab,
setSelectedTab,
} = useLogger();
const { logs, clear, setIsLogListVisible, isLogListVisible, selectedTab, setSelectedTab } =
useLogger();
const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString();
const [open, setOpen] = useState(false);
const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString();
const [open, setOpen] = useState(false);
console.log('open: ', open);
const filteredLogs =
selectedTab === "all"
? [...logs].reverse()
: [...logs].filter((log) => log.type === selectedTab).reverse();
const filteredLogs =
selectedTab === "all"
? [...logs].reverse()
: [...logs].filter((log) => log.type === selectedTab).reverse();
useEffect(() => {
if (isLogListVisible && logs.length > 0) {
const lastLog = logs[logs.length - 1];
const validTypes = ["all", "info", "warning", "error"];
if (validTypes.includes(lastLog.type)) {
setSelectedTab(lastLog.type as any);
} else {
setSelectedTab("all");
}
}
// eslint-disable-next-line
}, [isLogListVisible]);
useEffect(() => {
if (isLogListVisible && logs.length > 0) {
const lastLog = logs[logs.length - 1];
const validTypes = ["all", "info", "warning", "error"];
if (validTypes.includes(lastLog.type)) {
setSelectedTab(lastLog.type as any);
} else {
setSelectedTab("all");
}
}
// eslint-disable-next-line
}, [isLogListVisible]);
return (
<>
{!open ? (
<div className="log-list-container" onClick={() => setIsLogListVisible(false)}>
{/* eslint-disable-next-line */}
<div
className="log-list-wrapper"
onClick={(e) => {
e.stopPropagation();
}}
>
<div className="log-header">
<div className="log-header-wrapper">
<div className="icon">
<LogListIcon />
</div>
<div className="head">Log List</div>
</div>
<div className="action-buttons-container">
<button
id="expand-log-btn"
title="open in new tab"
className="expand-btn"
onClick={() => {
setOpen(true);
}}
>
<ExpandIcon2 />
</button>
<button
id="close-btn"
title="close"
className="close"
onClick={() => setIsLogListVisible(false)}
>
<CloseIcon />
</button>
</div>
</div>
return (
<>
{!open ? (
<div
className="log-list-container"
onClick={() => setIsLogListVisible(false)}
>
{/* eslint-disable-next-line */}
<div
className="log-list-wrapper"
onClick={(e) => {
e.stopPropagation();
}}
>
<div className="log-header">
<div className="log-header-wrapper">
<div className="icon">
<LogListIcon />
{/* Logs Section */}
<Logs
selectedTab={selectedTab as any}
setSelectedTab={setSelectedTab as any}
clear={clear}
filteredLogs={filteredLogs}
formatTimestamp={formatTimestamp}
/>
</div>
</div>
<div className="head">Log List</div>
</div>
<div className="action-buttons-container">
<button
id="expand-log-btn"
title="open in new tab"
className="expand-btn"
onClick={() => {
setOpen(true);
}}
) : (
<RenderInNewWindow
title="Log list"
onClose={() => {
setOpen(false);
setIsLogListVisible(false);
}}
>
<ExpandIcon2 />
</button>
<button
id="close-btn"
title="close"
className="close"
onClick={() => setIsLogListVisible(false)}
>
<CloseIcon />
</button>
</div>
</div>
{/* Logs Section */}
<Logs
selectedTab={selectedTab as any}
setSelectedTab={setSelectedTab as any}
clear={clear}
filteredLogs={filteredLogs}
formatTimestamp={formatTimestamp}
/>
</div>
</div>
) : (
<RenderInNewWindow
title="Log list"
onClose={() => {
setOpen(false);
setIsLogListVisible(false);
}}
>
<div className="log-list-new-window-wrapper">
<Logs
selectedTab={selectedTab as any}
setSelectedTab={setSelectedTab as any}
clear={clear}
filteredLogs={filteredLogs}
formatTimestamp={formatTimestamp}
/>
</div>
</RenderInNewWindow>
)}
</>
);
<div className="log-list-new-window-wrapper">
<Logs
selectedTab={selectedTab as any}
setSelectedTab={setSelectedTab as any}
clear={clear}
filteredLogs={filteredLogs}
formatTimestamp={formatTimestamp}
/>
</div>
</RenderInNewWindow>
)}
</>
);
};
export default LogList;

View File

@@ -13,13 +13,17 @@ import { getAssetFieldApi } from "../../../../../services/factoryBuilder/asset/f
import { ModelAnimator } from "./animator/modelAnimator";
import { useModelEventHandlers } from "./eventHandlers/useModelEventHandlers";
function Model({ asset, isRendered, loader }: Readonly<{ asset: Asset; isRendered: boolean; loader: GLTFLoader }>) {
function Model({
asset,
isRendered,
loader,
}: Readonly<{ asset: Asset; isRendered: boolean; loader: GLTFLoader }>) {
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
const savedTheme: string = localStorage.getItem("theme") || "light";
const { toolMode } = useToolMode();
const { toggleView } = useToggleView();
const { activeModule } = useModuleStore();
const { assetStore } = useSceneContext();
const { assetStore, layout } = useSceneContext();
const { resetAnimation, hasSelectedAsset, updateSelectedAsset, selectedAssets } = assetStore();
const { setDeletableFloorAsset } = useBuilderStore();
const [gltfScene, setGltfScene] = useState<GLTF["scene"] | null>(null);
@@ -52,7 +56,12 @@ function Model({ asset, isRendered, loader }: Readonly<{ asset: Asset; isRendere
}, [activeModule, toolMode, selectedAssets]);
useEffect(() => {
if (groupRef.current && selectedAssets.length === 1 && selectedAssets[0].userData.modelUuid === asset.modelUuid && hasSelectedAsset(asset.modelUuid)) {
if (
groupRef.current &&
selectedAssets.length === 1 &&
selectedAssets[0].userData.modelUuid === asset.modelUuid &&
hasSelectedAsset(asset.modelUuid)
) {
updateSelectedAsset(groupRef.current);
}
}, [isRendered, selectedAssets, asset]);
@@ -130,7 +139,10 @@ function Model({ asset, isRendered, loader }: Readonly<{ asset: Asset; isRendere
logModelStatus(assetId, "backend-loaded");
})
.catch((error) => {
console.error(`[Backend] Error storing/loading ${asset.modelName}:`, error);
console.error(
`[Backend] Error storing/loading ${asset.modelName}:`,
error
);
});
},
undefined,
@@ -144,7 +156,8 @@ function Model({ asset, isRendered, loader }: Readonly<{ asset: Asset; isRendere
});
}, []);
const { handleDblClick, handleClick, handlePointerOver, handlePointerOut, handleContextMenu } = useModelEventHandlers({ boundingBox, groupRef, asset });
const { handleDblClick, handleClick, handlePointerOver, handlePointerOut, handleContextMenu } =
useModelEventHandlers({ boundingBox, groupRef, asset });
return (
<group
@@ -197,9 +210,25 @@ function Model({ asset, isRendered, loader }: Readonly<{ asset: Asset; isRendere
<ModelAnimator asset={asset} gltfScene={gltfScene} />
</>
) : (
<>{!hasSelectedAsset(asset.modelUuid) && <AssetBoundingBox name="Asset Fallback" boundingBox={boundingBox} color="gray" lineWidth={2.5} />}</>
<>
{!hasSelectedAsset(asset.modelUuid) && (
<AssetBoundingBox
name="Asset Fallback"
boundingBox={boundingBox}
color="gray"
lineWidth={2.5}
/>
)}
</>
)}
{hasSelectedAsset(asset.modelUuid) && (
<AssetBoundingBox
name="Asset BBox"
boundingBox={boundingBox}
color={savedTheme === "dark" ? "#c4abf1" : "#6f42c1"}
lineWidth={2.7}
/>
)}
{hasSelectedAsset(asset.modelUuid) && <AssetBoundingBox name="Asset BBox" boundingBox={boundingBox} color={savedTheme === "dark" ? "#c4abf1" : "#6f42c1"} lineWidth={2.7} />}
</>
)}
</group>

View File

@@ -3,7 +3,11 @@ import { Group, Vector3 } from "three";
import { CameraControls } from "@react-three/drei";
import { GLTFLoader } from "three/examples/jsm/Addons";
import { useThree, useFrame } from "@react-three/fiber";
import { useContextActionStore, useLimitDistance, useRenderDistance } from "../../../../store/builder/store";
import {
useContextActionStore,
useLimitDistance,
useRenderDistance,
} from "../../../../store/builder/store";
import { useSelectedAsset } from "../../../../store/simulation/useSimulationStore";
import { useSceneContext } from "../../../scene/sceneContext";
import useZoomMesh from "../../hooks/useZoomMesh";
@@ -11,12 +15,14 @@ import useCallBackOnKey from "../../../../utils/hooks/useCallBackOnKey";
import Model from "./model/model";
const distanceWorker = new Worker(new URL("../../../../services/factoryBuilder/webWorkers/distanceWorker.js", import.meta.url));
const distanceWorker = new Worker(
new URL("../../../../services/factoryBuilder/webWorkers/distanceWorker.js", import.meta.url)
);
function Models({ loader }: { readonly loader: GLTFLoader }) {
const { controls, camera } = useThree();
const assetGroupRef = useRef<Group>(null);
const { assetStore } = useSceneContext();
const { assetStore, layout } = useSceneContext();
const { assets, selectedAssets, getSelectedAssetUuids } = assetStore();
const { selectedAsset, clearSelectedAsset } = useSelectedAsset();
const { contextAction, setContextAction } = useContextActionStore();
@@ -30,6 +36,14 @@ function Models({ loader }: { readonly loader: GLTFLoader }) {
// console.log(assets);
}, [assets]);
useEffect(() => {
const initialRenderMap: Record<string, boolean> = {};
assets.forEach((asset) => {
initialRenderMap[asset.modelUuid] = true;
});
setRenderMap(initialRenderMap);
}, [assets.length]);
useEffect(() => {
if (contextAction === "focusAsset") {
zoomMeshes(getSelectedAssetUuids());
@@ -55,20 +69,51 @@ function Models({ loader }: { readonly loader: GLTFLoader }) {
return { ...prev, [modelUuid]: shouldRender };
});
};
}, []);
return () => {
distanceWorker.terminate();
};
}, [distanceWorker, layout]);
useFrame(() => {
camera.getWorldPosition(cameraPos.current);
for (const asset of assets) {
const isRendered = renderMap[asset.modelUuid] ?? false;
distanceWorker.postMessage({
modelUuid: asset.modelUuid,
assetPosition: { x: asset.position[0], y: asset.position[1], z: asset.position[2] },
cameraPosition: cameraPos.current,
limitDistance,
renderDistance,
isRendered,
});
// distanceWorker.postMessage({
// modelUuid: asset.modelUuid,
// assetPosition: { x: asset.position[0], y: asset.position[1], z: asset.position[2] },
// cameraPosition: cameraPos.current,
// limitDistance,
// renderDistance,
// isRendered,
// });
const assetVec = new Vector3(...asset.position);
const cameraVec = new Vector3(
cameraPos.current.x,
cameraPos.current.y,
cameraPos.current.z
);
const distance = assetVec.distanceTo(cameraVec);
if (limitDistance) {
if (!isRendered && distance <= renderDistance) {
setRenderMap((prev) => {
if (prev[asset.modelUuid] === true) return prev;
return { ...prev, [asset.modelUuid]: true };
});
} else if (isRendered && distance > renderDistance) {
setRenderMap((prev) => {
if (prev[asset.modelUuid] === false) return prev;
return { ...prev, [asset.modelUuid]: false };
});
}
} else if (!isRendered) {
setRenderMap((prev) => {
if (prev[asset.modelUuid] === true) return prev;
return { ...prev, [asset.modelUuid]: true };
});
}
}
});
@@ -88,7 +133,12 @@ function Models({ loader }: { readonly loader: GLTFLoader }) {
}}
>
{assets.map((asset) => (
<Model key={asset.modelUuid} asset={asset} isRendered={renderMap[asset.modelUuid] ?? false} loader={loader} />
<Model
key={asset.modelUuid}
asset={asset}
isRendered={renderMap[asset.modelUuid] ?? false}
loader={loader}
/>
))}
</group>
);

View File

@@ -2,13 +2,11 @@ import { Vector3 } from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { CameraControls } from "@react-three/drei";
import { useSceneContext } from "../sceneContext";
import { useIsComparing } from "../../../store/builder/store";
import { useCreateNewWindow, useIsComparing } from "../../../store/builder/store";
import { useSceneStore } from "../../../store/scene/useSceneStore";
import { useSimulationState } from "../../../store/simulation/useSimulationStore";
import useModuleStore from "../../../store/ui/useModuleStore";
import * as CONSTANTS from "../../../types/world/worldConstants";
function SyncCam() {
const { layout } = useSceneContext();
const { controls } = useThree();
@@ -16,19 +14,39 @@ function SyncCam() {
const { activeModule } = useModuleStore();
const { comparisonScene } = useSimulationState();
const { setCamera, camState } = useSceneStore();
const { windowRendered } = useCreateNewWindow();
function getControls() {
const position = (controls as CameraControls).getPosition(new Vector3());
const target = (controls as CameraControls).getTarget(new Vector3());
setCamera(position, target);
}
function setControls() {
(controls as CameraControls).setLookAt(
camState.position.x,
camState.position.y,
camState.position.z,
camState.target.x,
camState.target.y,
camState.target.z,
true
);
}
useFrame(() => {
if (layout === "Comparison Layout" && controls && camState) {
(controls as any).mouseButtons.left = CONSTANTS.controlsTransition.leftMouse;
(controls as any).mouseButtons.right = CONSTANTS.controlsTransition.rightMouse;
(controls as any).mouseButtons.wheel = CONSTANTS.controlsTransition.wheelMouse;
(controls as any).mouseButtons.middle = CONSTANTS.controlsTransition.middleMouse;
(controls as CameraControls).setLookAt(camState.position.x, camState.position.y, camState.position.z, camState.target.x, camState.target.y, camState.target.z, true);
}
if (layout === "Main Layout" && controls && isComparing && activeModule === "simulation" && comparisonScene) {
const position = (controls as CameraControls).getPosition(new Vector3());
const target = (controls as CameraControls).getTarget(new Vector3());
setCamera(position, target);
if (
controls &&
isComparing &&
activeModule === "simulation" &&
comparisonScene &&
camState
) {
if (layout === windowRendered) {
getControls();
} else {
setControls();
}
}
});

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo } from "react";
import { Canvas } from "@react-three/fiber";
import { Canvas, useThree } from "@react-three/fiber";
import { KeyboardControls } from "@react-three/drei";
import { useSceneContext } from "./sceneContext";
@@ -11,13 +11,17 @@ import Collaboration from "../collaboration/collaboration";
import useModuleStore from "../../store/ui/useModuleStore";
import { useParams } from "react-router-dom";
import { getUserData } from "../../functions/getUserData";
import { useLoadingProgress } from "../../store/builder/store";
import { useCreateNewWindow, useLoadingProgress } from "../../store/builder/store";
import { useSocketStore } from "../../store/socket/useSocketStore";
import { Color, SRGBColorSpace } from "three";
import { compressImage } from "../../utils/compressImage";
import { ALPHA_ORG } from "../../pages/Dashboard";
export default function Scene({ layout }: { readonly layout: "Main Layout" | "Comparison Layout" }) {
export default function Scene({
layout,
}: {
readonly layout: "Main Layout" | "Comparison Layout";
}) {
const map = useMemo(
() => [
{ name: "forward", keys: ["ArrowUp", "w", "W"] },
@@ -35,11 +39,16 @@ export default function Scene({ layout }: { readonly layout: "Main Layout" | "Co
const { projectSocket } = useSocketStore();
const { activeModule } = useModuleStore();
const { loadingProgress } = useLoadingProgress();
const { setWindowRendered } = useCreateNewWindow();
useEffect(() => {
if (!projectId || loadingProgress !== 0) return;
const canvas = document.getElementById("sceneCanvas")?.getElementsByTagName("canvas")[0];
if (!canvas || !(layoutType === "default" || (layoutType === "useCase" && organization === ALPHA_ORG))) return;
if (
!canvas ||
!(layoutType === "default" || (layoutType === "useCase" && organization === ALPHA_ORG))
)
return;
compressImage(canvas.toDataURL("image/png")).then((screenshotDataUrl) => {
const updateProjects = {
projectId,
@@ -64,11 +73,21 @@ export default function Scene({ layout }: { readonly layout: "Main Layout" | "Co
onContextMenu={(e) => {
e.preventDefault();
}}
resize={{ polyfill: ResizeObserver }}
style={{ width: "100vw", height: "100vh", background: "#202020" }}
performance={{ min: 0.9, max: 1.0 }}
onCreated={(e) => {
e.scene.background = layout === "Main Layout" ? null : new Color(0x19191d);
}}
gl={{ outputColorSpace: SRGBColorSpace, powerPreference: "high-performance", antialias: true, preserveDrawingBuffer: true }}
gl={{
outputColorSpace: SRGBColorSpace,
powerPreference: "high-performance",
antialias: true,
preserveDrawingBuffer: true,
}}
onPointerEnter={() => {
setWindowRendered(layout);
}}
>
<Setup />
<Collaboration />

View File

@@ -171,6 +171,7 @@ const Project: React.FC = () => {
<MainScene />
</SceneProvider>
<SceneProvider layout="Comparison Layout" layoutType={layoutType}>
<ComparisonScene />
</SceneProvider>
{selectedUser && <FollowPerson />}

View File

@@ -138,7 +138,8 @@ export const useDrieTemp = create<any>((set: any) => ({
export const useDrieUIValue = create<any>((set: any) => ({
drieUIValue: { touch: null, temperature: null, humidity: null },
setDrieUIValue: (x: any) => set((state: any) => ({ drieUIValue: { ...state.drieUIValue, ...x } })),
setDrieUIValue: (x: any) =>
set((state: any) => ({ drieUIValue: { ...state.drieUIValue, ...x } })),
setTouch: (value: any) =>
set((state: any) => ({
@@ -462,9 +463,18 @@ interface DecalStore {
// Create the Zustand store with types
export const useDecalStore = create<DecalStore>((set) => ({
selectedSubCategory: "Safety",
setSelectedSubCategory: (subCategory: string | null) => set({ selectedSubCategory: subCategory }),
setSelectedSubCategory: (subCategory: string | null) =>
set({ selectedSubCategory: subCategory }),
}));
export const comparsionMaterialData = create<any>((set: any) => ({
materialData: [],
setMaterialData: (x: any) => set({ materialData: x }),
}));
export const useCreateNewWindow = create<any>((set: any) => ({
createNewWindow: false,
setCreateNewWindow: (x: any) => set({ createNewWindow: x }),
windowRendered: "",
setWindowRendered: (x: any) => set({ windowRendered: x }),
}));