first commit

This commit is contained in:
2025-06-10 15:28:23 +05:30
commit e22a2dc275
699 changed files with 100382 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
import React, { useState } from "react";
interface HandleDivProps {
switchesRef: React.RefObject<HTMLDivElement>;
setMenuLeftPosition: React.Dispatch<React.SetStateAction<number>>;
setMenuTopPosition: React.Dispatch<React.SetStateAction<number>>;
setMenuVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
// The functional component that handles right-click (contextmenu) events
export default function ContextMenuHandler({
switchesRef,
setMenuLeftPosition,
setMenuTopPosition,
setMenuVisible,
}: HandleDivProps) {
const [width, setWidth] = useState<number>(0);
const [height, setHeight] = useState<number>(0);
// Function to handle the contextmenu event when a right-click happens
const handleClick = (event: MouseEvent) => {
event.preventDefault();
const targets = event.target as HTMLElement;
const isInsideSwitches = switchesRef.current?.contains(
targets as Node
);
const rect = switchesRef.current?.getBoundingClientRect();
if (!rect) return;
const totalHeight = rect.height + rect.top;
const totalWidth = rect.width + rect.left;
// Calculate the new position for the context menu
if (isInsideSwitches) {
const yPosition = event.clientY;
const xPosition = event.clientX;
//for top contextmenu handling
if (
totalHeight - yPosition > 20 &&
totalHeight - yPosition < 260
) {
const minTop = yPosition - 110;
setMenuTopPosition(minTop);
} else if (
totalHeight - yPosition >= 260 &&
yPosition > height - 73
) {
const minTop = yPosition + 115;
setMenuTopPosition(minTop);
}
// for top contextmenu handling
if (
totalWidth - xPosition > 500 &&
totalWidth - xPosition < 900
) {
const minLeft = xPosition + 80;
setMenuLeftPosition(minLeft);
} else if (
totalWidth - xPosition > 10 &&
totalWidth - xPosition > 150
) {
const minLeft = xPosition + 80;
setMenuLeftPosition(minLeft);
} else {
const minLeft = xPosition - 80;
setMenuLeftPosition(minLeft);
}
// setMenuVisible(true);
} else {
setMenuVisible(false);
}
};
React.useEffect(() => {
const element = switchesRef.current;
// Create a resize observer
const resizeObserver = new ResizeObserver((entries) => {
if (entries.length > 0) {
// Update the width state with the new width of the element
const { width, height } = entries[0].contentRect;
setWidth(width);
setHeight(height);
}
});
// Start observing the element's width changes
if (element) {
resizeObserver.observe(element);
}
// Cleanup observer on component unmount
return () => {
if (element) {
resizeObserver.unobserve(element);
}
};
}, [height, width]);
React.useEffect(() => {
let drag = false;
let isRightMouseDown = false;
const handleDown = (event: MouseEvent) => {
if (event.button === 2) {
isRightMouseDown = true;
drag = false;
}
}
const handleUp = (event: MouseEvent) => {
if (event.button === 2) {
isRightMouseDown = false;
if (!drag) {
handleClick(event);
}
};
}
const handleMove = (event: MouseEvent) => {
if (isRightMouseDown) { drag = true; };
}
document.addEventListener("mousedown", handleDown);
document.addEventListener("mousemove", handleMove);
document.addEventListener("mouseup", handleUp);
return () => {
document.removeEventListener("mousedown", handleDown);
document.removeEventListener("mousemove", handleMove);
document.removeEventListener("mouseup", handleUp);
};
}, []);
return null;
}

View File

@@ -0,0 +1,22 @@
export function snapControls(value: number, event: string): number {
const CTRL_DISTANCE = 1; // Snap to whole numbers when Ctrl is pressed
const SHIFT_DISTANCE = 0.01; // Snap to half-step increments when Shift is pressed
const CTRL_SHIFT_DISTANCE = 0.1; // Snap to fine increments when both Ctrl and Shift are pressed
switch (event) {
case "Ctrl":
return Math.round(value / CTRL_DISTANCE) * CTRL_DISTANCE;
case "Shift":
return Math.round(value / SHIFT_DISTANCE) * SHIFT_DISTANCE;
case "Ctrl+Shift":
const base = Math.floor(value / CTRL_DISTANCE) * CTRL_DISTANCE;
const offset =
Math.round((value - base) / CTRL_SHIFT_DISTANCE) * CTRL_SHIFT_DISTANCE;
return base + offset;
default:
return value; // No snapping if no modifier key is pressed
}
}

View File

@@ -0,0 +1,45 @@
const DB_NAME = 'GLTFStorage';
const STORE_NAME = 'models';
const DB_VERSION = 1;
export function initializeDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
export async function storeGLTF(key: string, file: Blob): Promise<void> {
const db = await initializeDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.put(file, key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
export async function retrieveGLTF(key: string): Promise<Blob | undefined> {
const db = await initializeDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(key);
request.onsuccess = () => resolve(request.result as Blob | undefined);
request.onerror = () => reject(request.error);
});
}

View File

@@ -0,0 +1,33 @@
import React from "react";
interface OuterClickProps {
contextClassName: string[]; // Make sure this is an array of strings
setMenuVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
export default function OuterClick({
contextClassName,
setMenuVisible,
}: OuterClickProps) {
const handleClick = (event: MouseEvent) => {
const targets = event.target as HTMLElement;
// Check if the click is outside of any of the provided class names
const isOutside = contextClassName.every(
(className) => !targets.closest(`.${className}`)
);
if (isOutside) {
setMenuVisible(false); // Close the menu by updating the state
}
};
// Add event listener on mount and remove it on unmount
React.useEffect(() => {
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
}, [contextClassName]); // Add contextClassName to dependency array to handle any changes
return null; // This component doesn't render anything
}

View File

@@ -0,0 +1,31 @@
// Function to detect if Shift, Ctrl, Alt, or combinations are pressed
// and return the corresponding key combination string
export const detectModifierKeys = (event: KeyboardEvent): string => {
const modifiers = [
event.ctrlKey ? "Ctrl" : "",
event.altKey ? "Alt" : "",
event.shiftKey ? "Shift" : "",
event.metaKey ? "Meta" : "" // Add support for Command/Win key
].filter(Boolean);
// Ignore modifier keys when they're pressed alone
const isModifierKey = [
"Control", "Shift", "Alt", "Meta",
"Ctrl", "AltGraph", "OS" // Additional modifier key aliases
].includes(event.key);
const mainKey = isModifierKey ? "" : event.key.toUpperCase();
// Handle special cases for keys with different representations
const normalizedKey = mainKey === " " ? "Space" : mainKey;
// Build the combination string
if (modifiers.length > 0 && normalizedKey) {
return `${modifiers.join("+")}+${normalizedKey}`;
} else if (modifiers.length > 0) {
return modifiers.join("+");
} else {
return normalizedKey;
}
};

View File

@@ -0,0 +1,254 @@
import React, { useEffect } from "react";
import useModuleStore, { useThreeDStore } from "../../store/useModuleStore";
import { usePlayerStore, useToggleStore } from "../../store/useUIToggleStore";
import {
useActiveSubTool,
useActiveTool,
useAddAction,
useDeleteTool,
useRenameModeStore,
useSaveVersion,
useSelectedFloorItem,
useSelectedWallItem,
useShortcutStore,
useToggleView,
useToolMode,
useViewSceneStore,
} from "../../store/builder/store";
import useCameraModeStore, {
usePlayButtonStore,
} from "../../store/usePlayButtonStore";
import { detectModifierKeys } from "./detectModifierKeys";
import { useSelectedZoneStore } from "../../store/visualization/useZoneStore";
import { useLogger } from "../../components/ui/log/LoggerContext";
import { useComparisonProduct } from "../../store/simulation/useSimulationStore";
const KeyPressListener: React.FC = () => {
const { clearComparisonProduct } = useComparisonProduct();
const { activeModule, setActiveModule } = useModuleStore();
const { setActiveSubTool } = useActiveSubTool();
const { toggleUILeft, toggleUIRight, setToggleUI } = useToggleStore();
const { setToggleThreeD } = useThreeDStore();
const { setToolMode } = useToolMode();
const { isPlaying, setIsPlaying } = usePlayButtonStore();
const { toggleView, setToggleView } = useToggleView();
const { setDeleteTool } = useDeleteTool();
const { setAddAction } = useAddAction();
const { setSelectedWallItem } = useSelectedWallItem();
const { setActiveTool } = useActiveTool();
const { clearSelectedZone } = useSelectedZoneStore();
const { showShortcuts, setShowShortcuts } = useShortcutStore();
const { setWalkMode } = useCameraModeStore();
const { setIsVersionSaved } = useSaveVersion();
const { isLogListVisible, setIsLogListVisible } = useLogger();
const { hidePlayer, setHidePlayer } = usePlayerStore();
const { viewSceneLabels, setViewSceneLabels } = useViewSceneStore();
const { isRenameMode, setIsRenameMode } = useRenameModeStore();
const { selectedFloorItem } = useSelectedFloorItem();
const isTextInput = (element: Element | null): boolean =>
element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element?.getAttribute("contenteditable") === "true";
const handleModuleSwitch = (keyCombination: string) => {
const modules: Record<string, string> = {
"1": "builder",
"2": "simulation",
"3": "visualization",
"4": "market",
};
const module = modules[keyCombination];
if (module && !toggleView) {
console.log("hi");
setActiveTool("cursor");
setActiveSubTool("cursor");
if (module === "market") setToggleUI(false, false);
setActiveModule(module);
}
};
const handlePrimaryTools = (key: string) => {
const toolMap: Record<string, string> = {
V: "cursor",
X: "delete",
H: "free-hand",
};
const tool = toolMap[key];
if (tool) {
setActiveTool(tool);
setActiveSubTool(tool);
}
};
const handleBuilderShortcuts = (key: string) => {
if (activeModule !== "builder" || isPlaying) return;
if (key === "TAB") {
const toggleTo2D = toggleView;
setToggleView(!toggleTo2D);
setToggleThreeD(toggleTo2D);
setToggleUI(toggleTo2D, toggleTo2D);
if (toggleTo2D) {
setSelectedWallItem(null);
setDeleteTool(false);
setAddAction(null);
}
setActiveTool("cursor");
setActiveSubTool("cursor");
return;
}
// These should only apply in 2D view
const twoDToolConfigs: Record<string, { tool: string; mode: string }> = {
Q: { tool: "draw-wall", mode: "Wall" },
"6": { tool: "draw-wall", mode: "Wall" },
R: { tool: "draw-aisle", mode: "Aisle" },
"7": { tool: "draw-aisle", mode: "Aisle" },
E: { tool: "draw-zone", mode: "Zone" },
"8": { tool: "draw-zone", mode: "Zone" },
T: { tool: "draw-floor", mode: "Floor" },
"9": { tool: "draw-floor", mode: "Floor" },
};
const config = twoDToolConfigs[key];
if (toggleView && config) {
setActiveTool(config.tool);
setToolMode(config.mode);
}
// Measurement tool should work in both 2D and 3D
if (key === "M") {
setActiveTool("measure");
setToolMode("MeasurementScale");
}
};
const handleSidebarShortcuts = (key: string) => {
if (activeModule === "market") return;
const updateLocalStorage = (left: boolean, right: boolean) => {
localStorage.setItem("navBarUiLeft", JSON.stringify(left));
localStorage.setItem("navBarUiRight", JSON.stringify(right));
};
switch (key) {
case "Ctrl+\\":
if (toggleUILeft === toggleUIRight) {
const newState = !toggleUILeft;
setToggleUI(newState, newState);
updateLocalStorage(newState, newState);
} else {
setToggleUI(true, true);
updateLocalStorage(true, true);
}
break;
case "Ctrl+]":
setToggleUI(toggleUILeft, !toggleUIRight);
updateLocalStorage(toggleUILeft, !toggleUIRight);
break;
case "Ctrl+[":
setToggleUI(!toggleUILeft, toggleUIRight);
updateLocalStorage(!toggleUILeft, toggleUIRight);
break;
default:
break;
}
};
const handleKeyPress = (event: KeyboardEvent) => {
if (isTextInput(document.activeElement)) return;
const keyCombination = detectModifierKeys(event);
if (isTextInput(document.activeElement) && keyCombination !== "ESCAPE")
return;
if (keyCombination === "ESCAPE") {
setWalkMode(false);
setActiveTool("cursor");
setActiveSubTool("cursor");
setIsPlaying(false);
clearSelectedZone();
setShowShortcuts(false);
setIsVersionSaved(false);
clearComparisonProduct();
setIsLogListVisible(false);
setIsRenameMode(false);
}
if (
!keyCombination ||
["F5", "F11", "F12"].includes(event.key) ||
keyCombination === "Ctrl+R"
)
return;
event.preventDefault();
// Shortcuts specific for sidebar visibility toggle and others specific to sidebar if added
handleSidebarShortcuts(keyCombination);
// Active module selection (builder, simulation, etc.)
handleModuleSwitch(keyCombination);
// Common editing tools: cursor | delete | free-hand
handlePrimaryTools(keyCombination);
// Shortcuts specific to the builder module (e.g., drawing and measurement tools)
handleBuilderShortcuts(keyCombination);
// Shortcut to enter play mode
if (keyCombination === "Ctrl+P" && !toggleView) {
setIsPlaying(true);
}
if (keyCombination === "L") {
setIsLogListVisible(!isLogListVisible);
}
if (keyCombination === "H") {
setHidePlayer(!hidePlayer);
}
if (keyCombination === "Ctrl+Shift+?") {
setShowShortcuts(!showShortcuts);
}
if (keyCombination === "U") {
console.log("viewSceneLabels: ", viewSceneLabels);
setViewSceneLabels((prev) => !prev);
}
if (selectedFloorItem && keyCombination === "F2") {
setIsRenameMode(true);
}
// Placeholder for future implementation
if (
["Ctrl+Z", "Ctrl+Y", "Ctrl+Shift+Z", "Ctrl+F"].includes(keyCombination)
) {
// Implement undo/redo/help/find/shortcuts
}
};
useEffect(() => {
window.addEventListener("keydown", handleKeyPress);
return () => window.removeEventListener("keydown", handleKeyPress);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
activeModule,
toggleUIRight,
toggleUILeft,
toggleView,
showShortcuts,
isPlaying,
isLogListVisible,
hidePlayer,
selectedFloorItem,
isRenameMode,
]);
return null;
};
export default KeyPressListener;

21
app/src/utils/theme.ts Normal file
View File

@@ -0,0 +1,21 @@
export { };
// Function to set the theme based on user preference or system default
function setTheme() {
const savedTheme: string | null = localStorage.getItem('theme');
const systemPrefersDark: boolean = window.matchMedia('(prefers-color-scheme: dark)').matches;
const defaultTheme: string = savedTheme ?? (systemPrefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', defaultTheme);
localStorage.setItem('theme', defaultTheme);
}
// Function to toggle the theme
export function toggleTheme() {
const currentTheme: string | null = document.documentElement.getAttribute('data-theme');
const newTheme: string = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
setTheme();