From f4d1167e4d3b5760d1bf680bbcd1d9e0cf555871 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 23 Apr 2026 15:56:59 -0600 Subject: [PATCH] added functions to return the node/cable clicked on all the way up to the parent --- src/3dModels/Shapes/BaseMesh.tsx | 14 +- src/bin/3dView/Scene/Bin3dView.tsx | 16 +- src/bin/3dView/Systems/Cables/BinCables.tsx | 17 +- src/bin/3dView/Systems/Cables/CableNode.tsx | 226 ++++++------------- src/bin/3dView/Systems/Cables/GrainCable.tsx | 5 +- 5 files changed, 113 insertions(+), 165 deletions(-) diff --git a/src/3dModels/Shapes/BaseMesh.tsx b/src/3dModels/Shapes/BaseMesh.tsx index 00e7ad2..d92601d 100644 --- a/src/3dModels/Shapes/BaseMesh.tsx +++ b/src/3dModels/Shapes/BaseMesh.tsx @@ -1,4 +1,5 @@ import { Euler, Vector3, BufferGeometry, MaterialParameters, Side } from "three"; +import { ThreeEvent } from "@react-three/fiber"; export interface BaseMeshProps { geometry: BufferGeometry; @@ -13,8 +14,9 @@ export interface BaseMeshProps { materialType?: "standard" | "basic"; materialProps?: Partial; materialOverride?: React.ReactNode; - side?: Side - depthWrite?: boolean + side?: Side; + depthWrite?: boolean; + onClick?: (event: ThreeEvent) => void; } export default function BaseMesh(props: BaseMeshProps) { @@ -32,10 +34,12 @@ export default function BaseMesh(props: BaseMeshProps) { materialProps, materialOverride, side, - depthWrite = true + depthWrite = true, + onClick, } = props; const isTransparent = opacity < 1; + const buildMaterial = () => { if (materialOverride) return materialOverride; switch (materialType) { @@ -50,7 +54,7 @@ export default function BaseMesh(props: BaseMeshProps) { {...materialProps} /> ); - + case "standard": default: return ( @@ -71,7 +75,7 @@ export default function BaseMesh(props: BaseMeshProps) { return ( {/* Main surface */} - + {buildMaterial()} diff --git a/src/bin/3dView/Scene/Bin3dView.tsx b/src/bin/3dView/Scene/Bin3dView.tsx index 45fcdb6..403dc8f 100644 --- a/src/bin/3dView/Scene/Bin3dView.tsx +++ b/src/bin/3dView/Scene/Bin3dView.tsx @@ -6,8 +6,8 @@ import GrainFillFlat from "../Systems/Inventory/GrainFillFlat"; import BinCables from "../Systems/Cables/BinCables"; import { Vector3 } from "three"; import { useMemo } from "react"; -import { BuildCableData } from "../Data/BuildCableData"; -import { BuildNodeData } from "../Data/BuildNodeData"; +import { BuildCableData, CableData } from "../Data/BuildCableData"; +import { BuildNodeData, NodeData } from "../Data/BuildNodeData"; import NodePointCloud from "../Systems/Heatmap/NodePointCloud"; import { pond } from "protobuf-ts/pond"; import GrainCableFill from "../Systems/Inventory/GrainCableFill"; @@ -27,13 +27,14 @@ interface Props { * optional: the percent of the bin to fill using a level top, this will be used with manual and lidar controls */ fillPercent?: number + nodeClick?: (node: NodeData, cable: CableData) => void } export default function Bin3dView(props: Props){ //this function will generate a 3D model of a bin based on its settings using multiple meshes, cylinder for the body, cone for the roof // and either a cone for the hopper or circle for flat bottom, it is possible to also use lathe geometry for this as well, // it might even work better because we can control the smoothness easier with it being only one mesh rather than 3 - const {bin, scale = 100, fillPercent} = props + const {bin, scale = 100, fillPercent, nodeClick} = props const binCenter = useMemo(() => new Vector3(0, 0, 0), []); const cableData = useMemo(() => BuildCableData(bin), [bin]); const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]); @@ -80,7 +81,14 @@ export default function Bin3dView(props: Props){ {/* grain - cylinder*/} {grainInventory()} {/* cables */} - + { + // console.log(node) + // console.log(cable) + if(nodeClick){ + nodeClick(node, cable) + } + }}/> diff --git a/src/bin/3dView/Systems/Cables/BinCables.tsx b/src/bin/3dView/Systems/Cables/BinCables.tsx index 452a471..1c526f3 100644 --- a/src/bin/3dView/Systems/Cables/BinCables.tsx +++ b/src/bin/3dView/Systems/Cables/BinCables.tsx @@ -10,16 +10,29 @@ interface Props { nodeData: NodeData[] bin: Bin binCenter: Vector3 + onNodeClick?: (node: NodeData, cable: CableData) => void } export default function BinCables(props: Props){ - const {bin, binCenter, cableData, nodeData} = props + const {bin, binCenter, cableData, nodeData, onNodeClick} = props return ( {cableData.map((cable, i) => { const cableNodes = nodeData.filter(n => n.cableIndex === cable.cableIndex); + return ( - + { + if(onNodeClick){ + onNodeClick(node, cable) + } + }} + /> )} )} diff --git a/src/bin/3dView/Systems/Cables/CableNode.tsx b/src/bin/3dView/Systems/Cables/CableNode.tsx index a5c410f..daf7744 100644 --- a/src/bin/3dView/Systems/Cables/CableNode.tsx +++ b/src/bin/3dView/Systems/Cables/CableNode.tsx @@ -8,7 +8,6 @@ import { quack } from "protobuf-ts/quack" import { useGlobalState } from "providers" import { pond } from "protobuf-ts/pond" import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring" -// import { Color } from "three"; import { NodeData } from "../../Data/BuildNodeData" import { TempToColour } from "bin/3dView/utils/tempToColour" @@ -17,31 +16,24 @@ interface Props { binCenter: Vector3 lowerThreshold: number upperThreshold: number + onNodeClick?: (node: NodeData) => void } -export default function CableNode(props: Props){ - const {node, binCenter, lowerThreshold, upperThreshold} = props +export default function CableNode(props: Props) { + const { node, binCenter, lowerThreshold, upperThreshold, onNodeClick } = props const { camera } = useThree(); - const [{user}] = useGlobalState(); + const [{ user }] = useGlobalState(); const [showLabel, setShowLabel] = useState(false); const labelGroupRef = React.useRef(null); const ROW_HEIGHT = 35; const PADDING = 20; - //node colour varables for adjusting lighness and saturation for the thresholds - // const colourFade = 20 //this number is the temp diff above or below the thresholds that the node will reach its maximum red or blue intensity - // const minimumLightness = 0.3 //the minimum brightness of the colour, to low would approach black - // const lightnessRange = 0.2 //how much can the node brighten, to high will allow the node to turn white - // const minimumSaturation = 0.7 - // const saturationRange = 0.8 - useFrame(() => { - //use the cameras distance to the center of the bin const distance = camera.position.distanceTo(binCenter) - + const SHOW_DISTANCE = 15; - const HIDE_DISTANCE = 16; // hysteresis buffer - + const HIDE_DISTANCE = 16; + if (!showLabel && distance < SHOW_DISTANCE) { setShowLabel(true); } else if (showLabel && distance > HIDE_DISTANCE) { @@ -55,116 +47,46 @@ export default function CableNode(props: Props){ } }); - const tempDisplay = () => { - let temp = node.celcius.toFixed(2) + "°C" + const tempDisplay = useMemo(() => { if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { - temp = ((node.celcius * 9/5) + 32).toFixed(2) + "°F" + return ((node.celcius * 9 / 5) + 32).toFixed(2) + "°F" } - return temp - } - - // const nodeColour = () => { - // if (!node.inGrain) return "white"; - // if (node.excluded) return "grey"; - - // const lower = lowerThreshold; - // const upper = upperThreshold; - // const temp = node.celcius; - - // const GREEN = new Color("#52c41a"); - // const BLUE = new Color("#3399ff"); - // const RED = new Color("#ff4d4f"); - - // if (temp >= lower && temp <= upper) { - // return GREEN.getStyle(); - // } - - // // shared helper logic concept: - // const getCloseness = (distance: number) => { - // const normalized = Math.min(1, distance / colourFade); - // return 1 - normalized; // 1 = near threshold, 0 = far - // }; - - // const color = new Color(); - - // // BELOW lower (blue) - // if (temp < lower) { - // const distance = lower - temp; - // const closeness = getCloseness(distance); - - // const hsl = { h: 0, s: 1, l: 1 }; - // BLUE.getHSL(hsl); - - // const lightness = (lightnessRange * closeness) + minimumLightness; - - // // 🔥 NEW: saturation as second intensity layer - // const saturation = (saturationRange * (1 - closeness)) + minimumSaturation; - - // color.setHSL(hsl.h, saturation, lightness); - - // return color.getStyle(); - // } - - // // ABOVE upper (red) - // if (temp > upper) { - // const distance = temp - upper; - // const closeness = getCloseness(distance); - - // const hsl = { h: 0, s: 1, l: 1 }; - // RED.getHSL(hsl); - - // const lightness = (lightnessRange * closeness) + minimumLightness; - - // // 🔥 NEW: saturation as second intensity layer - // const saturation = (saturationRange * (1 - closeness)) + minimumSaturation; - - // color.setHSL(hsl.h, saturation, lightness); - - // return color.getStyle(); - // } - - // return GREEN.getStyle(); - // }; + return node.celcius.toFixed(2) + "°C" + }, [node.celcius, user]); const rows = useMemo(() => { const r: { text: string; color: string }[] = []; - + if (node.excluded) { - r.push({ - text: "Excluded", - color: "red" - }); - return r; // nothing else matters + r.push({ text: "Excluded", color: "red" }); + return r; } - + if (node.topNode) { - r.push({ - text: "Top Node", - color: "yellow" - }); + r.push({ text: "Top Node", color: "yellow" }); } - + r.push({ - text: tempDisplay(), + text: tempDisplay, color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour() }); - + if (node.humidity) { r.push({ text: `${node.humidity.toFixed(2)}%`, color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour() }); } - + if (node.moisture) { r.push({ text: `${node.moisture.toFixed(2)}% EMC`, color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour() }); } - + return r; - }, [node, user]); + }, [node, tempDisplay]); const geometry = React.useMemo(() => ({ radius: node.radius, @@ -173,20 +95,20 @@ export default function CableNode(props: Props){ const topNodeGeo = React.useMemo(() => ({ radius: node.radius, thickness: 5, - } as RingGeometry), [node]) + } as RingGeometry), [node]); - const topNodePosition = React.useMemo(() => (new Vector3(node.position.x, node.position.y + 25, node.position.z)), [node]) + const topNodePosition = React.useMemo( + () => new Vector3(node.position.x, node.position.y + 25, node.position.z), + [node] + ); const topNodeRotation = React.useMemo(() => new Euler(-Math.PI / 2, 0, 0), []); - const labelPosition = node.position.clone(); const labelHeight = rows.length * ROW_HEIGHT + PADDING; const dir = camera.position.clone().sub(node.position).normalize(); - - // push label slightly toward camera - labelPosition.add(dir.multiplyScalar(1)); // small offset like 0.5–2 units + labelPosition.add(dir.multiplyScalar(1)); const getRowY = (index: number) => { const totalHeight = rows.length * ROW_HEIGHT; @@ -194,59 +116,59 @@ export default function CableNode(props: Props){ }; const nodeColour = () => { - if (node.excluded){ - return "grey" - } - if (!node.inGrain){ - return "white" - } - let c = TempToColour(node, lowerThreshold, upperThreshold, "node") - if (c !== null){ - return c.getStyle() - } - return "white" - } + if (node.excluded) return "grey"; + if (!node.inGrain) return "white"; + const c = TempToColour(node, lowerThreshold, upperThreshold, "node"); + return c !== null ? c.getStyle() : "white"; + }; + const handleClick = (e: any) => { + e.stopPropagation(); + onNodeClick?.(node); + }; return ( {(!node.inGrain || !showLabel) && - - - {node.topNode && ( - - )} - + + + {node.topNode && ( + + )} + } {node.inGrain && showLabel && - - - {/* Background */} - - - - - {/* Rows */} - {rows.map((row, i) => ( - - {row.text} - - ))} - - + + + + + + + {rows.map((row, i) => ( + + {row.text} + + ))} + + } ) diff --git a/src/bin/3dView/Systems/Cables/GrainCable.tsx b/src/bin/3dView/Systems/Cables/GrainCable.tsx index 44c83ff..f8ab324 100644 --- a/src/bin/3dView/Systems/Cables/GrainCable.tsx +++ b/src/bin/3dView/Systems/Cables/GrainCable.tsx @@ -11,9 +11,10 @@ interface Props { nodes: NodeData[] bin: Bin binCenter: Vector3 + onNodeClick?: (node: NodeData) => void } export default function GrainCable(props: Props) { - const {cable, nodes, bin, binCenter} = props + const {cable, nodes, bin, binCenter, onNodeClick} = props const calculateHeight = () => { return cable.topPosition.distanceTo(cable.bottomPosition) @@ -40,7 +41,7 @@ export default function GrainCable(props: Props) { {nodes.map((node, i) => ( - + ))} )