added functions to return the node/cable clicked on all the way up to the parent

This commit is contained in:
csawatzky 2026-04-23 15:56:59 -06:00
parent 0701be2539
commit f4d1167e4d
5 changed files with 113 additions and 165 deletions

View file

@ -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<MaterialParameters>;
materialOverride?: React.ReactNode;
side?: Side
depthWrite?: boolean
side?: Side;
depthWrite?: boolean;
onClick?: (event: ThreeEvent<MouseEvent>) => 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) {
@ -71,7 +75,7 @@ export default function BaseMesh(props: BaseMeshProps) {
return (
<group position={position} rotation={rotation}>
{/* Main surface */}
<mesh geometry={geometry}>
<mesh geometry={geometry} onClick={onClick}>
{buildMaterial()}
</mesh>

View file

@ -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 */}
<BinCables cableData={cableData} nodeData={nodeData} bin={bin} binCenter={binCenter}/>
<BinCables cableData={cableData} nodeData={nodeData} bin={bin} binCenter={binCenter}
onNodeClick={(node, cable) => {
// console.log(node)
// console.log(cable)
if(nodeClick){
nodeClick(node, cable)
}
}}/>
<NodePointCloud bin={bin} nodes={nodeData} />
</group>

View file

@ -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 (
<React.Fragment>
{cableData.map((cable, i) => {
const cableNodes = nodeData.filter(n => n.cableIndex === cable.cableIndex);
return (
<GrainCable key={"cable " + i} cable={cable} nodes={cableNodes} bin={bin} binCenter={binCenter}/>
<GrainCable
key={"cable " + i}
cable={cable}
nodes={cableNodes}
bin={bin}
binCenter={binCenter}
onNodeClick={(node) => {
if(onNodeClick){
onNodeClick(node, cable)
}
}}
/>
)}
)}

View file

@ -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,30 +16,23 @@ 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<Group>(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);
@ -55,97 +47,27 @@ 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()
});
@ -164,7 +86,7 @@ export default function CableNode(props: Props){
}
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.52 units
labelPosition.add(dir.multiplyScalar(1));
const getRowY = (index: number) => {
const totalHeight = rows.length * ROW_HEIGHT;
@ -194,34 +116,35 @@ 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 (
<React.Fragment>
{(!node.inGrain || !showLabel) &&
<React.Fragment>
<Sphere geometry={geometry} position={node.position} colour={nodeColour()}/>
<Sphere
geometry={geometry}
position={node.position}
colour={nodeColour()}
onClick={handleClick}
/>
{node.topNode && (
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation}/>
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation} />
)}
</React.Fragment>
}
{node.inGrain && showLabel &&
<Billboard position={labelPosition}>
<group ref={labelGroupRef} renderOrder={999}>
{/* Background */}
<group ref={labelGroupRef} renderOrder={999} onClick={handleClick}>
<mesh position={[0, 0, -1]}>
<planeGeometry args={[200, labelHeight]} />
<meshBasicMaterial
@ -232,7 +155,6 @@ export default function CableNode(props: Props){
depthTest={false}
/>
</mesh>
{/* Rows */}
{rows.map((row, i) => (
<Text
key={i}

View file

@ -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) {
<React.Fragment>
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5}/>
{nodes.map((node, i) => (
<CableNode key={"node " + i} node={node} binCenter={binCenter} lowerThreshold={bin.lowerTempThreshold()} upperThreshold={bin.upperTempThreshold()}/>
<CableNode onNodeClick={onNodeClick} key={"node " + i} node={node} binCenter={binCenter} lowerThreshold={bin.lowerTempThreshold()} upperThreshold={bin.upperTempThreshold()}/>
))}
</React.Fragment>
)