diff --git a/src/3dModels/CameraControls/OrbitCameraControls.tsx b/src/3dModels/CameraControls/OrbitCameraControls.tsx index 5d707c3..0385cdd 100644 --- a/src/3dModels/CameraControls/OrbitCameraControls.tsx +++ b/src/3dModels/CameraControls/OrbitCameraControls.tsx @@ -26,9 +26,9 @@ interface Props { export default function OrbitCameraControls(props: Props) { const { target, clampVerticalRotation, minPhi = 0.01, maxPhi = Math.PI - 0.01 } = props; const { camera, gl } = useThree(); - const panPlane = useRef(new Plane()); - const panStartPoint = useRef(new Vector3()); - const raycaster = useRef(new Raycaster()); + // const panPlane = useRef(new Plane()); + // const panStartPoint = useRef(new Vector3()); + // const raycaster = useRef(new Raycaster()); const mouse = useRef(new Vector2()); const targetRef = useRef({ x: target?.x ?? 0, @@ -39,6 +39,7 @@ export default function OrbitCameraControls(props: Props) { const isDragging = useRef(false); const isPanning = useRef(false); const lastPos = useRef({ x: 0, y: 0 }); + const panCameraPosition = useRef(new Vector3()); // Spherical coords const spherical = useRef({ @@ -57,7 +58,11 @@ export default function OrbitCameraControls(props: Props) { const y = radius * Math.cos(phi); const z = radius * Math.sin(phi) * Math.cos(theta); - camera.position.set(x, y, z); + camera.position.set( + targetRef.current.x + x, + targetRef.current.y + y, + targetRef.current.z + z + ); camera.lookAt( targetRef.current.x, targetRef.current.y, @@ -71,6 +76,7 @@ export default function OrbitCameraControls(props: Props) { if (e.target !== canvas) return; lastPos.current = { x: e.clientX, y: e.clientY }; + panCameraPosition.current.copy(camera.position); // Left click = rotate if (e.button === 0) { @@ -87,28 +93,29 @@ export default function OrbitCameraControls(props: Props) { const camDir = new Vector3(); camera.getWorldDirection(camDir); - panPlane.current.setFromNormalAndCoplanarPoint( - camDir, - new Vector3( - targetRef.current.x, - targetRef.current.y, - targetRef.current.z - ) - ); + // panPlane.current.setFromNormalAndCoplanarPoint( + // camDir, + // new Vector3( + // targetRef.current.x, + // targetRef.current.y, + // targetRef.current.z + // ) + // ); + + // const rect = canvas.getBoundingClientRect(); + + // raycaster.current.setFromCamera( + // new Vector2( + // ((e.clientX - rect.left) / rect.width) * 2 - 1, + // -((e.clientY - rect.top) / rect.height) * 2 + 1 + // ), + // camera + // ); - // starting intersection point - raycaster.current.setFromCamera( - new Vector2( - (e.clientX / window.innerWidth) * 2 - 1, - -(e.clientY / window.innerHeight) * 2 + 1 - ), - camera - ); - - raycaster.current.ray.intersectPlane( - panPlane.current, - panStartPoint.current - ); + // raycaster.current.ray.intersectPlane( + // panPlane.current, + // panStartPoint.current + // ); } }; @@ -132,28 +139,61 @@ export default function OrbitCameraControls(props: Props) { } // PAN + // if (isPanning.current) { + // const rect = canvas.getBoundingClientRect(); + + // mouse.current.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + // mouse.current.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + + // const currentPoint = new Vector3(); + + // const tempCamera = camera.clone(); + // tempCamera.position.copy(panCameraPosition.current); + // tempCamera.updateMatrixWorld(); + + // raycaster.current.setFromCamera(mouse.current, tempCamera); + + // raycaster.current.ray.intersectPlane(panPlane.current, currentPoint); + + // const delta = new Vector3().subVectors( + // panStartPoint.current, + // currentPoint + // ); + + // targetRef.current.x += delta.x; + // targetRef.current.y += delta.y; + // targetRef.current.z += delta.z; + + // panStartPoint.current.copy(currentPoint); + // } if (isPanning.current) { - const raycaster = new Raycaster(); + const deltaX = e.clientX - lastPos.current.x; + const deltaY = e.clientY - lastPos.current.y; - mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1; - mouse.current.y = -(e.clientY / window.innerHeight) * 2 + 1; + // distance-based scaling (important for consistent feel) + const distance = spherical.current.radius; - raycaster.setFromCamera(mouse.current, camera); + const panSpeed = 0.002 * distance; - const currentPoint = new Vector3(); + const offset = new Vector3(); - raycaster.ray.intersectPlane(panPlane.current, currentPoint); + // get camera basis vectors + const right = new Vector3(); + const up = new Vector3(); - const delta = new Vector3().subVectors( - panStartPoint.current, - currentPoint - ); + camera.getWorldDirection(offset); // forward + right.crossVectors(offset, camera.up).normalize(); // right + up.copy(camera.up).normalize(); - targetRef.current.x += delta.x; - targetRef.current.y += delta.y; - targetRef.current.z += delta.z; + // apply movement + right.multiplyScalar(-deltaX * panSpeed); + up.multiplyScalar(deltaY * panSpeed); - panStartPoint.current.copy(currentPoint); + const pan = new Vector3().addVectors(right, up); + + targetRef.current.x += pan.x; + targetRef.current.y += pan.y; + targetRef.current.z += pan.z; } lastPos.current = { x: e.clientX, y: e.clientY }; diff --git a/src/bin/3dView/BinCables/BinCables.tsx b/src/bin/3dView/BinCables/BinCables.tsx deleted file mode 100644 index e7b5684..0000000 --- a/src/bin/3dView/BinCables/BinCables.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import GrainCable, { CableData } from "./GrainCable"; -import { avg } from "utils"; -import { Vector3 } from "three"; -import { useMemo } from "react"; -import { Bin } from "models"; -import React from "react"; - -interface CableOrbit { - orbit: number; - radius: number; // in cm - expectedCount: number; - assignedCount?: number; -} - -interface Props { - bin: Bin - binCenter: Vector3 -} - -export default function BinCables(props: Props){ - const {bin, binCenter} = props - const getLayoutFromDiameter = (diameterCm: number): CableOrbit[] => { - const diameterFt = diameterCm / 30.48; - - let summaries: { Orbit: number; DistanceFromCenter: number; NumberOfCables: number }[] = []; - - if (diameterFt < 24) { - summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); - } else if (diameterFt < 36) { - summaries.push({ Orbit: 1, DistanceFromCenter: 7, NumberOfCables: 3 }); - } else if (diameterFt < 42) { - summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); - summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 }); - } else if (diameterFt < 48) { - summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); - summaries.push({ Orbit: 1, DistanceFromCenter: 14, NumberOfCables: 4 }); - } else if (diameterFt < 54) { - summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); - summaries.push({ Orbit: 1, DistanceFromCenter: 16, NumberOfCables: 5 }); - } else if (diameterFt < 60) { - summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); - summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 }); - } else if (diameterFt < 72) { - summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); - summaries.push({ Orbit: 1, DistanceFromCenter: 20, NumberOfCables: 7 }); - } else if (diameterFt < 78) { - summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 }); - summaries.push({ Orbit: 2, DistanceFromCenter: 27, NumberOfCables: 9 }); - } else if (diameterFt < 90) { - summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 }); - summaries.push({ Orbit: 2, DistanceFromCenter: 29, NumberOfCables: 10 }); - } else if (diameterFt < 105) { - summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); - summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 }); - summaries.push({ Orbit: 2, DistanceFromCenter: 36, NumberOfCables: 12 }); - } else { - summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 }); - summaries.push({ Orbit: 2, DistanceFromCenter: 26.5, NumberOfCables: 9 }); - summaries.push({ Orbit: 3, DistanceFromCenter: 44, NumberOfCables: 14 }); - } - - return summaries.map(s => ({ - orbit: s.Orbit, - radius: s.DistanceFromCenter * 30.48, // ft → cm - expectedCount: s.NumberOfCables - })); - }; - - const distributeCables = (cableCount: number, layout: CableOrbit[]) => { - const totalExpected = layout.reduce((sum, o) => sum + o.expectedCount, 0); - - let assigned = layout.map(o => ({ - ...o, - assignedCount: Math.round((o.expectedCount / totalExpected) * cableCount) - })); - - // normalize rounding - let currentTotal = assigned.reduce((sum, o) => sum + (o.assignedCount ?? 0), 0); - - while (currentTotal < cableCount) { - assigned[assigned.length - 1].assignedCount!++; - currentTotal++; - } - - while (currentTotal > cableCount) { - assigned[assigned.length - 1].assignedCount!--; - currentTotal--; - } - - return assigned; - }; - - const getTopY = (radiusFromCenter: number) => { - const binRadius = bin.diameter() / 2; - - const distanceFromEdge = binRadius - radiusFromCenter; - - const angleRad = bin.roofAngle() * (Math.PI / 180); - - const roofRise = Math.tan(angleRad) * distanceFromEdge; - - return bin.sidewallHeight() / 2 + roofRise; - }; - - const getBottomY = (radiusFromCenter: number) => { - const binRadius = bin.diameter() / 2; - - const distanceFromCenter = binRadius - radiusFromCenter; - - const angleRad = bin.hopperAngle() * (Math.PI / 180); - - const hopperDrop = Math.tan(angleRad) * distanceFromCenter; - - const clearance = 60 //this is in cm - - return -bin.sidewallHeight() / 2 - hopperDrop + clearance; - }; - - const buildCablePositions = (bin: Bin): CableData[] => { - const cables = [...(bin.status.grainCables ?? [])]; - - if (cables.length === 0) return []; - - // optional: sort cables (better visual consistency later) - //cables.sort((a, b) => b.celcius.length - a.celcius.length); - cables.sort((a, b) => { - //if the cables have the same number of nodes - if (b.celcius.length === a.celcius.length) { - //sort by temp only last and any type that has humidity first - if ((avg(a.relativeHumidity) === 0) && (avg(b.relativeHumidity) !== 0)) { - return 1; - } else if ((avg(b.relativeHumidity) === 0) && (avg(a.relativeHumidity) !== 0)) { - return -1; - } - else { - return a.key > b.key ? 1 : -1; - } - } - //otherwise sort by the longest cable first - return b.celcius.length - a.celcius.length; - }); - - const layout = getLayoutFromDiameter(bin.diameter()); - const distributed = distributeCables(cables.length, layout); - - - const cableRadius = 5; - - const result: CableData[] = []; - - let cableIndex = 0; - - distributed.forEach(orbit => { - const count = orbit.assignedCount ?? 0; - - const topY = getTopY(orbit.radius) - const bottomY = getBottomY(orbit.radius) - - if (orbit.orbit === 0 && count > 0) { - const cable = cables[cableIndex++]; - - result.push({ - radius: cableRadius, - topPosition: new Vector3(0, topY, 0), - bottomPosition: new Vector3(0, bottomY, 0), - grainCable: cable - // optionally attach cable data later - }); - return; - } - - for (let i = 0; i < count; i++) { - const angle = (i / count) * Math.PI * 2; - const x = Math.cos(angle) * orbit.radius; - const z = Math.sin(angle) * orbit.radius; - const cable = cables[cableIndex++]; - - result.push({ - radius: cableRadius, - topPosition: new Vector3(x,topY,z), - bottomPosition: new Vector3(x,bottomY,z), - grainCable: cable - }); - } - }); - - return result; - }; - - const cables3D = useMemo(() => { - return buildCablePositions(bin); - }, [bin]); - - return ( - - {cables3D.map((cable, i) => ( - - ))} - - ) -} \ No newline at end of file diff --git a/src/bin/3dView/BinCables/GrainCable.tsx b/src/bin/3dView/BinCables/GrainCable.tsx deleted file mode 100644 index e889893..0000000 --- a/src/bin/3dView/BinCables/GrainCable.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import Cylinder, { CylinderGeometry } from "3dModels/Shapes/3D/Cylinder" -import { pond } from "protobuf-ts/pond" -import React from "react" -import { Vector3 } from "three" -import CableNode, { NodeData } from "./CableNode" -import { Bin } from "models" - -export interface CableData { - radius: number - topPosition: Vector3 - bottomPosition: Vector3 - grainCable: pond.GrainCable - radialSegments?: number - heightSegments?: number -} - -interface Props { - cable: CableData - bin: Bin - binCenter: Vector3 -} -export default function GrainCable(props: Props) { - const {cable, bin, binCenter} = props - - const calculateHeight = () => { - return cable.topPosition.distanceTo(cable.bottomPosition) - } - - const calculateCenter = () => { - return cable.bottomPosition.clone() - .add( - cable.topPosition.clone() - .sub(cable.bottomPosition) - .multiplyScalar(0.5) - ); - }; - - const buildCableNodes = () => { - const nodeRadius = 20 - const grainCable = cable.grainCable - - const nodeCount = grainCable.celcius.length; - - if (nodeCount === 0) return []; - - const bottomY = cable.bottomPosition.y; - const topY = cable.topPosition.y; - const height = topY - bottomY; - - const spacing = height / nodeCount; - - let nodeData: NodeData[] = [] - grainCable.celcius.forEach((celcius,i) => { - const nodeY = bottomY + (i * spacing); - - const humidity = grainCable.relativeHumidity[i] || undefined; - const moisture = grainCable.moisture[i] || undefined; - - nodeData.push({ - radius: nodeRadius, - position: new Vector3(cable.bottomPosition.x, nodeY, cable.bottomPosition.z), - celcius: celcius, - humidity: humidity, - moisture: moisture, - topNode: grainCable.topNode === i+1, //top node tracking starts at 1 not 0 so add one to the index - excluded: grainCable.excludedNodes.includes(i), - inGrain: i+1 <= grainCable.topNode - }) - - }) - - return nodeData - } - - const geometry = React.useMemo(() => ({ - radiusTop: cable.radius, - radiusBottom: cable.radius, - height: calculateHeight(), - radialSegments: cable.radialSegments ?? 10, - heightSegments: cable.heightSegments ?? 2 - } as CylinderGeometry), [cable]); - - return ( - - - {buildCableNodes().map((node, i) => ( - - ))} - - ) -} \ No newline at end of file diff --git a/src/bin/3dView/Data/BuildCableData.ts b/src/bin/3dView/Data/BuildCableData.ts new file mode 100644 index 0000000..f9493a0 --- /dev/null +++ b/src/bin/3dView/Data/BuildCableData.ts @@ -0,0 +1,194 @@ +import { Bin } from "models"; +import { pond } from "protobuf-ts/pond"; +import { Vector3 } from "three"; +import { avg } from "utils"; + +export interface CableData { + cableIndex: number + radius: number + topPosition: Vector3 + bottomPosition: Vector3 + grainCable: pond.GrainCable + radialSegments?: number + heightSegments?: number +} + +export interface CableOrbit { + orbit: number; + radius: number; // in cm + expectedCount: number; + assignedCount?: number; +} + + +function getLayoutFromDiameter(diameterCm: number): CableOrbit[] { + const diameterFt = diameterCm / 30.48; + + let summaries: { Orbit: number; DistanceFromCenter: number; NumberOfCables: number }[] = []; + + if (diameterFt < 24) { + summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); + } else if (diameterFt < 36) { + summaries.push({ Orbit: 1, DistanceFromCenter: 7, NumberOfCables: 3 }); + } else if (diameterFt < 42) { + summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); + summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 }); + } else if (diameterFt < 48) { + summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); + summaries.push({ Orbit: 1, DistanceFromCenter: 14, NumberOfCables: 4 }); + } else if (diameterFt < 54) { + summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); + summaries.push({ Orbit: 1, DistanceFromCenter: 16, NumberOfCables: 5 }); + } else if (diameterFt < 60) { + summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); + summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 }); + } else if (diameterFt < 72) { + summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); + summaries.push({ Orbit: 1, DistanceFromCenter: 20, NumberOfCables: 7 }); + } else if (diameterFt < 78) { + summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 }); + summaries.push({ Orbit: 2, DistanceFromCenter: 27, NumberOfCables: 9 }); + } else if (diameterFt < 90) { + summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 }); + summaries.push({ Orbit: 2, DistanceFromCenter: 29, NumberOfCables: 10 }); + } else if (diameterFt < 105) { + summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 }); + summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 }); + summaries.push({ Orbit: 2, DistanceFromCenter: 36, NumberOfCables: 12 }); + } else { + summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 }); + summaries.push({ Orbit: 2, DistanceFromCenter: 26.5, NumberOfCables: 9 }); + summaries.push({ Orbit: 3, DistanceFromCenter: 44, NumberOfCables: 14 }); + } + + return summaries.map(s => ({ + orbit: s.Orbit, + radius: s.DistanceFromCenter * 30.48, // ft → cm + expectedCount: s.NumberOfCables + })); +}; + +function distributeCables(cableCount: number, layout: CableOrbit[]) { + const totalExpected = layout.reduce((sum, o) => sum + o.expectedCount, 0); + + let assigned = layout.map(o => ({ + ...o, + assignedCount: Math.round((o.expectedCount / totalExpected) * cableCount) + })); + + // normalize rounding + let currentTotal = assigned.reduce((sum, o) => sum + (o.assignedCount ?? 0), 0); + + while (currentTotal < cableCount) { + assigned[assigned.length - 1].assignedCount!++; + currentTotal++; + } + + while (currentTotal > cableCount) { + assigned[assigned.length - 1].assignedCount!--; + currentTotal--; + } + + return assigned; +}; + +function getTopY(radiusFromCenter: number, bin: Bin) { + const binRadius = bin.diameter() / 2; + + const distanceFromEdge = binRadius - radiusFromCenter; + + const angleRad = bin.roofAngle() * (Math.PI / 180); + + const roofRise = Math.tan(angleRad) * distanceFromEdge; + + return bin.sidewallHeight() / 2 + roofRise; +}; + +function getBottomY(radiusFromCenter: number, bin: Bin) { + const binRadius = bin.diameter() / 2; + + const distanceFromCenter = binRadius - radiusFromCenter; + + const angleRad = bin.hopperAngle() * (Math.PI / 180); + + const hopperDrop = Math.tan(angleRad) * distanceFromCenter; + + const clearance = 60 //this is in cm + + return -bin.sidewallHeight() / 2 - hopperDrop + clearance; +}; + + +export function BuildCableData(bin: Bin): CableData[] { + const cables = [...(bin.status.grainCables ?? [])]; + + if (cables.length === 0) return []; + + // optional: sort cables (better visual consistency later) + //cables.sort((a, b) => b.celcius.length - a.celcius.length); + cables.sort((a, b) => { + //if the cables have the same number of nodes + if (b.celcius.length === a.celcius.length) { + //sort by temp only last and any type that has humidity first + if ((avg(a.relativeHumidity) === 0) && (avg(b.relativeHumidity) !== 0)) { + return 1; + } else if ((avg(b.relativeHumidity) === 0) && (avg(a.relativeHumidity) !== 0)) { + return -1; + } + else { + return a.key > b.key ? 1 : -1; + } + } + //otherwise sort by the longest cable first + return b.celcius.length - a.celcius.length; + }); + + const layout = getLayoutFromDiameter(bin.diameter()); + const distributed = distributeCables(cables.length, layout); + + + const cableRadius = 5; + + const result: CableData[] = []; + + let cableIndex = 0; + + distributed.forEach(orbit => { + const count = orbit.assignedCount ?? 0; + + const topY = getTopY(orbit.radius, bin) + const bottomY = getBottomY(orbit.radius, bin) + + if (orbit.orbit === 0 && count > 0) { + const cable = cables[cableIndex]; + if (!cable) return + result.push({ + cableIndex: cableIndex, + radius: cableRadius, + topPosition: new Vector3(0, topY, 0), + bottomPosition: new Vector3(0, bottomY, 0), + grainCable: cable + }); + cableIndex++ + return; + } + + for (let i = 0; i < count; i++) { + const angle = (i / count) * Math.PI * 2; + const x = Math.cos(angle) * orbit.radius; + const z = Math.sin(angle) * orbit.radius; + const cable = cables[cableIndex]; + + result.push({ + cableIndex: cableIndex, + radius: cableRadius, + topPosition: new Vector3(x,topY,z), + bottomPosition: new Vector3(x,bottomY,z), + grainCable: cable + }); + cableIndex++ + } + }); + + return result; +}; \ No newline at end of file diff --git a/src/bin/3dView/Data/BuildNodeData.ts b/src/bin/3dView/Data/BuildNodeData.ts new file mode 100644 index 0000000..de14d23 --- /dev/null +++ b/src/bin/3dView/Data/BuildNodeData.ts @@ -0,0 +1,60 @@ +import { Vector3 } from "three" +import { CableData } from "./BuildCableData" + +export interface NodeData { + cableIndex: number + nodeIndex: number + radius: number + position: Vector3 + celcius: number + humidity?: number + moisture?: number + topNode?: boolean + excluded?: boolean + inGrain?: boolean +} + +export function BuildNodeData(cables: CableData[]): NodeData[] { + const nodeRadius = 20 + let nodeData: NodeData[] = [] + + cables.forEach((cable, cableIndex) => { + const grainCable = cable.grainCable + + const nodeCount = grainCable.celcius.length; + + if (nodeCount === 0) return; + + const bottomY = cable.bottomPosition.y; + const topY = cable.topPosition.y; + const height = topY - bottomY; + + const spacing = height / nodeCount; + + + grainCable.celcius.forEach((celcius,i) => { + const nodeY = bottomY + (i * spacing); + + const humidity = grainCable.relativeHumidity[i] || undefined; + const moisture = grainCable.moisture[i] || undefined; + + nodeData.push({ + cableIndex: cableIndex, + nodeIndex: i, + radius: nodeRadius, + position: new Vector3(cable.bottomPosition.x, nodeY, cable.bottomPosition.z), + celcius: celcius, + humidity: humidity, + moisture: moisture, + topNode: grainCable.topNode === i+1, //top node tracking starts at 1 not 0 so add one to the index + excluded: grainCable.excludedNodes?.includes(i) ?? false, + inGrain: i+1 <= grainCable.topNode + // optional: (may want to add this to the node data later) + // normalizedHeight: (nodeY - bottomY) / height + }) + + }) + }) + + return nodeData +} \ No newline at end of file diff --git a/src/bin/3dView/Bin3dView.tsx b/src/bin/3dView/Scene/Bin3dView.tsx similarity index 83% rename from src/bin/3dView/Bin3dView.tsx rename to src/bin/3dView/Scene/Bin3dView.tsx index 6b6213d..5be25d8 100644 --- a/src/bin/3dView/Bin3dView.tsx +++ b/src/bin/3dView/Scene/Bin3dView.tsx @@ -1,11 +1,13 @@ import OrbitCameraControls from "3dModels/CameraControls/OrbitCameraControls"; import { Canvas } from "@react-three/fiber"; import { Bin } from "models"; -import BinShell from "./BinParts/BinShell"; -import GrainFillFlat from "./BinInventory/GrainFillFlat"; -import BinCables from "./BinCables/BinCables"; +import BinShell from "../BinParts/BinShell"; +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"; interface Props { /** @@ -30,8 +32,8 @@ export default function Bin3dView(props: Props){ // 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 binCenter = useMemo(() => new Vector3(0, 0, 0), []); - - console.log(bin) + const cableData = useMemo(() => BuildCableData(bin), [bin]); + const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]); return ( @@ -46,6 +48,7 @@ export default function Bin3dView(props: Props){ sidewallHeight={bin.sidewallHeight()} roofHeight={bin.roofHeight()} hopperHeight={bin.hopperHeight()} + /> {/* grain - cylinder*/} @@ -58,7 +61,7 @@ export default function Bin3dView(props: Props){ /> )} {/* cables */} - + {/* lighting */} diff --git a/src/bin/3dView/Systems/Cables/BinCables.tsx b/src/bin/3dView/Systems/Cables/BinCables.tsx new file mode 100644 index 0000000..452a471 --- /dev/null +++ b/src/bin/3dView/Systems/Cables/BinCables.tsx @@ -0,0 +1,28 @@ +import GrainCable from "./GrainCable"; +import { Vector3 } from "three"; +import { Bin } from "models"; +import React from "react"; +import { CableData } from "../../Data/BuildCableData"; +import { NodeData } from "../../Data/BuildNodeData"; + +interface Props { + cableData: CableData[] + nodeData: NodeData[] + bin: Bin + binCenter: Vector3 +} + +export default function BinCables(props: Props){ + const {bin, binCenter, cableData, nodeData} = props + return ( + + {cableData.map((cable, i) => { + const cableNodes = nodeData.filter(n => n.cableIndex === cable.cableIndex); + return ( + + )} + )} + + + ) +} \ No newline at end of file diff --git a/src/bin/3dView/BinCables/CableNode.tsx b/src/bin/3dView/Systems/Cables/CableNode.tsx similarity index 65% rename from src/bin/3dView/BinCables/CableNode.tsx rename to src/bin/3dView/Systems/Cables/CableNode.tsx index a9803fc..b0b7307 100644 --- a/src/bin/3dView/BinCables/CableNode.tsx +++ b/src/bin/3dView/Systems/Cables/CableNode.tsx @@ -1,24 +1,16 @@ import Sphere, { SphereGeometry } from "3dModels/Shapes/3D/Sphere" import { useFrame, useThree } from "@react-three/fiber" import { Billboard, Text } from "@react-three/drei" -import React, { useMemo, useRef, useState } from "react" +import React, { useMemo, useState } from "react" import { Euler, Group, Vector3 } from "three" import { describeMeasurement } from "pbHelpers/MeasurementDescriber" import { quack } from "protobuf-ts/quack" import { useGlobalState } from "providers" import { pond } from "protobuf-ts/pond" import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring" - -export interface NodeData { - radius: number - position: Vector3 - celcius: number - humidity?: number - moisture?: number - topNode?: boolean - excluded?: boolean - inGrain?: boolean -} +// import { Color } from "three"; +import { NodeData } from "../../Data/BuildNodeData" +import { tempToColour } from "bin/3dView/utils/tempToColour" interface Props { node: NodeData @@ -36,6 +28,13 @@ export default function CableNode(props: Props){ 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) @@ -64,22 +63,68 @@ export default function CableNode(props: Props){ return temp } - const nodeColour = () => { - if (!node.inGrain) return "white"; + // const nodeColour = () => { + // if (!node.inGrain) return "white"; + // if (node.excluded) return "grey"; - const lower = lowerThreshold; - const upper = upperThreshold; + // const lower = lowerThreshold; + // const upper = upperThreshold; + // const temp = node.celcius; - if (lower !== undefined && node.celcius < lower) { - return "#3399ff"; // blue - } + // const GREEN = new Color("#52c41a"); + // const BLUE = new Color("#3399ff"); + // const RED = new Color("#ff4d4f"); - if (upper !== undefined && node.celcius > upper) { - return "#ff4d4f"; // red - } + // if (temp >= lower && temp <= upper) { + // return GREEN.getStyle(); + // } - return "#52c41a"; // green - }; + // // 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 r: { text: string; color: string }[] = []; @@ -153,7 +198,7 @@ export default function CableNode(props: Props){ {(!node.inGrain || !showLabel) && - + {node.topNode && ( )} diff --git a/src/bin/3dView/Systems/Cables/GrainCable.tsx b/src/bin/3dView/Systems/Cables/GrainCable.tsx new file mode 100644 index 0000000..44c83ff --- /dev/null +++ b/src/bin/3dView/Systems/Cables/GrainCable.tsx @@ -0,0 +1,47 @@ +import Cylinder, { CylinderGeometry } from "3dModels/Shapes/3D/Cylinder" +import React from "react" +import { Vector3 } from "three" +import CableNode from "./CableNode" +import { Bin } from "models" +import { CableData } from "../../Data/BuildCableData" +import { NodeData } from "../../Data/BuildNodeData" + +interface Props { + cable: CableData + nodes: NodeData[] + bin: Bin + binCenter: Vector3 +} +export default function GrainCable(props: Props) { + const {cable, nodes, bin, binCenter} = props + + const calculateHeight = () => { + return cable.topPosition.distanceTo(cable.bottomPosition) + } + + const calculateCenter = () => { + return cable.bottomPosition.clone() + .add( + cable.topPosition.clone() + .sub(cable.bottomPosition) + .multiplyScalar(0.5) + ); + }; + + const geometry = React.useMemo(() => ({ + radiusTop: cable.radius, + radiusBottom: cable.radius, + height: calculateHeight(), + radialSegments: cable.radialSegments ?? 10, + heightSegments: cable.heightSegments ?? 2 + } as CylinderGeometry), [cable]); + + return ( + + + {nodes.map((node, i) => ( + + ))} + + ) +} \ No newline at end of file diff --git a/src/bin/3dView/Systems/Heatmap/Heatmap.tsx b/src/bin/3dView/Systems/Heatmap/Heatmap.tsx new file mode 100644 index 0000000..0434581 --- /dev/null +++ b/src/bin/3dView/Systems/Heatmap/Heatmap.tsx @@ -0,0 +1,16 @@ +import { NodeData } from "bin/3dView/Data/BuildNodeData"; +import { Bin } from "models"; + +interface Props{ + bin: Bin + nodes: NodeData +} + +export default function Heatmap(props: Props){ + const {bin, nodes} = props + + return ( + <> + + ) +} \ No newline at end of file diff --git a/src/bin/3dView/BinInventory/GrainFillFlat.tsx b/src/bin/3dView/Systems/Inventory/GrainFillFlat.tsx similarity index 100% rename from src/bin/3dView/BinInventory/GrainFillFlat.tsx rename to src/bin/3dView/Systems/Inventory/GrainFillFlat.tsx diff --git a/src/bin/3dView/utils/tempToColour.ts b/src/bin/3dView/utils/tempToColour.ts new file mode 100644 index 0000000..c2c6d79 --- /dev/null +++ b/src/bin/3dView/utils/tempToColour.ts @@ -0,0 +1,68 @@ +import { Color } from "three"; +import { NodeData } from "../Data/BuildNodeData"; + +type ColourMode = "node" | "heatmap"; + +const GREEN = new Color("#52c41a"); +const BLUE = new Color("#3399ff"); +const RED = new Color("#ff4d4f"); + +const colourFade = 20; +const minimumLightness = 0.3; +const lightnessRange = 0.2; +const minimumSaturation = 0.7; +const saturationRange = 0.8; + +const getTemperatureIntensity = (temp: number, lower: number, upper: number) => { + if (temp >= lower && temp <= upper) return 0; + if (temp < lower) return lower - temp; + return temp - upper; +} + +const getVisualIntensity = (distance: number, mode: ColourMode) => { + // 1 = near threshold, 0 = far + const intensity = Math.min(1, distance / colourFade); + switch(mode){ + case "node": + return 1 - intensity + case "heatmap": + return intensity + } +}; + +export function tempToColour( + node: NodeData, + lower: number, + upper: number, + mode: ColourMode +): Color | null { + + if (!node.inGrain || node.excluded) return null; + + const temp = node.celcius; + + // in-threshold behavior + if (temp >= lower && temp <= upper) { + return mode === "heatmap" ? null : GREEN; + } + + const distance = getTemperatureIntensity(temp, lower, upper); + const intensity = getVisualIntensity(distance, mode); + + const color = new Color(); + const hsl = { h: 0, s: 1, l: 1 }; + + if (temp < lower) { + BLUE.getHSL(hsl); + } else { + RED.getHSL(hsl); + } + + color.setHSL( + hsl.h, + (saturationRange * intensity) + minimumSaturation, + (lightnessRange * intensity) + minimumLightness + ); + + return color; +} \ No newline at end of file diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index 9a938bc..78e6c38 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -66,7 +66,7 @@ import ButtonGroup from "common/ButtonGroup"; import BinTransactions from "bin/BinTransactions"; import { CO2 } from "models/CO2"; import ObjectInteractions from "objects/objectInteractions/ObjectInteractions"; -import Bin3dView from "bin/3dView/Bin3dView"; +import Bin3dView from "bin/3dView/Scene/Bin3dView"; interface TabPanelProps { children?: React.ReactNode;