first commit
This commit is contained in:
137
app/src/utils/contextmenuHandler.ts
Normal file
137
app/src/utils/contextmenuHandler.ts
Normal 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;
|
||||
}
|
||||
22
app/src/utils/handleSnap.ts
Normal file
22
app/src/utils/handleSnap.ts
Normal 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
|
||||
}
|
||||
}
|
||||
45
app/src/utils/indexDB/idbUtils.ts
Normal file
45
app/src/utils/indexDB/idbUtils.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
33
app/src/utils/outerClick.ts
Normal file
33
app/src/utils/outerClick.ts
Normal 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
|
||||
}
|
||||
31
app/src/utils/shortcutkeys/detectModifierKeys.ts
Normal file
31
app/src/utils/shortcutkeys/detectModifierKeys.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
254
app/src/utils/shortcutkeys/handleShortcutKeys.ts
Normal file
254
app/src/utils/shortcutkeys/handleShortcutKeys.ts
Normal 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
21
app/src/utils/theme.ts
Normal 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();
|
||||
Reference in New Issue
Block a user