added functions to return the node/cable clicked on all the way up to the parent
This commit is contained in:
parent
0701be2539
commit
f4d1167e4d
5 changed files with 113 additions and 165 deletions
|
|
@ -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) {
|
||||
|
|
@ -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 (
|
||||
<group position={position} rotation={rotation}>
|
||||
{/* Main surface */}
|
||||
<mesh geometry={geometry}>
|
||||
<mesh geometry={geometry} onClick={onClick}>
|
||||
{buildMaterial()}
|
||||
</mesh>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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);
|
||||
} 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 (
|
||||
<React.Fragment>
|
||||
{(!node.inGrain || !showLabel) &&
|
||||
<React.Fragment>
|
||||
<Sphere geometry={geometry} position={node.position} colour={nodeColour()}/>
|
||||
{node.topNode && (
|
||||
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation}/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<Sphere
|
||||
geometry={geometry}
|
||||
position={node.position}
|
||||
colour={nodeColour()}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
{node.topNode && (
|
||||
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
{node.inGrain && showLabel &&
|
||||
<Billboard position={labelPosition}>
|
||||
<group ref={labelGroupRef} renderOrder={999}>
|
||||
{/* Background */}
|
||||
<mesh position={[0, 0, -1]}>
|
||||
<planeGeometry args={[200, labelHeight]} />
|
||||
<meshBasicMaterial
|
||||
color="black"
|
||||
transparent
|
||||
opacity={0.9}
|
||||
depthWrite={false}
|
||||
depthTest={false}
|
||||
/>
|
||||
</mesh>
|
||||
{/* Rows */}
|
||||
{rows.map((row, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
position={[0, getRowY(i), 0]}
|
||||
fontSize={30}
|
||||
color={row.color}
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
>
|
||||
{row.text}
|
||||
</Text>
|
||||
))}
|
||||
</group>
|
||||
</Billboard>
|
||||
<Billboard position={labelPosition}>
|
||||
<group ref={labelGroupRef} renderOrder={999} onClick={handleClick}>
|
||||
<mesh position={[0, 0, -1]}>
|
||||
<planeGeometry args={[200, labelHeight]} />
|
||||
<meshBasicMaterial
|
||||
color="black"
|
||||
transparent
|
||||
opacity={0.9}
|
||||
depthWrite={false}
|
||||
depthTest={false}
|
||||
/>
|
||||
</mesh>
|
||||
{rows.map((row, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
position={[0, getRowY(i), 0]}
|
||||
fontSize={30}
|
||||
color={row.color}
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
>
|
||||
{row.text}
|
||||
</Text>
|
||||
))}
|
||||
</group>
|
||||
</Billboard>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue