From f593fcd578afcc90a297e511f3b92e38765b30bf Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Mon, 23 Jun 2025 13:38:26 +0530 Subject: [PATCH] Enhance builder functionality by implementing drag-and-drop for lines and points, adding hover effects, and improving wall distance display in the UI. --- .../aisle/Instances/aisleInstances.tsx | 4 - app/src/modules/builder/builder.tsx | 4 +- app/src/modules/builder/groups/zoneGroup.tsx | 6 - app/src/modules/builder/line/line.tsx | 136 ++++++++++++++++-- app/src/modules/builder/point/point.tsx | 63 +++++--- .../builder/wall/Instances/wallInstances.tsx | 44 ++++-- app/src/store/builder/useBuilderStore.ts | 12 ++ app/src/store/builder/useWallStore.tsx | 21 +++ 8 files changed, 237 insertions(+), 53 deletions(-) diff --git a/app/src/modules/builder/aisle/Instances/aisleInstances.tsx b/app/src/modules/builder/aisle/Instances/aisleInstances.tsx index ec38412..7683457 100644 --- a/app/src/modules/builder/aisle/Instances/aisleInstances.tsx +++ b/app/src/modules/builder/aisle/Instances/aisleInstances.tsx @@ -48,14 +48,11 @@ function AisleInstances() { {toggleView && } - ) })} diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index ab123ad..cbbf2fe 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -104,7 +104,7 @@ export default function Builder() { const { setWalls } = useWalls(); const { refTextupdate, setRefTextUpdate } = useRefTextUpdate(); const { projectId } = useParams(); - const { setHoveredPoint } = useBuilderStore(); + const { setHoveredPoint, setHoveredLine } = useBuilderStore(); const { userId, organization } = getUserData(); // const loader = new GLTFLoader(); @@ -127,7 +127,9 @@ export default function Builder() { dragPointControls ); } else { + setHoveredLine(null); setHoveredPoint(null); + state.gl.domElement.style.cursor = 'default'; setToolMode('cursor'); loadWalls(lines, setWalls); setUpdateScene(true); diff --git a/app/src/modules/builder/groups/zoneGroup.tsx b/app/src/modules/builder/groups/zoneGroup.tsx index bfaba3f..24f6ee4 100644 --- a/app/src/modules/builder/groups/zoneGroup.tsx +++ b/app/src/modules/builder/groups/zoneGroup.tsx @@ -384,13 +384,7 @@ const ZoneGroup: React.FC = () => { drag = true; } raycaster.setFromCamera(pointer, camera); - const intersects = raycaster.intersectObjects(groupsRef.current.children, true); - if (intersects.length > 0 && intersects[0].object.name.includes("point")) { - gl.domElement.style.cursor = toolMode === "move" ? "pointer" : "default"; - } else { - gl.domElement.style.cursor = "default"; - } if (isDragging && draggedSphere) { raycaster.setFromCamera(pointer, camera); const intersectionPoint = new THREE.Vector3(); diff --git a/app/src/modules/builder/line/line.tsx b/app/src/modules/builder/line/line.tsx index aa929d2..68402d9 100644 --- a/app/src/modules/builder/line/line.tsx +++ b/app/src/modules/builder/line/line.tsx @@ -1,13 +1,26 @@ import * as THREE from 'three'; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import * as Constants from '../../../types/world/worldConstants'; -import { Tube } from '@react-three/drei'; +import { DragControls, Tube } from '@react-three/drei'; +import { useToolMode } from '../../../store/builder/store'; +import { useBuilderStore } from '../../../store/builder/useBuilderStore'; +import { useWallStore } from '../../../store/builder/useWallStore'; +import { useThree } from '@react-three/fiber'; interface LineProps { points: [Point, Point]; } function Line({ points }: Readonly) { + const [isHovered, setIsHovered] = useState(false); + const { raycaster, camera, pointer, gl } = useThree(); + const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); + const [isDeletable, setIsDeletable] = useState(false); + const { toolMode } = useToolMode(); + const { removeWallByPoints, setPosition } = useWallStore(); + const [dragOffset, setDragOffset] = useState(null); + const { hoveredLine, setHoveredLine, hoveredPoint } = useBuilderStore(); + const path = useMemo(() => { const [start, end] = points.map(p => new THREE.Vector3(...p.position)); return new THREE.LineCurve3(start, end); @@ -44,16 +57,119 @@ function Line({ points }: Readonly) { } } + useEffect(() => { + if (toolMode === '2D-Delete') { + if (isHovered && !hoveredPoint) { + setIsDeletable(true); + } else { + setIsDeletable(false); + } + } else { + setIsDeletable(false); + } + }, [isHovered, colors.defaultLineColor, colors.defaultDeleteColor, toolMode, hoveredPoint]); + + useEffect(() => { + if (hoveredLine && (hoveredLine[0].pointUuid !== points[0].pointUuid || hoveredLine[1].pointUuid !== points[1].pointUuid)) { + setIsHovered(false); + } + }, [hoveredLine]) + + const handlePointClick = (points: [Point, Point]) => { + if (toolMode === '2D-Delete') { + if (points[0].pointType === 'Wall' && points[1].pointType === 'Wall') { + removeWallByPoints(points); + } + gl.domElement.style.cursor = 'default'; + } + } + + const handleDrag = (points: [Point, Point]) => { + if (toolMode === 'move' && isHovered && dragOffset) { + raycaster.setFromCamera(pointer, camera); + const intersectionPoint = new THREE.Vector3(); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + + if (hit) { + gl.domElement.style.cursor = 'move'; + const positionWithOffset = new THREE.Vector3().addVectors(hit, dragOffset); + + const start = new THREE.Vector3(...points[0].position); + const end = new THREE.Vector3(...points[1].position); + const midPoint = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5); + + const delta = new THREE.Vector3().subVectors(positionWithOffset, midPoint); + + const newStart = new THREE.Vector3().addVectors(start, delta); + const newEnd = new THREE.Vector3().addVectors(end, delta); + + setPosition(points[0].pointUuid, [newStart.x, newStart.y, newStart.z]); + setPosition(points[1].pointUuid, [newEnd.x, newEnd.y, newEnd.z]); + } + } + }; + + const handleDragStart = (points: [Point, Point]) => { + raycaster.setFromCamera(pointer, camera); + const intersectionPoint = new THREE.Vector3(); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + + if (hit && !hoveredPoint) { + const start = new THREE.Vector3(...points[0].position); + const end = new THREE.Vector3(...points[1].position); + const midPoint = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5); + + const offset = new THREE.Vector3().subVectors(midPoint, hit); + setDragOffset(offset); + } + }; + + const handleDragEnd = (points: [Point, Point]) => { + gl.domElement.style.cursor = 'default'; + setDragOffset(null); + if (toolMode !== 'move') return; + if (points[0].pointType === 'Wall' && points[1].pointType === 'Wall') { + // console.log('Wall after drag: ', points); + } + } + return ( - handleDragStart(points)} + onDrag={() => handleDrag(points)} + onDragEnd={() => handleDragEnd(points)} > - - + { + handlePointClick(points); + }} + onPointerOver={() => { + if (!hoveredLine) { + setHoveredLine(points); + setIsHovered(true) + if (toolMode === 'move' && !hoveredPoint) { + gl.domElement.style.cursor = 'pointer'; + } + } + }} + onPointerOut={() => { + if (hoveredLine) { + setHoveredLine(null); + gl.domElement.style.cursor = 'default'; + } + setIsHovered(false) + }} + > + + + ); } diff --git a/app/src/modules/builder/point/point.tsx b/app/src/modules/builder/point/point.tsx index 425d24b..3545e99 100644 --- a/app/src/modules/builder/point/point.tsx +++ b/app/src/modules/builder/point/point.tsx @@ -15,9 +15,10 @@ import { useSceneContext } from '../../scene/sceneContext'; function Point({ point }: { readonly point: Point }) { const materialRef = useRef(null); - const { raycaster, camera, pointer } = useThree(); + const { raycaster, camera, pointer, gl } = useThree(); const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); const [isHovered, setIsHovered] = useState(false); + const [dragOffset, setDragOffset] = useState(null); const { toolMode } = useToolMode(); const { aisleStore } = useSceneContext(); const { setPosition: setAislePosition, removePoint: removeAislePoint, getAislesByPointId } = aisleStore(); @@ -91,33 +92,45 @@ function Point({ point }: { readonly point: Point }) { }), [colors.defaultInnerColor, colors.defaultOuterColor]); const handleDrag = (point: Point) => { - if (toolMode === 'move' && isHovered) { + if (toolMode === 'move' && isHovered && dragOffset) { raycaster.setFromCamera(pointer, camera); const intersectionPoint = new THREE.Vector3(); - const position = raycaster.ray.intersectPlane(plane, intersectionPoint); - if (point.pointType === 'Aisle') { - if (position) { - const newPosition: [number, number, number] = [position.x, position.y, position.z]; - const aisleSnappedPosition = snapAisleAngle(newPosition); - const finalSnappedPosition = snapAislePoint(aisleSnappedPosition.position); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); - setAislePosition(point.pointUuid, finalSnappedPosition.position); + if (hit) { + gl.domElement.style.cursor = 'move'; + const positionWithOffset = new THREE.Vector3().addVectors(hit, dragOffset); + const newPosition: [number, number, number] = [positionWithOffset.x, positionWithOffset.y, positionWithOffset.z]; - } - } else if (point.pointType === 'Wall') { - if (position) { - const newPosition: [number, number, number] = [position.x, position.y, position.z]; - const wallSnappedPosition = snapWallAngle(newPosition); - const finalSnappedPosition = snapWallPoint(wallSnappedPosition.position); - - setWallPosition(point.pointUuid, finalSnappedPosition.position); + if (point.pointType === 'Aisle') { + const aisleSnapped = snapAisleAngle(newPosition); + const finalSnapped = snapAislePoint(aisleSnapped.position); + setAislePosition(point.pointUuid, finalSnapped.position); + } else if (point.pointType === 'Wall') { + const wallSnapped = snapWallAngle(newPosition); + const finalSnapped = snapWallPoint(wallSnapped.position); + setWallPosition(point.pointUuid, finalSnapped.position); } } } - } + }; + + const handleDragStart = (point: Point) => { + raycaster.setFromCamera(pointer, camera); + const intersectionPoint = new THREE.Vector3(); + const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); + + if (hit) { + const currentPosition = new THREE.Vector3(...point.position); + const offset = new THREE.Vector3().subVectors(currentPosition, hit); + setDragOffset(offset); + } + }; const handleDragEnd = (point: Point) => { - if (toolMode === '2D-Delete') return; + gl.domElement.style.cursor = 'default'; + setDragOffset(null); + if (toolMode !== 'move') return; if (point.pointType === 'Aisle') { const updatedAisles = getAislesByPointId(point.pointUuid); if (updatedAisles.length > 0 && projectId) { @@ -148,6 +161,7 @@ function Point({ point }: { readonly point: Point }) { setHoveredPoint(null); } } + gl.domElement.style.cursor = 'default'; } } @@ -165,8 +179,9 @@ function Point({ point }: { readonly point: Point }) { { handleDrag(point) }} - onDragEnd={() => { handleDragEnd(point) }} + onDragStart={() => handleDragStart(point)} + onDrag={() => handleDrag(point)} + onDragEnd={() => handleDragEnd(point)} > { if (!hoveredPoint) { setHoveredPoint(point); - setIsHovered(true) + setIsHovered(true); + if (toolMode === 'move') { + gl.domElement.style.cursor = 'pointer'; + } } }} onPointerOut={() => { if (hoveredPoint) { setHoveredPoint(null); + gl.domElement.style.cursor = 'default'; } setIsHovered(false) }} diff --git a/app/src/modules/builder/wall/Instances/wallInstances.tsx b/app/src/modules/builder/wall/Instances/wallInstances.tsx index 21e4819..eacf943 100644 --- a/app/src/modules/builder/wall/Instances/wallInstances.tsx +++ b/app/src/modules/builder/wall/Instances/wallInstances.tsx @@ -1,10 +1,12 @@ -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useWallStore } from '../../../../store/builder/useWallStore' import WallInstance from './instance/wallInstance'; import Line from '../../line/line'; import Point from '../../point/point'; import { useToggleView } from '../../../../store/builder/store'; import { Geometry } from '@react-three/csg'; +import { Vector3 } from 'three'; +import { Html } from '@react-three/drei'; function WallInstances() { const { walls } = useWallStore(); @@ -34,21 +36,12 @@ function WallInstances() { <> {!toggleView && ( - - {/* */} - {walls.map((wall) => ( ))} - - {/* - - - - */} )} @@ -61,11 +54,42 @@ function WallInstances() { + {walls.map((wall) => ( ))} + + {walls.map((wall) => { + const textPosition = new Vector3().addVectors(new Vector3(...wall.points[0].position), new Vector3(...wall.points[1].position)).divideScalar(2); + const distance = new Vector3(...wall.points[0].position).distanceTo(new Vector3(...wall.points[1].position)); + + return ( + < React.Fragment key={wall.wallUuid}> + {toggleView && + +
+ {distance.toFixed(2)} m +
+ + } + + ) + })} +
)} diff --git a/app/src/store/builder/useBuilderStore.ts b/app/src/store/builder/useBuilderStore.ts index da52a2e..086a020 100644 --- a/app/src/store/builder/useBuilderStore.ts +++ b/app/src/store/builder/useBuilderStore.ts @@ -9,6 +9,8 @@ interface BuilderState { snappedPoint: Point | null; snappedPosition: [number, number, number] | null; + hoveredLine: [Point, Point] | null; + // Wall wallThickness: number; @@ -47,6 +49,8 @@ interface BuilderState { setSnappedPoint: (point: Point | null) => void; setSnappedPosition: (position: [number, number, number] | null) => void; + setHoveredLine: (line: [Point, Point] | null) => void; + setSelectedAisle: (aisle: Object3D | null) => void; setAisleType: (type: AisleTypes) => void; @@ -81,6 +85,8 @@ export const useBuilderStore = create()( snappedPoint: null, snappedPosition: null, + hoveredLine: null, + // Wall wallThickness: 0.5, @@ -128,6 +134,12 @@ export const useBuilderStore = create()( }); }, + setHoveredLine: (line: [Point, Point] | null) => { + set((state) => { + state.hoveredLine = line; + }) + }, + setSnappedPoint: (point: Point | null) => { set((state) => { state.snappedPoint = point; diff --git a/app/src/store/builder/useWallStore.tsx b/app/src/store/builder/useWallStore.tsx index 029ace6..4f57849 100644 --- a/app/src/store/builder/useWallStore.tsx +++ b/app/src/store/builder/useWallStore.tsx @@ -7,6 +7,7 @@ interface WallStore { addWall: (wall: Wall) => void; updateWall: (uuid: string, updated: Partial) => void; removeWall: (uuid: string) => void; + removeWallByPoints: (Points: [Point, Point]) => Wall | undefined; addDecal: (wallUuid: string, decal: Decal) => void; updateDecal: (decalUuid: string, decal: Decal) => void; removeDecal: (decalUuid: string) => void; @@ -48,6 +49,26 @@ export const useWallStore = create()( state.walls = state.walls.filter(w => w.wallUuid !== uuid); }), + removeWallByPoints: (points) => { + let removedWall: Wall | undefined; + const [pointA, pointB] = points; + + set((state) => { + state.walls = state.walls.filter(wall => { + const wallPoints = wall.points.map(p => p.pointUuid); + const hasBothPoints = wallPoints.includes(pointA.pointUuid) && wallPoints.includes(pointB.pointUuid); + + if (hasBothPoints) { + removedWall = JSON.parse(JSON.stringify(wall)); + return false; + } + return true; + }); + }); + + return removedWall; + }, + addDecal: (wallUuid, decal) => set((state) => { const wallToUpdate = state.walls.find(w => w.wallUuid === wallUuid); if (wallToUpdate) {