From b4d5087298b101cf1ae4a763ad2bb8a65a0dce9e Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Mon, 29 Dec 2025 16:45:52 +0530 Subject: [PATCH 1/2] feat: Add components for rendering interactive 3D bezier lines and instanced arrows to visualize trigger connections and a preview line. --- .../simulation/events/arrows/arrows.tsx | 393 ++++++++++++------ .../triggerConnections/triggerConnector.tsx | 67 +-- 2 files changed, 267 insertions(+), 193 deletions(-) diff --git a/app/src/modules/simulation/events/arrows/arrows.tsx b/app/src/modules/simulation/events/arrows/arrows.tsx index ab3784c..bcce2c7 100644 --- a/app/src/modules/simulation/events/arrows/arrows.tsx +++ b/app/src/modules/simulation/events/arrows/arrows.tsx @@ -1,6 +1,6 @@ import { useParams } from "react-router-dom"; import * as THREE from "three"; -import { useMemo, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useThree } from "@react-three/fiber"; import { useToolMode } from "../../../../store/builder/store"; import { useSceneContext } from "../../../scene/sceneContext"; @@ -8,6 +8,8 @@ import { useSocketStore } from "../../../../store/socket/useSocketStore"; import { updateEventToBackend } from "../../../../components/layout/sidebarRight/properties/eventProperties/functions/handleUpdateEventToBackend"; +import { QuadraticBezierLine } from "@react-three/drei"; + interface ConnectionLine { id: string; startPointUuid: string; @@ -15,9 +17,15 @@ interface ConnectionLine { trigger: TriggerSchema; } -export function Arrows({ connections }: { readonly connections: ConnectionLine[] }) { +interface PreviewLine { + start: THREE.Vector3; + mid: THREE.Vector3; + end: THREE.Vector3; + color: string; +} + +export function Arrows({ connections, previewLine }: { readonly connections: ConnectionLine[]; readonly previewLine?: PreviewLine | null }) { const [hoveredArrowTrigger, setHoveredArrowTrigger] = useState(null); - const groupRef = useRef(null); const { simulationSocket } = useSocketStore(); const { scene } = useThree(); const { toolMode } = useToolMode(); @@ -38,6 +46,15 @@ export function Arrows({ connections }: { readonly connections: ConnectionLine[] }); }; + const removeConnection = (trigger: TriggerSchema) => { + if (trigger.triggerUuid) { + const event = peekRemoveTrigger(selectedProduct.productUuid, trigger.triggerUuid); + if (event) { + updateBackend(event); + } + } + }; + const getWorldPositionFromScene = (uuid: string): THREE.Vector3 | null => { const obj = scene.getObjectByProperty("uuid", uuid); if (!obj) return null; @@ -46,137 +63,110 @@ export function Arrows({ connections }: { readonly connections: ConnectionLine[] return pos; }; - const processedArrows = new Set(); + const { arrowsData, linesData } = useMemo(() => { + const processedArrows = new Set(); + const arrowDataList: { + key: string; + trigger: TriggerSchema; + subCurve: THREE.Curve; + scale: number; + matrix: THREE.Matrix4; + }[] = []; - const createArrow = (key: string, fullCurve: THREE.QuadraticBezierCurve3, centerT: number, segmentSize: number, scale: number, reverse: boolean, trigger: TriggerSchema) => { - if (processedArrows.has(trigger.triggerUuid)) return []; - processedArrows.add(trigger.triggerUuid); + const lineDataList: { + key: string; + start: THREE.Vector3; + end: THREE.Vector3; + mid: THREE.Vector3; + trigger: TriggerSchema; + }[] = []; - const t1 = Math.max(0, centerT - segmentSize / 2); - const t2 = Math.min(1, centerT + segmentSize / 2); - const subCurve = getSubCurve(fullCurve, t1, t2, reverse); + const getSubCurve = (curve: THREE.Curve, t1: number, t2: number, reverse = false) => { + const divisions = 10; + const subPoints = Array.from({ length: divisions + 1 }, (_, i) => { + const t = THREE.MathUtils.lerp(t1, t2, i / divisions); + return curve.getPoint(t); + }); - const shaftGeometry = new THREE.TubeGeometry(subCurve, 8, 0.01 * scale, 8, false); + if (reverse) subPoints.reverse(); - const end = subCurve.getPoint(1); - const tangent = subCurve.getTangent(1).normalize(); - - const arrowHeadLength = 0.15 * scale; - const arrowRadius = 0.01 * scale; - const arrowHeadRadius = arrowRadius * 2.5; - - const headGeometry = new THREE.ConeGeometry(arrowHeadRadius, arrowHeadLength, 8); - headGeometry.translate(0, arrowHeadLength / 2, 0); - - const rotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), tangent); - - const removeConnection = (trigger: TriggerSchema) => { - if (trigger.triggerUuid) { - const event = peekRemoveTrigger(selectedProduct.productUuid, trigger.triggerUuid); - if (event) { - updateBackend(event); - } - } + return new THREE.CatmullRomCurve3(subPoints); }; - return ( - { - if (toolMode === "3D-Delete") { - setHoveredArrowTrigger(trigger.triggerUuid); - } - }} - onPointerOut={() => { - if (toolMode === "3D-Delete") { - setHoveredArrowTrigger(null); - } - }} - onClick={() => { - if (toolMode === "3D-Delete") { - removeConnection(trigger); - } - }} - > - - - - - - - - ); - }; + connections.forEach((connection) => { + const { startPointUuid, endPointUuid, trigger, id } = connection; - const getSubCurve = (curve: THREE.Curve, t1: number, t2: number, reverse = false) => { - const divisions = 10; - const subPoints = Array.from({ length: divisions + 1 }, (_, i) => { - const t = THREE.MathUtils.lerp(t1, t2, i / divisions); - return curve.getPoint(t); + if (processedArrows.has(trigger.triggerUuid)) return; + processedArrows.add(trigger.triggerUuid); + + const start = getWorldPositionFromScene(startPointUuid); + const end = getWorldPositionFromScene(endPointUuid); + if (!start || !end) return; + + const isBidirectional = connections.some((other) => other.startPointUuid === endPointUuid && other.endPointUuid === startPointUuid); + + const distance = start.distanceTo(end); + const heightFactor = Math.max(0.5, distance * 0.2); + const control = new THREE.Vector3((start.x + end.x) / 2, Math.max(start.y, end.y) + heightFactor, (start.z + end.z) / 2); + + lineDataList.push({ + key: id, + start: start.clone(), + end: end.clone(), + mid: control.clone(), + trigger, + }); + + const curve = new THREE.QuadraticBezierCurve3(start, control, end); + const scale = THREE.MathUtils.clamp(distance * 0.75, 0.5, 5); + + const addArrow = (suffix: string, centerT: number, segmentSize: number, reverse: boolean) => { + const t1 = Math.max(0, centerT - segmentSize / 2); + const t2 = Math.min(1, centerT + segmentSize / 2); + const subCurve = getSubCurve(curve, t1, t2, reverse); + + const endPoint = subCurve.getPoint(1); + const tangent = subCurve.getTangent(1).normalize(); + + const rotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), tangent); + + const matrix = new THREE.Matrix4(); + matrix.compose(endPoint, rotation, new THREE.Vector3(scale, scale, scale)); + + arrowDataList.push({ + key: id + suffix, + trigger, + subCurve, + scale, + matrix, + }); + }; + + if (isBidirectional) { + addArrow("", 0.66, 0.25, false); + } else { + addArrow("", 0.5, 0.3, false); + } }); - if (reverse) subPoints.reverse(); + return { arrowsData: arrowDataList, linesData: lineDataList }; + }, [connections, scene]); - return new THREE.CatmullRomCurve3(subPoints); - }; + // Preview Line Logic + const previewData = useMemo(() => { + if (!previewLine) return null; - const arrowGroups = connections.flatMap((connection) => { - const { startPointUuid, endPointUuid } = connection; - const start = getWorldPositionFromScene(startPointUuid); - const end = getWorldPositionFromScene(endPointUuid); - if (!start || !end) return []; + const { start, mid, end, color } = previewLine; + const startVec = start; + const midVec = mid; + const endVec = end; - const isBidirectional = connections.some((other) => other.startPointUuid === endPointUuid && other.endPointUuid === startPointUuid); - - const distance = start.distanceTo(end); - const heightFactor = Math.max(0.5, distance * 0.2); - const control = new THREE.Vector3((start.x + end.x) / 2, Math.max(start.y, end.y) + heightFactor, (start.z + end.z) / 2); - const curve = new THREE.QuadraticBezierCurve3(start, control, end); + const fullCurve = new THREE.QuadraticBezierCurve3(startVec, midVec, endVec); + const distance = startVec.distanceTo(endVec); const scale = THREE.MathUtils.clamp(distance * 0.75, 0.5, 5); - if (isBidirectional) { - return [createArrow(connection.id + "-fwd", curve, 0.33, 0.25, scale, true, connection.trigger), createArrow(connection.id + "-bwd", curve, 0.66, 0.25, scale, false, connection.trigger)]; - } else { - return [createArrow(connection.id, curve, 0.5, 0.3, scale, false, connection.trigger)]; - } - }); - - return ( - - {arrowGroups} - - ); -} - -export function ArrowOnQuadraticBezier({ - start, - mid, - end, - color = "#42a5f5", -}: Readonly<{ - start: number[]; - mid: number[]; - end: number[]; - color?: string; -}>) { - const minScale = 0.5; - const maxScale = 5; - const segmentSize = 0.3; - - const startVec = useMemo(() => new THREE.Vector3(...start), [start]); - const midVec = useMemo(() => new THREE.Vector3(...mid), [mid]); - const endVec = useMemo(() => new THREE.Vector3(...end), [end]); - - const fullCurve = useMemo(() => new THREE.QuadraticBezierCurve3(startVec, midVec, endVec), [startVec, midVec, endVec]); - - const distance = useMemo(() => startVec.distanceTo(endVec), [startVec, endVec]); - const scale = useMemo(() => THREE.MathUtils.clamp(distance * 0.75, minScale, maxScale), [distance]); - - const arrowHeadLength = 0.15 * scale; - const arrowRadius = 0.01 * scale; - const arrowHeadRadius = arrowRadius * 2.5; - - const subCurve = useMemo(() => { + // Arrow part + const segmentSize = 0.3; const centerT = 0.5; const t1 = Math.max(0, centerT - segmentSize / 2); const t2 = Math.min(1, centerT + segmentSize / 2); @@ -186,32 +176,159 @@ export function ArrowOnQuadraticBezier({ const t = THREE.MathUtils.lerp(t1, t2, i / divisions); return fullCurve.getPoint(t); }); - return new THREE.CatmullRomCurve3(subPoints); - }, [fullCurve, segmentSize]); + const subCurve = new THREE.CatmullRomCurve3(subPoints); - const tubeGeometry = useMemo(() => new THREE.TubeGeometry(subCurve, 20, arrowRadius, 8, false), [subCurve, arrowRadius]); + const endPoint = subCurve.getPoint(1); + const tangent = subCurve.getTangent(1).normalize(); - const arrowPosition = useMemo(() => subCurve.getPoint(1), [subCurve]); - const arrowTangent = useMemo(() => subCurve.getTangent(1).normalize(), [subCurve]); + const rotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), tangent); + const matrix = new THREE.Matrix4(); + matrix.compose(endPoint, rotation, new THREE.Vector3(scale, scale, scale)); - const arrowRotation = useMemo(() => { - return new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), arrowTangent); - }, [arrowTangent]); + return { + subCurve, + scale, + matrix, + startVec, + midVec, + endVec, + color, + }; + }, [previewLine]); + + const instancedMeshRef = useRef(null); + const tempColor = useMemo(() => new THREE.Color(), []); const coneGeometry = useMemo(() => { - const geom = new THREE.ConeGeometry(arrowHeadRadius, arrowHeadLength, 8); - geom.translate(0, arrowHeadLength / 2, 0); + const geom = new THREE.ConeGeometry(0.025, 0.15, 8); + geom.translate(0, 0.075, 0); return geom; - }, [arrowHeadRadius, arrowHeadLength]); + }, []); + + useLayoutEffect(() => { + if (instancedMeshRef.current) { + let index = 0; + // Main arrows + arrowsData.forEach((data) => { + instancedMeshRef.current!.setMatrixAt(index++, data.matrix); + }); + // Preview arrow + if (previewData) { + instancedMeshRef.current!.setMatrixAt(index, previewData.matrix); + } + instancedMeshRef.current.instanceMatrix.needsUpdate = true; + } + }, [arrowsData, previewData]); + + useEffect(() => { + if (instancedMeshRef.current) { + let index = 0; + // Main arrows + arrowsData.forEach((data) => { + const isHovered = toolMode === "3D-Delete" && hoveredArrowTrigger === data.trigger.triggerUuid; + instancedMeshRef.current!.setColorAt(index++, tempColor.set(isHovered ? "red" : "#42a5f5")); + }); + // Preview arrow + if (previewData) { + instancedMeshRef.current!.setColorAt(index, tempColor.set(previewData.color)); + } + if (instancedMeshRef.current.instanceColor) instancedMeshRef.current.instanceColor.needsUpdate = true; + } + }, [arrowsData, previewData, hoveredArrowTrigger, toolMode, tempColor]); + + const totalInstances = arrowsData.length + (previewData ? 1 : 0); return ( - - - - - - - + + {/* Connection Lines */} + {linesData.map((line) => ( + { + e.stopPropagation(); + if (toolMode === "3D-Delete") setHoveredArrowTrigger(line.trigger.triggerUuid); + }} + onPointerOut={(e) => { + e.stopPropagation(); + if (toolMode === "3D-Delete") setHoveredArrowTrigger(null); + }} + onClick={(e) => { + e.stopPropagation(); + if (toolMode === "3D-Delete") removeConnection(line.trigger); + }} + /> + ))} + + {/* Preview Line */} + {previewData && ( + + )} + + {/* Shafts (Main) */} + {arrowsData.map((data) => ( + { + e.stopPropagation(); + if (toolMode === "3D-Delete") setHoveredArrowTrigger(data.trigger.triggerUuid); + }} + onPointerOut={(e) => { + e.stopPropagation(); + if (toolMode === "3D-Delete") setHoveredArrowTrigger(null); + }} + onClick={(e) => { + e.stopPropagation(); + if (toolMode === "3D-Delete") removeConnection(data.trigger); + }} + > + + + + ))} + + {/* Shaft (Preview) */} + {previewData && ( + + + + + )} + + {/* Heads (Instanced) */} + {totalInstances > 0 && ( + { + e.stopPropagation(); + const id = e.instanceId; + if (id !== undefined && id < arrowsData.length && toolMode === "3D-Delete") { + setHoveredArrowTrigger(arrowsData[id].trigger.triggerUuid); + } + }} + onPointerOut={(e) => { + e.stopPropagation(); + if (toolMode === "3D-Delete") setHoveredArrowTrigger(null); + }} + onClick={(e) => { + e.stopPropagation(); + const id = e.instanceId; + if (id !== undefined && id < arrowsData.length && toolMode === "3D-Delete") { + removeConnection(arrowsData[id].trigger); + } + }} + > + + + )} ); } diff --git a/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx b/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx index 425c3ef..c4cb6e9 100644 --- a/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx +++ b/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx @@ -1,14 +1,14 @@ import { useParams } from "react-router-dom"; import * as THREE from "three"; import { useEffect, useRef, useState } from "react"; -import { QuadraticBezierLine } from "@react-three/drei"; + import { useFrame, useThree } from "@react-three/fiber"; import { useSubModuleStore } from "../../../../store/ui/useModuleStore"; import { useSelectedAction, useSelectedAsset, useSelectedEventData } from "../../../../store/simulation/useSimulationStore"; import { handleAddEventToProduct } from "../../functions/handleAddEventToProduct"; import { usePlayButtonStore } from "../../../../store/ui/usePlayButtonStore"; import { useSocketStore } from "../../../../store/socket/useSocketStore"; -import { ArrowOnQuadraticBezier, Arrows } from "../arrows/arrows"; +import { Arrows } from "../arrows/arrows"; import { useToolMode } from "../../../../store/builder/store"; import { useSceneContext } from "../../../scene/sceneContext"; @@ -483,61 +483,18 @@ function TriggerConnector() { } }; + const previewLine = currentLine + ? { + start: currentLine.start, + mid: currentLine.mid, + end: currentLine.end, + color: helperLineColor, + } + : null; + return ( - {connections.map((connection) => { - const startPoint = getWorldPositionFromScene(connection.startPointUuid); - const endPoint = getWorldPositionFromScene(connection.endPointUuid); - - if (!startPoint || !endPoint) return null; - - const distance = startPoint.distanceTo(endPoint); - const heightFactor = Math.max(0.5, distance * 0.2); - const midPoint = new THREE.Vector3((startPoint.x + endPoint.x) / 2, Math.max(startPoint.y, endPoint.y) + heightFactor, (startPoint.z + endPoint.z) / 2); - - return ( - (groupRefs.current[connection.id] = el!)} - start={startPoint.toArray()} - end={endPoint.toArray()} - mid={midPoint.toArray()} - color={toolMode === "3D-Delete" && hoveredLineKey === connection.id ? "red" : "#42a5f5"} - lineWidth={4} - dashed={toolMode === "3D-Delete" && hoveredLineKey === connection.id ? false : true} - dashSize={0.75} - dashScale={20} - // onPointerOver={() => setHoveredLineKey(connection.id)} - // onPointerOut={() => setHoveredLineKey(null)} - // onClick={() => { - // if (toolMode === '3D-Delete') { - // setHoveredLineKey(null); - // setCurrentLine(null); - // removeConnection(connection); - // } - // }} - userData={connection.trigger} - /> - ); - })} - - - - {currentLine && ( - <> - - - - )} + ); } From f71068151dc9d0e3481cf97c4a3bf4495fc49830 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Mon, 29 Dec 2025 17:15:58 +0530 Subject: [PATCH 2/2] feat: Add 3D wall rendering component with textures and decals, and a trigger connector for managing simulation events. --- app/src/modules/builder/wall/Instances/instance/wall.tsx | 4 ++-- .../events/triggerConnections/triggerConnector.tsx | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/app/src/modules/builder/wall/Instances/instance/wall.tsx b/app/src/modules/builder/wall/Instances/instance/wall.tsx index 55b6f38..36b6ede 100644 --- a/app/src/modules/builder/wall/Instances/instance/wall.tsx +++ b/app/src/modules/builder/wall/Instances/instance/wall.tsx @@ -94,9 +94,9 @@ function Wall({ wall }: { readonly wall: Wall }) { if (!wallVisibility && wallType.type === "room") { meshRef.current.getWorldDirection(v); - camera.getWorldDirection(u); + u.subVectors(camera.position, meshRef.current.position); if (!u || !v) return; - nextVisible = 2 * v.dot(u) <= 0.1; + nextVisible = v.dot(u) >= 0; } if (prevVisibleRef.current !== nextVisible) { diff --git a/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx b/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx index c4cb6e9..0bd6fdc 100644 --- a/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx +++ b/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx @@ -483,14 +483,7 @@ function TriggerConnector() { } }; - const previewLine = currentLine - ? { - start: currentLine.start, - mid: currentLine.mid, - end: currentLine.end, - color: helperLineColor, - } - : null; + const previewLine = currentLine ? { start: currentLine.start, mid: currentLine.mid, end: currentLine.end, color: helperLineColor } : null; return (