+ {visibility.rename && (
+
+ )}
+ {visibility.focus && (
+
+ )}
+ {visibility.flipX && (
+
+ )}
+ {visibility.flipZ && (
+
+ )}
+ {(visibility.move || visibility.rotate) && (
+
+
+
+
+ {visibility.move && (
+
+ )}
+ {visibility.rotate && (
+
+ )}
+
+
+ )}
+ {visibility.duplicate && (
+
+
+
Ctrl + D
+
+ )}
+ {visibility.copy && (
+
+
+
Ctrl + C
+
+ )}
+ {visibility.paste && (
+
+
+
Ctrl + V
+
+ )}
+ {visibility.modifier && (
+
+ )}
+ {(visibility.group || visibility.array) && (
+
+
+
+ {visibility.group && (
+
+
+ Ctrl + G
+
+ )}
+ {visibility.array && (
+
+ )}
+
+
+ )}
+ {visibility.delete && (
+
+ )}
+
+ );
+};
+
+export default ContextMenu;
diff --git a/app/src/functions/isPointInsidePolygon.ts b/app/src/functions/isPointInsidePolygon.ts
new file mode 100644
index 0000000..7fa1846
--- /dev/null
+++ b/app/src/functions/isPointInsidePolygon.ts
@@ -0,0 +1,20 @@
+export const isPointInsidePolygon = (
+ point: [number, number],
+ polygon: [number, number][]
+) => {
+ let inside = false;
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
+ const xi = polygon[i][0],
+ zi = polygon[i][1];
+ const xj = polygon[j][0],
+ zj = polygon[j][1];
+
+ const intersect =
+ // eslint-disable-next-line no-mixed-operators
+ zi > point[1] !== zj > point[1] &&
+ point[0] < ((xj - xi) * (point[1] - zi)) / (zj - zi + 0.000001) + xi;
+
+ if (intersect) inside = !inside;
+ }
+ return inside;
+};
\ No newline at end of file
diff --git a/app/src/modules/scene/controls/contextControls/contextControls.tsx b/app/src/modules/scene/controls/contextControls/contextControls.tsx
new file mode 100644
index 0000000..38219ec
--- /dev/null
+++ b/app/src/modules/scene/controls/contextControls/contextControls.tsx
@@ -0,0 +1,194 @@
+import { useEffect, useState } from 'react';
+import { useThree } from '@react-three/fiber';
+import { CameraControls, Html, ScreenSpace } from '@react-three/drei';
+import { useContextActionStore, useRenameModeStore, useSelectedAssets } from '../../../../store/builder/store';
+import ContextMenu from '../../../../components/ui/menu/contextMenu';
+
+function ContextControls() {
+ const { gl, controls } = useThree();
+ const [canRender, setCanRender] = useState(false);
+ const [visibility, setVisibility] = useState({ rename: true, focus: true, flipX: true, flipZ: true, move: true, rotate: true, duplicate: true, copy: true, paste: true, modifier: false, group: false, array: false, delete: true, });
+ const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
+ const { selectedAssets } = useSelectedAssets();
+ const { setContextAction } = useContextActionStore();
+ const { setIsRenameMode } = useRenameModeStore();
+
+ useEffect(() => {
+ if (selectedAssets.length === 1) {
+ setVisibility({
+ rename: true,
+ focus: true,
+ flipX: true,
+ flipZ: true,
+ move: true,
+ rotate: true,
+ duplicate: true,
+ copy: true,
+ paste: true,
+ modifier: false,
+ group: false,
+ array: false,
+ delete: true,
+ });
+ } else if (selectedAssets.length > 1) {
+ setVisibility({
+ rename: false,
+ focus: true,
+ flipX: true,
+ flipZ: true,
+ move: true,
+ rotate: true,
+ duplicate: true,
+ copy: true,
+ paste: true,
+ modifier: false,
+ group: true,
+ array: false,
+ delete: true,
+ });
+ } else {
+ setVisibility({
+ rename: false,
+ focus: false,
+ flipX: false,
+ flipZ: false,
+ move: false,
+ rotate: false,
+ duplicate: false,
+ copy: false,
+ paste: false,
+ modifier: false,
+ group: false,
+ array: false,
+ delete: false,
+ });
+ }
+ }, [selectedAssets]);
+
+ useEffect(() => {
+ const canvasElement = gl.domElement;
+
+ const handleContextClick = (event: MouseEvent) => {
+ event.preventDefault();
+ if (selectedAssets.length > 0) {
+ setMenuPosition({ x: event.clientX - gl.domElement.width / 2, y: event.clientY - gl.domElement.height / 2 });
+ setCanRender(true);
+ if (controls) {
+ (controls as CameraControls).enabled = false;
+ }
+ } else {
+ setCanRender(false);
+ if (controls) {
+ (controls as CameraControls).enabled = true;
+ }
+ }
+ };
+
+ if (selectedAssets.length > 0) {
+ canvasElement.addEventListener('contextmenu', handleContextClick)
+ } else {
+ setCanRender(false);
+ if (controls) {
+ (controls as CameraControls).enabled = true;
+ }
+ setMenuPosition({ x: 0, y: 0 });
+ }
+
+ return () => {
+ canvasElement.removeEventListener('contextmenu', handleContextClick);
+ };
+ }, [gl, selectedAssets]);
+
+ const handleAssetRename = () => {
+ setCanRender(false);
+ if (controls) {
+ (controls as CameraControls).enabled = true;
+ }
+ setContextAction("renameAsset");
+ setIsRenameMode(true);
+ }
+ const handleAssetFocus = () => {
+ setCanRender(false);
+ if (controls) {
+ (controls as CameraControls).enabled = true;
+ }
+ setContextAction("focusAsset");
+ }
+ const handleAssetMove = () => {
+ setCanRender(false);
+ if (controls) {
+ (controls as CameraControls).enabled = true;
+ }
+ setContextAction("moveAsset")
+ }
+ const handleAssetRotate = () => {
+ setCanRender(false);
+ if (controls) {
+ (controls as CameraControls).enabled = true;
+ }
+ setContextAction("rotateAsset")
+ }
+ const handleAssetCopy = () => {
+ setCanRender(false);
+ if (controls) {
+ (controls as CameraControls).enabled = true;
+ }
+ setContextAction("copyAsset")
+ }
+ const handleAssetPaste = () => {
+ setCanRender(false);
+ if (controls) {
+ (controls as CameraControls).enabled = true;
+ }
+ setContextAction("pasteAsset")
+ }
+ const handleAssetDelete = () => {
+ setCanRender(false);
+ if (controls) {
+ (controls as CameraControls).enabled = true;
+ }
+ setContextAction("deleteAsset")
+ }
+ const handleAssetDuplicate = () => {
+ setCanRender(false);
+ if (controls) {
+ (controls as CameraControls).enabled = true;
+ }
+ setContextAction("duplicateAsset")
+ }
+
+ return (
+ <>
+ {canRender && (
+