Enhance builder functionality by implementing drag-and-drop for lines and points, adding hover effects, and improving wall distance display in the UI.

This commit is contained in:
2025-06-23 13:38:26 +05:30
parent 329acbe397
commit f593fcd578
8 changed files with 237 additions and 53 deletions

View File

@@ -48,14 +48,11 @@ function AisleInstances() {
{toggleView &&
<Html
// data
key={`${aisle.points[0].pointUuid}_${aisle.points[1].pointUuid}`}
userData={aisle}
position={[textPosition.x, 1, textPosition.z]}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
sprite
@@ -68,7 +65,6 @@ function AisleInstances() {
</div>
</Html>
}
</React.Fragment>
)
})}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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<LineProps>) {
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<THREE.Vector3 | null>(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<LineProps>) {
}
}
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 (
<Tube
name={`${points[0].pointType}-Line`}
key={`${points[0].pointUuid}-${points[1].pointUuid}`}
uuid={`${points[0].pointUuid}-${points[1].pointUuid}`}
userData={{ points, path }}
args={[path, Constants.lineConfig.tubularSegments, Constants.lineConfig.radius, Constants.lineConfig.radialSegments, false]}
<DragControls
axisLock="y"
autoTransform={false}
onDragStart={() => handleDragStart(points)}
onDrag={() => handleDrag(points)}
onDragEnd={() => handleDragEnd(points)}
>
<meshStandardMaterial color={colors.defaultLineColor} />
</Tube>
<Tube
name={`${points[0].pointType}-Line`}
key={`${points[0].pointUuid}-${points[1].pointUuid}`}
uuid={`${points[0].pointUuid}-${points[1].pointUuid}`}
userData={{ points, path }}
args={[path, Constants.lineConfig.tubularSegments, Constants.lineConfig.radius, Constants.lineConfig.radialSegments, false]}
onClick={() => {
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)
}}
>
<meshStandardMaterial color={isDeletable ? colors.defaultDeleteColor : colors.defaultLineColor} />
</Tube>
</DragControls >
);
}

View File

@@ -15,9 +15,10 @@ import { useSceneContext } from '../../scene/sceneContext';
function Point({ point }: { readonly point: Point }) {
const materialRef = useRef<THREE.ShaderMaterial>(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<THREE.Vector3 | null>(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 }) {
<DragControls
axisLock='y'
autoTransform={false}
onDrag={() => { handleDrag(point) }}
onDragEnd={() => { handleDragEnd(point) }}
onDragStart={() => handleDragStart(point)}
onDrag={() => handleDrag(point)}
onDragEnd={() => handleDragEnd(point)}
>
<mesh
key={point.pointUuid}
@@ -179,12 +194,16 @@ function Point({ point }: { readonly point: Point }) {
onPointerOver={() => {
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)
}}

View File

@@ -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 && (
<mesh name='Walls-Group'>
{/* <Base name="base" geometry={box} scale={[3, 3, 3]} /> */}
<Geometry useGroups>
{walls.map((wall) => (
<WallInstance key={wall.wallUuid} wall={wall} />
))}
</Geometry>
{/* <Subtraction rotation={[0, Math.PI / 2, 0]} position={[-1.425, -0.45, 0]} scale={[1, 3, 1]}>
<Geometry>
<Base geometry={box} />
</Geometry>
</Subtraction> */}
</mesh>
)}
@@ -61,11 +54,42 @@ function WallInstances() {
</group>
<group name='Wall-Lines-Group'>
{walls.map((wall) => (
<React.Fragment key={wall.wallUuid}>
<Line points={wall.points} />
</React.Fragment>
))}
{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 &&
<Html
key={`${wall.points[0].pointUuid}_${wall.points[1].pointUuid}`}
userData={wall}
position={[textPosition.x, 1, textPosition.z]}
wrapperClass="distance-text-wrapper"
className="distance-text"
zIndexRange={[1, 0]}
prepend
sprite
>
<div
key={wall.wallUuid}
className={`distance ${wall.wallUuid}`}
>
{distance.toFixed(2)} m
</div>
</Html>
}
</React.Fragment>
)
})}
</group>
</>
)}

View File

@@ -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<BuilderState>()(
snappedPoint: null,
snappedPosition: null,
hoveredLine: null,
// Wall
wallThickness: 0.5,
@@ -128,6 +134,12 @@ export const useBuilderStore = create<BuilderState>()(
});
},
setHoveredLine: (line: [Point, Point] | null) => {
set((state) => {
state.hoveredLine = line;
})
},
setSnappedPoint: (point: Point | null) => {
set((state) => {
state.snappedPoint = point;

View File

@@ -7,6 +7,7 @@ interface WallStore {
addWall: (wall: Wall) => void;
updateWall: (uuid: string, updated: Partial<Wall>) => 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<WallStore>()(
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) {