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) => (
+
+ ))}
+