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 { Euler, Vector3, BufferGeometry, MaterialParameters, Side } from "three";
|
||||||
|
import { ThreeEvent } from "@react-three/fiber";
|
||||||
|
|
||||||
export interface BaseMeshProps {
|
export interface BaseMeshProps {
|
||||||
geometry: BufferGeometry;
|
geometry: BufferGeometry;
|
||||||
|
|
@ -13,8 +14,9 @@ export interface BaseMeshProps {
|
||||||
materialType?: "standard" | "basic";
|
materialType?: "standard" | "basic";
|
||||||
materialProps?: Partial<MaterialParameters>;
|
materialProps?: Partial<MaterialParameters>;
|
||||||
materialOverride?: React.ReactNode;
|
materialOverride?: React.ReactNode;
|
||||||
side?: Side
|
side?: Side;
|
||||||
depthWrite?: boolean
|
depthWrite?: boolean;
|
||||||
|
onClick?: (event: ThreeEvent<MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BaseMesh(props: BaseMeshProps) {
|
export default function BaseMesh(props: BaseMeshProps) {
|
||||||
|
|
@ -32,10 +34,12 @@ export default function BaseMesh(props: BaseMeshProps) {
|
||||||
materialProps,
|
materialProps,
|
||||||
materialOverride,
|
materialOverride,
|
||||||
side,
|
side,
|
||||||
depthWrite = true
|
depthWrite = true,
|
||||||
|
onClick,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isTransparent = opacity < 1;
|
const isTransparent = opacity < 1;
|
||||||
|
|
||||||
const buildMaterial = () => {
|
const buildMaterial = () => {
|
||||||
if (materialOverride) return materialOverride;
|
if (materialOverride) return materialOverride;
|
||||||
switch (materialType) {
|
switch (materialType) {
|
||||||
|
|
@ -71,7 +75,7 @@ export default function BaseMesh(props: BaseMeshProps) {
|
||||||
return (
|
return (
|
||||||
<group position={position} rotation={rotation}>
|
<group position={position} rotation={rotation}>
|
||||||
{/* Main surface */}
|
{/* Main surface */}
|
||||||
<mesh geometry={geometry}>
|
<mesh geometry={geometry} onClick={onClick}>
|
||||||
{buildMaterial()}
|
{buildMaterial()}
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import GrainFillFlat from "../Systems/Inventory/GrainFillFlat";
|
||||||
import BinCables from "../Systems/Cables/BinCables";
|
import BinCables from "../Systems/Cables/BinCables";
|
||||||
import { Vector3 } from "three";
|
import { Vector3 } from "three";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { BuildCableData } from "../Data/BuildCableData";
|
import { BuildCableData, CableData } from "../Data/BuildCableData";
|
||||||
import { BuildNodeData } from "../Data/BuildNodeData";
|
import { BuildNodeData, NodeData } from "../Data/BuildNodeData";
|
||||||
import NodePointCloud from "../Systems/Heatmap/NodePointCloud";
|
import NodePointCloud from "../Systems/Heatmap/NodePointCloud";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import GrainCableFill from "../Systems/Inventory/GrainCableFill";
|
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
|
* optional: the percent of the bin to fill using a level top, this will be used with manual and lidar controls
|
||||||
*/
|
*/
|
||||||
fillPercent?: number
|
fillPercent?: number
|
||||||
|
nodeClick?: (node: NodeData, cable: CableData) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Bin3dView(props: Props){
|
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
|
//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,
|
// 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
|
// 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 binCenter = useMemo(() => new Vector3(0, 0, 0), []);
|
||||||
const cableData = useMemo(() => BuildCableData(bin), [bin]);
|
const cableData = useMemo(() => BuildCableData(bin), [bin]);
|
||||||
const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]);
|
const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]);
|
||||||
|
|
@ -80,7 +81,14 @@ export default function Bin3dView(props: Props){
|
||||||
{/* grain - cylinder*/}
|
{/* grain - cylinder*/}
|
||||||
{grainInventory()}
|
{grainInventory()}
|
||||||
{/* cables */}
|
{/* 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} />
|
<NodePointCloud bin={bin} nodes={nodeData} />
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,29 @@ interface Props {
|
||||||
nodeData: NodeData[]
|
nodeData: NodeData[]
|
||||||
bin: Bin
|
bin: Bin
|
||||||
binCenter: Vector3
|
binCenter: Vector3
|
||||||
|
onNodeClick?: (node: NodeData, cable: CableData) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BinCables(props: Props){
|
export default function BinCables(props: Props){
|
||||||
const {bin, binCenter, cableData, nodeData} = props
|
const {bin, binCenter, cableData, nodeData, onNodeClick} = props
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{cableData.map((cable, i) => {
|
{cableData.map((cable, i) => {
|
||||||
const cableNodes = nodeData.filter(n => n.cableIndex === cable.cableIndex);
|
const cableNodes = nodeData.filter(n => n.cableIndex === cable.cableIndex);
|
||||||
|
|
||||||
return (
|
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 { useGlobalState } from "providers"
|
||||||
import { pond } from "protobuf-ts/pond"
|
import { pond } from "protobuf-ts/pond"
|
||||||
import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring"
|
import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring"
|
||||||
// import { Color } from "three";
|
|
||||||
import { NodeData } from "../../Data/BuildNodeData"
|
import { NodeData } from "../../Data/BuildNodeData"
|
||||||
import { TempToColour } from "bin/3dView/utils/tempToColour"
|
import { TempToColour } from "bin/3dView/utils/tempToColour"
|
||||||
|
|
||||||
|
|
@ -17,30 +16,23 @@ interface Props {
|
||||||
binCenter: Vector3
|
binCenter: Vector3
|
||||||
lowerThreshold: number
|
lowerThreshold: number
|
||||||
upperThreshold: number
|
upperThreshold: number
|
||||||
|
onNodeClick?: (node: NodeData) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CableNode(props: Props){
|
export default function CableNode(props: Props) {
|
||||||
const {node, binCenter, lowerThreshold, upperThreshold} = props
|
const { node, binCenter, lowerThreshold, upperThreshold, onNodeClick } = props
|
||||||
const { camera } = useThree();
|
const { camera } = useThree();
|
||||||
const [{user}] = useGlobalState();
|
const [{ user }] = useGlobalState();
|
||||||
const [showLabel, setShowLabel] = useState(false);
|
const [showLabel, setShowLabel] = useState(false);
|
||||||
const labelGroupRef = React.useRef<Group>(null);
|
const labelGroupRef = React.useRef<Group>(null);
|
||||||
const ROW_HEIGHT = 35;
|
const ROW_HEIGHT = 35;
|
||||||
const PADDING = 20;
|
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(() => {
|
useFrame(() => {
|
||||||
//use the cameras distance to the center of the bin
|
|
||||||
const distance = camera.position.distanceTo(binCenter)
|
const distance = camera.position.distanceTo(binCenter)
|
||||||
|
|
||||||
const SHOW_DISTANCE = 15;
|
const SHOW_DISTANCE = 15;
|
||||||
const HIDE_DISTANCE = 16; // hysteresis buffer
|
const HIDE_DISTANCE = 16;
|
||||||
|
|
||||||
if (!showLabel && distance < SHOW_DISTANCE) {
|
if (!showLabel && distance < SHOW_DISTANCE) {
|
||||||
setShowLabel(true);
|
setShowLabel(true);
|
||||||
|
|
@ -55,97 +47,27 @@ export default function CableNode(props: Props){
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const tempDisplay = () => {
|
const tempDisplay = useMemo(() => {
|
||||||
let temp = node.celcius.toFixed(2) + "°C"
|
|
||||||
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
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
|
return node.celcius.toFixed(2) + "°C"
|
||||||
}
|
}, [node.celcius, user]);
|
||||||
|
|
||||||
// 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();
|
|
||||||
// };
|
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
const r: { text: string; color: string }[] = [];
|
const r: { text: string; color: string }[] = [];
|
||||||
|
|
||||||
if (node.excluded) {
|
if (node.excluded) {
|
||||||
r.push({
|
r.push({ text: "Excluded", color: "red" });
|
||||||
text: "Excluded",
|
return r;
|
||||||
color: "red"
|
|
||||||
});
|
|
||||||
return r; // nothing else matters
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.topNode) {
|
if (node.topNode) {
|
||||||
r.push({
|
r.push({ text: "Top Node", color: "yellow" });
|
||||||
text: "Top Node",
|
|
||||||
color: "yellow"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r.push({
|
r.push({
|
||||||
text: tempDisplay(),
|
text: tempDisplay,
|
||||||
color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour()
|
color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -164,7 +86,7 @@ export default function CableNode(props: Props){
|
||||||
}
|
}
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}, [node, user]);
|
}, [node, tempDisplay]);
|
||||||
|
|
||||||
const geometry = React.useMemo(() => ({
|
const geometry = React.useMemo(() => ({
|
||||||
radius: node.radius,
|
radius: node.radius,
|
||||||
|
|
@ -173,20 +95,20 @@ export default function CableNode(props: Props){
|
||||||
const topNodeGeo = React.useMemo(() => ({
|
const topNodeGeo = React.useMemo(() => ({
|
||||||
radius: node.radius,
|
radius: node.radius,
|
||||||
thickness: 5,
|
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 topNodeRotation = React.useMemo(() => new Euler(-Math.PI / 2, 0, 0), []);
|
||||||
|
|
||||||
|
|
||||||
const labelPosition = node.position.clone();
|
const labelPosition = node.position.clone();
|
||||||
const labelHeight = rows.length * ROW_HEIGHT + PADDING;
|
const labelHeight = rows.length * ROW_HEIGHT + PADDING;
|
||||||
|
|
||||||
const dir = camera.position.clone().sub(node.position).normalize();
|
const dir = camera.position.clone().sub(node.position).normalize();
|
||||||
|
labelPosition.add(dir.multiplyScalar(1));
|
||||||
// push label slightly toward camera
|
|
||||||
labelPosition.add(dir.multiplyScalar(1)); // small offset like 0.5–2 units
|
|
||||||
|
|
||||||
const getRowY = (index: number) => {
|
const getRowY = (index: number) => {
|
||||||
const totalHeight = rows.length * ROW_HEIGHT;
|
const totalHeight = rows.length * ROW_HEIGHT;
|
||||||
|
|
@ -194,34 +116,35 @@ export default function CableNode(props: Props){
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeColour = () => {
|
const nodeColour = () => {
|
||||||
if (node.excluded){
|
if (node.excluded) return "grey";
|
||||||
return "grey"
|
if (!node.inGrain) return "white";
|
||||||
}
|
const c = TempToColour(node, lowerThreshold, upperThreshold, "node");
|
||||||
if (!node.inGrain){
|
return c !== null ? c.getStyle() : "white";
|
||||||
return "white"
|
};
|
||||||
}
|
|
||||||
let c = TempToColour(node, lowerThreshold, upperThreshold, "node")
|
|
||||||
if (c !== null){
|
|
||||||
return c.getStyle()
|
|
||||||
}
|
|
||||||
return "white"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const handleClick = (e: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onNodeClick?.(node);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{(!node.inGrain || !showLabel) &&
|
{(!node.inGrain || !showLabel) &&
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Sphere geometry={geometry} position={node.position} colour={nodeColour()}/>
|
<Sphere
|
||||||
|
geometry={geometry}
|
||||||
|
position={node.position}
|
||||||
|
colour={nodeColour()}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
{node.topNode && (
|
{node.topNode && (
|
||||||
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation}/>
|
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation} />
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
{node.inGrain && showLabel &&
|
{node.inGrain && showLabel &&
|
||||||
<Billboard position={labelPosition}>
|
<Billboard position={labelPosition}>
|
||||||
<group ref={labelGroupRef} renderOrder={999}>
|
<group ref={labelGroupRef} renderOrder={999} onClick={handleClick}>
|
||||||
{/* Background */}
|
|
||||||
<mesh position={[0, 0, -1]}>
|
<mesh position={[0, 0, -1]}>
|
||||||
<planeGeometry args={[200, labelHeight]} />
|
<planeGeometry args={[200, labelHeight]} />
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
|
|
@ -232,7 +155,6 @@ export default function CableNode(props: Props){
|
||||||
depthTest={false}
|
depthTest={false}
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
{/* Rows */}
|
|
||||||
{rows.map((row, i) => (
|
{rows.map((row, i) => (
|
||||||
<Text
|
<Text
|
||||||
key={i}
|
key={i}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ interface Props {
|
||||||
nodes: NodeData[]
|
nodes: NodeData[]
|
||||||
bin: Bin
|
bin: Bin
|
||||||
binCenter: Vector3
|
binCenter: Vector3
|
||||||
|
onNodeClick?: (node: NodeData) => void
|
||||||
}
|
}
|
||||||
export default function GrainCable(props: Props) {
|
export default function GrainCable(props: Props) {
|
||||||
const {cable, nodes, bin, binCenter} = props
|
const {cable, nodes, bin, binCenter, onNodeClick} = props
|
||||||
|
|
||||||
const calculateHeight = () => {
|
const calculateHeight = () => {
|
||||||
return cable.topPosition.distanceTo(cable.bottomPosition)
|
return cable.topPosition.distanceTo(cable.bottomPosition)
|
||||||
|
|
@ -40,7 +41,7 @@ export default function GrainCable(props: Props) {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5}/>
|
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5}/>
|
||||||
{nodes.map((node, i) => (
|
{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>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue