From 1ac937e7db4bd00f872f3c61063e92c9b90c6522 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 14 Jul 2026 13:21:53 -0600 Subject: [PATCH] updated the colldown mode, fixed a bug where the bin mode on the page was not changing in the dropdown after completing the mode change and some UI tweaks like text and descriptions --- .../CameraControls/OrbitCameraControls.tsx | 108 +++-- src/bin/3dView/Scene/Bin3dView.tsx | 2 + src/bin/bin3dVisualizer.tsx | 3 + src/bin/binModes/BinModeController.tsx | 20 +- src/bin/binModes/CooldownMode.tsx | 395 +++++++++++++++++- src/bin/binModes/DryingMode.tsx | 2 +- src/bin/binModes/HydratingMode.tsx | 26 +- src/bin/binModes/StorageMode.tsx | 5 +- src/bin/binModes/conditionDisplay.tsx | 40 +- 9 files changed, 500 insertions(+), 101 deletions(-) diff --git a/src/3dModels/CameraControls/OrbitCameraControls.tsx b/src/3dModels/CameraControls/OrbitCameraControls.tsx index 7e098d6..4793024 100644 --- a/src/3dModels/CameraControls/OrbitCameraControls.tsx +++ b/src/3dModels/CameraControls/OrbitCameraControls.tsx @@ -1,6 +1,5 @@ -import React from "react"; -import { useThree } from "@react-three/fiber"; -import { useEffect, useRef } from "react"; +import { useThree, useFrame } from "@react-three/fiber"; +import { useCallback, useEffect, useRef } from "react"; import { Vector2, Vector3 } from "three"; interface CameraTarget { @@ -45,6 +44,18 @@ interface Props { * Wire it by passing a ref setter: onReset={fn => resetFn.current = fn} */ onReset?: (resetFn: () => void) => void; + /** + * Automatically spin the camera around the target on mount. + * Any user interaction (drag, pan, zoom, pinch) stops it permanently + * until the camera is reset (reset restarts it if this is still true). + * @default false + */ + autoRotate?: boolean; + /** + * Auto-rotate speed in radians per second. + * @default 0.3 + */ + autoRotateSpeed?: number; } export default function OrbitCameraControls(props: Props) { @@ -58,6 +69,8 @@ export default function OrbitCameraControls(props: Props) { initialRadius, maxRadius, onReset, + autoRotate = false, + autoRotateSpeed = 0.3, } = props; const { camera, gl } = useThree(); @@ -75,6 +88,9 @@ export default function OrbitCameraControls(props: Props) { const panCameraPosition = useRef(new Vector3()); const lastPinchDist = useRef(0); + // Auto-rotate: active until the user does *anything* (drag/pan/zoom/pinch) + const autoRotateActive = useRef(autoRotate); + // Spherical coords const spherical = useRef({ radius: initialRadius ?? 10, @@ -82,6 +98,39 @@ export default function OrbitCameraControls(props: Props) { phi: Math.PI / 2, // vertical angle }); + // Pulled out of the effect so both the DOM-event effect and the + // useFrame auto-rotate loop below can call it. + const updateCamera = useCallback(() => { + const { radius, theta, phi } = spherical.current; + + const x = radius * Math.sin(phi) * Math.sin(theta); + const y = radius * Math.cos(phi); + const z = radius * Math.sin(phi) * Math.cos(theta); + + camera.position.set( + targetRef.current.x + x, + targetRef.current.y + y, + targetRef.current.z + z + ); + camera.lookAt( + targetRef.current.x, + targetRef.current.y, + targetRef.current.z + ); + + // Shift the projected image without affecting orbit + const { width, height } = gl.domElement.getBoundingClientRect(); + (camera as any).setViewOffset(width, height, viewOffset ?? 0, 0, width, height); + }, [camera, gl, viewOffset]); + + // Drives the auto-spin every frame while it's active. Runs inside the + // R3F render loop, so it's cheap and stays in sync with everything else. + useFrame((_, delta) => { + if (!autoRotateActive.current) return; + spherical.current.theta += autoRotateSpeed * delta; + updateCamera(); + }); + useEffect(() => { // Sync radius whenever initialRadius prop changes (e.g. after bin data loads) spherical.current.radius = initialRadius ?? 10; @@ -89,29 +138,6 @@ export default function OrbitCameraControls(props: Props) { const canvas = gl.domElement; canvas.addEventListener("contextmenu", e => e.preventDefault()); - const updateCamera = () => { - const { radius, theta, phi } = spherical.current; - - const x = radius * Math.sin(phi) * Math.sin(theta); - const y = radius * Math.cos(phi); - const z = radius * Math.sin(phi) * Math.cos(theta); - - camera.position.set( - targetRef.current.x + x, - targetRef.current.y + y, - targetRef.current.z + z - ); - camera.lookAt( - targetRef.current.x, - targetRef.current.y, - targetRef.current.z - ); - - // Shift the projected image without affecting orbit - const { width, height } = gl.domElement.getBoundingClientRect(); - (camera as any).setViewOffset(width, height, viewOffset ?? 0, 0, width, height); - }; - // Hand the reset function to the caller so they can trigger it // (e.g. from a button in CameraOverlay) without needing an external ref. if (onReset) { @@ -124,15 +150,25 @@ export default function OrbitCameraControls(props: Props) { y: (target?.y ?? 0) + (offset?.y ?? 0), z: (target?.z ?? 0) + (offset?.z ?? 0), }; + // Resuming auto-rotate on reset feels natural — remove this + // line if you'd rather it stay stopped once the user has + // touched the camera. + autoRotateActive.current = autoRotate; updateCamera(); }); } updateCamera(); + const stopAutoRotate = () => { + autoRotateActive.current = false; + }; + const handleMouseDown = (e: MouseEvent) => { if (e.target !== canvas) return; + stopAutoRotate(); + lastPos.current = { x: e.clientX, y: e.clientY }; panCameraPosition.current.copy(camera.position); @@ -213,6 +249,7 @@ export default function OrbitCameraControls(props: Props) { if (e.target !== canvas) return; e.preventDefault(); + stopAutoRotate(); spherical.current.radius += e.deltaY * 0.01; spherical.current.radius = Math.max( @@ -226,7 +263,8 @@ export default function OrbitCameraControls(props: Props) { const handleTouchStart = (e: TouchEvent) => { if (e.target !== canvas) return; e.preventDefault(); - + stopAutoRotate(); + if (e.touches.length === 1) { isDragging.current = true; isPanning.current = false; @@ -244,10 +282,10 @@ export default function OrbitCameraControls(props: Props) { lastPinchDist.current = Math.sqrt(dx * dx + dy * dy); } }; - + const handleTouchMove = (e: TouchEvent) => { e.preventDefault(); - + if (e.touches.length === 1 && isDragging.current) { const deltaX = e.touches[0].clientX - lastPos.current.x; const deltaY = e.touches[0].clientY - lastPos.current.y; @@ -258,7 +296,7 @@ export default function OrbitCameraControls(props: Props) { } lastPos.current = { x: e.touches[0].clientX, y: e.touches[0].clientY }; updateCamera(); - + } else if (e.touches.length === 2) { // Pinch zoom const dx = e.touches[0].clientX - e.touches[1].clientX; @@ -268,13 +306,13 @@ export default function OrbitCameraControls(props: Props) { spherical.current.radius += pinchDelta * 0.05; spherical.current.radius = Math.max(3, Math.min(maxRadius ?? 30, spherical.current.radius)); lastPinchDist.current = dist; - + // Two-finger pan const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2; const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2; const panDeltaX = midX - lastPos.current.x; const panDeltaY = midY - lastPos.current.y; - + const distance = spherical.current.radius; const panSpeed = 0.002 * distance; const forward = new Vector3(); @@ -288,12 +326,12 @@ export default function OrbitCameraControls(props: Props) { targetRef.current.x += pan.x; targetRef.current.y += pan.y; targetRef.current.z += pan.z; - + lastPos.current = { x: midX, y: midY }; updateCamera(); } }; - + const handleTouchEnd = (e: TouchEvent) => { if (e.touches.length === 0) { isDragging.current = false; @@ -323,7 +361,7 @@ export default function OrbitCameraControls(props: Props) { canvas.removeEventListener("touchmove", handleTouchMove); canvas.removeEventListener("touchend", handleTouchEnd); }; - }, [camera, gl, target, clampVerticalRotation, minPhi, maxPhi, initialRadius, maxRadius]); + }, [camera, gl, target, offset, clampVerticalRotation, minPhi, maxPhi, initialRadius, maxRadius, onReset, autoRotate, updateCamera]); return null; } \ No newline at end of file diff --git a/src/bin/3dView/Scene/Bin3dView.tsx b/src/bin/3dView/Scene/Bin3dView.tsx index 0d15558..ff96995 100644 --- a/src/bin/3dView/Scene/Bin3dView.tsx +++ b/src/bin/3dView/Scene/Bin3dView.tsx @@ -145,6 +145,8 @@ export default function Bin3dView(props: Props) { { resetCameraFn.current = fn; }} diff --git a/src/bin/bin3dVisualizer.tsx b/src/bin/bin3dVisualizer.tsx index bce1ab9..9478d83 100644 --- a/src/bin/bin3dVisualizer.tsx +++ b/src/bin/bin3dVisualizer.tsx @@ -367,6 +367,9 @@ export default function bin3dVisualizer(props: Props){ newMode={newMode} open={openModeChange} onClose={()=>{setOpenModeChange(false)}} + modeUpdated={(newMode) => { + setBinMode(newMode) + }} /> { - //this is just a function that is rin as soo as the start button is clicked it can be used to disable things while the change is in progress if we want to + //this is just a function that is rin as soon as the start button is clicked it can be used to disable things while the change is in progress if we want to }} onComplete={()=>{ closeDialog() @@ -218,13 +220,25 @@ export default function BinModeController(props: Props) { }} /> case pond.BinMode.BIN_MODE_COOLDOWN: - return + return { + buildStages(deviceData) + setShowProgress(true) + }} + /> default: return { buildStages(deviceData) setShowProgress(true) diff --git a/src/bin/binModes/CooldownMode.tsx b/src/bin/binModes/CooldownMode.tsx index 173cf15..55f9c56 100644 --- a/src/bin/binModes/CooldownMode.tsx +++ b/src/bin/binModes/CooldownMode.tsx @@ -1,9 +1,394 @@ -import { Box } from "@mui/material"; +import { Box, Theme, Stepper, Step, StepLabel, Typography, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import ConditioningSelector from "bin/conditioning/conditioningSelector"; +import { Component, Device, Interaction } from "models"; +import { Ambient } from "models/Ambient"; +import { Controller } from "models/Controller"; +import { Plenum } from "models/Plenum"; +import { pond, quack } from "protobuf-ts/pond"; +import { useEffect, useState } from "react"; +import { ComponentSet } from "bin/conditioning/pickComponentSet" +import { componentIDToString, sameComponentID } from "pbHelpers/Component"; +import moment from "moment"; +import { lowerCase } from "lodash"; +import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; +import { useGlobalState, useInteractionsAPI } from "providers"; +import ConditionDisplay from "./conditionDisplay"; +import React from "react"; +import { DeviceChangeData } from "./BinModeController"; +import { GrainCable } from "models/GrainCable"; + + +const useStyles = makeStyles((theme: Theme) => { + return ({ + stepper: { + padding: theme.spacing(0.5) + }, + }) +}); + +interface Step { + label: string; + completed?: boolean; +} + +interface Option { + label: string; + device: Device; + icon?: string; + } + +interface Props { + devices: Device[] + deviceComponents: Map + binPrefs?: Map + grain?: pond.Grain + customGrain?: pond.GrainSettings + cancel: () => void + confirm: (deviceData: DeviceChangeData[]) => void +} + +const steps = [{label: "Device"}, {label: "Interaction"}] + +//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing +export default function CooldownMode(props: Props){ + const {devices, deviceComponents, binPrefs, grain, customGrain, cancel, confirm} = props + const classes = useStyles() + const [{user}] = useGlobalState() + const interactionAPI = useInteractionsAPI() + const [options, setOptions] = useState([]) + const [deviceOption, setDeviceOption] = useState