From a8b0837e58f2378e995344347f76b0ef701841df Mon Sep 17 00:00:00 2001 From: Christian <80114847+TheBest6337@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:51:46 +0100 Subject: [PATCH 1/6] Add MinMaxValue component for displaying diameter ranges --- electron/src/control/MinMaxValue.tsx | 175 ++++++++++++++++++ .../laser/laser1/Laser1ControlPage.tsx | 29 +++ 2 files changed, 204 insertions(+) create mode 100644 electron/src/control/MinMaxValue.tsx diff --git a/electron/src/control/MinMaxValue.tsx b/electron/src/control/MinMaxValue.tsx new file mode 100644 index 00000000..4fb8be17 --- /dev/null +++ b/electron/src/control/MinMaxValue.tsx @@ -0,0 +1,175 @@ +import { Icon, IconName } from "@/components/Icon"; +import React, { useState, useMemo } from "react"; +import { + getUnitIcon, + renderValueToReactNode, + renderUnitSymbol, + Unit, +} from "./units"; +import { Label } from "./Label"; +import { TimeSeries, TimeSeriesValue } from "@/lib/timeseries"; +import { getSeriesMinMax } from "@/lib/timeseries"; + +type TimeframeOption = { + label: string; + value: number; // in milliseconds +}; + +const TIMEFRAME_OPTIONS: TimeframeOption[] = [ + { label: "1 min", value: 1 * 60 * 1000 }, + { label: "5 min", value: 5 * 60 * 1000 }, + { label: "10 min", value: 10 * 60 * 1000 }, + { label: "30 min", value: 30 * 60 * 1000 }, + { label: "1 hour", value: 60 * 60 * 1000 }, +]; + +type Props = { + label: string; + unit?: Unit; + timeseries: TimeSeries; + icon?: IconName; + renderValue?: (value: number) => string; + defaultTimeframe?: number; // in milliseconds, defaults to 5 minutes +}; + +export function MinMaxValue({ + unit, + timeseries, + icon, + label, + renderValue, + defaultTimeframe = 5 * 60 * 1000, // 5 minutes default +}: Props) { + const [selectedTimeframe, setSelectedTimeframe] = useState(defaultTimeframe); + + // Calculate min/max from the timeseries using the selected timeframe + const { min, max, hasData } = useMemo(() => { + // Use short series for timeframes <= 5 seconds, otherwise use long series + // Short series typically has ~5 second retention with high resolution + // Long series typically has ~1 hour retention with lower resolution + const series = + selectedTimeframe <= 5000 ? timeseries.short : timeseries.long; + + // Manually calculate min/max to exclude zero values (which might be invalid readings) + const cutoffTime = series.lastTimestamp - selectedTimeframe; + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; + let hasValidData = false; + let dataCount = 0; + + const { values: raw, index, size, validCount } = series; + + const startIdx = validCount < size ? 0 : index; + + for (let i = 0; i < validCount; i++) { + const idx = (startIdx + i) % size; + const val = raw[idx]; + + if ( + val && + val.timestamp > 0 && + val.timestamp >= cutoffTime && + val.value > 0 + ) { + hasValidData = true; + dataCount++; + if (val.value < min) min = val.value; + if (val.value > max) max = val.value; + } + } + + console.log("MinMaxValue Debug:", { + selectedTimeframe, + seriesType: selectedTimeframe <= 5000 ? "short" : "long", + validCount: series.validCount, + lastTimestamp: series.lastTimestamp, + cutoffTime, + dataCount, + min: hasValidData ? min : 0, + max: hasValidData ? max : 0, + currentValue: timeseries.current?.value, + sampleData: series.values + .slice(0, 5) + .filter((v): v is TimeSeriesValue => v !== null && v.timestamp > 0) + .map((v) => ({ value: v.value, timestamp: v.timestamp })), + }); + + if (!hasValidData) { + return { min: 0, max: 0, hasData: false }; + } + + return { min, max, hasData: true }; + }, [ + timeseries.short, + timeseries.long, + timeseries.current, + selectedTimeframe, + ]); + + return ( +
+ + + {/* Timeframe Selector */} +
+ {TIMEFRAME_OPTIONS.map((option) => ( + + ))} +
+
+ ); +} diff --git a/electron/src/machines/laser/laser1/Laser1ControlPage.tsx b/electron/src/machines/laser/laser1/Laser1ControlPage.tsx index f9a8a319..49245d94 100644 --- a/electron/src/machines/laser/laser1/Laser1ControlPage.tsx +++ b/electron/src/machines/laser/laser1/Laser1ControlPage.tsx @@ -3,6 +3,7 @@ import { Page } from "@/components/Page"; import React from "react"; import { ControlGrid } from "@/control/ControlGrid"; import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue"; +import { MinMaxValue } from "@/control/MinMaxValue"; import { EditValue } from "@/control/EditValue"; import { Label } from "@/control/Label"; @@ -77,6 +78,34 @@ export function Laser1ControlPage() { /> )} +
+ value.toFixed(3)} + /> +
+ {x_diameter?.current && ( +
+ value.toFixed(3)} + /> +
+ )} + {y_diameter?.current && ( +
+ value.toFixed(3)} + /> +
+ )} - {/* Timeframe Selector */} -
- {TIMEFRAME_OPTIONS.map((option) => ( - - ))} -
+ {/* Timeframe Selector (hidden when sharing selector externally) */} + {!hideSelector && ( +
+ {TIMEFRAME_OPTIONS.map((option) => ( + + ))} +
+ )} ); } diff --git a/electron/src/machines/laser/laser1/Laser1ControlPage.tsx b/electron/src/machines/laser/laser1/Laser1ControlPage.tsx index 01dd6805..bb256f74 100644 --- a/electron/src/machines/laser/laser1/Laser1ControlPage.tsx +++ b/electron/src/machines/laser/laser1/Laser1ControlPage.tsx @@ -3,7 +3,7 @@ import { Page } from "@/components/Page"; import React from "react"; import { ControlGrid } from "@/control/ControlGrid"; import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue"; -import { MinMaxValue } from "@/control/MinMaxValue"; +import { MinMaxValue, TIMEFRAME_OPTIONS } from "@/control/MinMaxValue"; import { EditValue } from "@/control/EditValue"; import { Label } from "@/control/Label"; @@ -32,6 +32,8 @@ export function Laser1ControlPage() { // Detect if this is a 2-axis laser const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current; + // Shared timeframe state (default 5 minutes) + const [timeframe, setTimeframe] = React.useState(5 * 60 * 1000); return ( @@ -83,6 +85,23 @@ export function Laser1ControlPage() { )}
+ {/* Shared timeframe selector for diameter/roundness */} +
+ {TIMEFRAME_OPTIONS.map((option) => ( + + ))} +
+ {isTwoAxis ? ( // For 2-axis lasers: show diameter and roundness min/max side by side
@@ -91,12 +110,16 @@ export function Laser1ControlPage() { unit="mm" timeseries={diameter} renderValue={(value) => value.toFixed(3)} + timeframe={timeframe} + hideSelector /> {roundness?.current && ( value.toFixed(3)} + timeframe={timeframe} + hideSelector /> )}
From 8594b06c3bd1357f767fd36ef8f81099d7efea9e Mon Sep 17 00:00:00 2001 From: Christian <80114847+TheBest6337@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:18:44 +0100 Subject: [PATCH 4/6] Reorganize shared timeframe selector for clarity and improve layout for diameter range display --- .../laser/laser1/Laser1ControlPage.tsx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/electron/src/machines/laser/laser1/Laser1ControlPage.tsx b/electron/src/machines/laser/laser1/Laser1ControlPage.tsx index bb256f74..e38e08a4 100644 --- a/electron/src/machines/laser/laser1/Laser1ControlPage.tsx +++ b/electron/src/machines/laser/laser1/Laser1ControlPage.tsx @@ -85,23 +85,6 @@ export function Laser1ControlPage() {
)}
- {/* Shared timeframe selector for diameter/roundness */} -
- {TIMEFRAME_OPTIONS.map((option) => ( - - ))} -
- {isTwoAxis ? ( // For 2-axis lasers: show diameter and roundness min/max side by side
@@ -124,14 +107,33 @@ export function Laser1ControlPage() { )}
) : ( - // For single-axis lasers: show only diameter min/max + // For single-axis lasers: show only diameter min/max with shared selector value.toFixed(3)} + timeframe={timeframe} + hideSelector /> )} + + {/* Shared timeframe selector for diameter/roundness */} +
+ {TIMEFRAME_OPTIONS.map((option) => ( + + ))} +