From 57065656829966a5d4e019b597668f5944c0bb1f Mon Sep 17 00:00:00 2001 From: Anas sarkiz Date: Thu, 5 Feb 2026 09:43:23 +0200 Subject: [PATCH] =?UTF-8?q?Add=20persistent=20custom=20laser=20profiles=20?= =?UTF-8?q?(saved=20in=20localStorage)=20with=20validated=20=E2=80=9CAdd?= =?UTF-8?q?=20Profile=E2=80=9D=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/components/laser-profile-dialog.tsx | 225 ++++++++++++++++++++++++ lib/components/numeric-control.tsx | 55 ++++++ lib/components/settings-panel.tsx | 195 ++++++++++++-------- 3 files changed, 398 insertions(+), 77 deletions(-) create mode 100644 lib/components/laser-profile-dialog.tsx create mode 100644 lib/components/numeric-control.tsx diff --git a/lib/components/laser-profile-dialog.tsx b/lib/components/laser-profile-dialog.tsx new file mode 100644 index 0000000..c6ad103 --- /dev/null +++ b/lib/components/laser-profile-dialog.tsx @@ -0,0 +1,225 @@ +import React, { useState } from "react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { NumericControl } from "./numeric-control" + +export type LaserProfile = { + copper: { + speed: number + numPasses: number + frequency: number + pulseWidth: number + } + board: { + speed: number + numPasses: number + frequency: number + pulseWidth: number + } +} + +type LaserProfileDialogProps = { + open: boolean + onOpenChange: (open: boolean) => void + initialProfile: LaserProfile + existingProfileNames: string[] + onSave: (name: string, profile: LaserProfile) => void +} + +export function LaserProfileDialog({ + open, + onOpenChange, + initialProfile, + existingProfileNames, + onSave, +}: LaserProfileDialogProps) { + const [profileName, setProfileName] = useState("") + const [profileError, setProfileError] = useState(null) + const [profileForm, setProfileForm] = useState(initialProfile) + + React.useEffect(() => { + if (!open) return + setProfileName("") + setProfileError(null) + setProfileForm(initialProfile) + }, [open, initialProfile]) + + const handleSave = () => { + const trimmedName = profileName.trim() + if (!trimmedName) { + setProfileError("Profile name is required.") + return + } + if (existingProfileNames.includes(trimmedName)) { + setProfileError("A profile with this name already exists.") + return + } + + onSave(trimmedName, profileForm) + onOpenChange(false) + } + + return ( + + + + Add Laser Profile + + Save the current laser settings as a reusable profile. + + +
+
+ +
+ { + setProfileName(e.target.value) + setProfileError(null) + }} + placeholder="e.g. 20W Copper 2-pass" + /> + {profileError && ( +
{profileError}
+ )} +
+
+ +
+
+ Copper Cutting +
+ + setProfileForm((prev) => ({ + ...prev, + copper: { ...prev.copper, speed: value }, + })) + } + label="Speed" + min={1} + unit="mm/s" + /> + + setProfileForm((prev) => ({ + ...prev, + copper: { ...prev.copper, numPasses: value }, + })) + } + label="Passes" + min={1} + unit=" " + /> + + setProfileForm((prev) => ({ + ...prev, + copper: { ...prev.copper, frequency: value }, + })) + } + label="Frequency" + min={1000} + unit="kHz" + /> + + setProfileForm((prev) => ({ + ...prev, + copper: { ...prev.copper, pulseWidth: value }, + })) + } + label="Pulse Width" + min={1} + unit="ns" + /> +
+ +
+
+ Board Cutting +
+ + setProfileForm((prev) => ({ + ...prev, + board: { ...prev.board, speed: value }, + })) + } + label="Speed" + min={1} + unit="mm/s" + /> + + setProfileForm((prev) => ({ + ...prev, + board: { ...prev.board, numPasses: value }, + })) + } + label="Passes" + min={1} + unit=" " + /> + + setProfileForm((prev) => ({ + ...prev, + board: { ...prev.board, frequency: value }, + })) + } + label="Frequency" + min={1000} + unit="kHz" + /> + + setProfileForm((prev) => ({ + ...prev, + board: { ...prev.board, pulseWidth: value }, + })) + } + label="Pulse Width" + min={1} + unit="ns" + /> +
+
+ + + + +
+
+ ) +} diff --git a/lib/components/numeric-control.tsx b/lib/components/numeric-control.tsx new file mode 100644 index 0000000..e19ff1a --- /dev/null +++ b/lib/components/numeric-control.tsx @@ -0,0 +1,55 @@ +import React, { useState } from "react" + +type NumericControlProps = { + value: number + onChange: (value: number) => void + label: string + min?: number + unit?: string +} + +export function NumericControl({ + value, + onChange, + label, + min = 0, + unit = "", +}: NumericControlProps) { + const [inputValue, setInputValue] = useState(value.toString()) + + React.useEffect(() => { + setInputValue(value.toString()) + }, [value]) + + const handleInputChange = (e: React.ChangeEvent) => { + const newValue = e.target.value + setInputValue(newValue) + } + + const handleInputBlur = () => { + const numericValue = parseFloat(inputValue) + if (Number.isNaN(numericValue) || numericValue < min) { + setInputValue(value.toString()) + } else { + onChange(Math.max(min, numericValue)) + } + } + + return ( +
+ {label} +
+ + {unit && ( + {unit} + )} +
+
+ ) +} diff --git a/lib/components/settings-panel.tsx b/lib/components/settings-panel.tsx index 04664cd..aa89313 100644 --- a/lib/components/settings-panel.tsx +++ b/lib/components/settings-panel.tsx @@ -3,8 +3,33 @@ import React, { useState, useRef, useCallback } from "react" import { Button } from "@/components/ui/button" import { Separator } from "@/components/ui/separator" import { Cpu, Layers, Settings, Upload, Zap } from "lucide-react" +import { type LaserProfile, LaserProfileDialog } from "./laser-profile-dialog" +import { NumericControl } from "./numeric-control" import { useWorkspace } from "./workspace-context" +const LASER_PROFILES_STORAGE_KEY = "pcb-burn:laser-profiles" + +const builtInLaserProfiles: Record = { + "Omni X 6W 150x150": { + copper: { speed: 300, numPasses: 1, frequency: 20, pulseWidth: 1 }, + board: { speed: 20, numPasses: 1, frequency: 20, pulseWidth: 1 }, + }, + Default: { + copper: { + speed: 300, + numPasses: 100, + frequency: 20000, + pulseWidth: 1, + }, + board: { + speed: 20, + numPasses: 100, + frequency: 20000, + pulseWidth: 1, + }, + }, +} + export function SettingsPanel() { const { circuitJson, @@ -22,29 +47,16 @@ export function SettingsPanel() { const [isResizing, setIsResizing] = useState(false) const panelRef = useRef(null) - // Laser profile presets - const laserProfiles = { - "Omni X 6W 150x150": { - copper: { speed: 300, numPasses: 1, frequency: 20, pulseWidth: 1 }, - board: { speed: 20, numPasses: 1, frequency: 20, pulseWidth: 1 }, - }, - Default: { - copper: { - speed: 300, - numPasses: 100, - frequency: 20000, - pulseWidth: 1, - }, - board: { - speed: 20, - numPasses: 100, - frequency: 20000, - pulseWidth: 1, - }, - }, - } - const [selectedProfile, setSelectedProfile] = useState("Default") + const [customProfiles, setCustomProfiles] = useState< + Record + >({}) + const [isProfileDialogOpen, setIsProfileDialogOpen] = useState(false) + + const laserProfiles = React.useMemo( + () => ({ ...builtInLaserProfiles, ...customProfiles }), + [customProfiles], + ) // Resize functionality const handleMouseDown = useCallback((e: React.MouseEvent) => { @@ -89,6 +101,75 @@ export function SettingsPanel() { } }, [isResizing, handleMouseMove, handleMouseUp]) + React.useEffect(() => { + if (typeof window === "undefined") return + try { + const storedProfiles = window.localStorage.getItem( + LASER_PROFILES_STORAGE_KEY, + ) + if (!storedProfiles) return + const parsedProfiles = JSON.parse(storedProfiles) as Record< + string, + LaserProfile + > + if (parsedProfiles && typeof parsedProfiles === "object") { + setCustomProfiles(parsedProfiles) + } + } catch (err) { + console.warn("Failed to load laser profiles", err) + } + }, []) + + React.useEffect(() => { + if (typeof window === "undefined") return + try { + window.localStorage.setItem( + LASER_PROFILES_STORAGE_KEY, + JSON.stringify(customProfiles), + ) + } catch (err) { + console.warn("Failed to save laser profiles", err) + } + }, [customProfiles]) + + React.useEffect(() => { + if (laserProfiles[selectedProfile]) return + setSelectedProfile("Default") + }, [laserProfiles, selectedProfile]) + + const handleSaveProfile = (name: string, profile: LaserProfile) => { + setCustomProfiles((prev) => ({ + ...prev, + [name]: profile, + })) + setLbrnOptions({ + ...lbrnOptions, + laserProfile: { + copper: { ...profile.copper }, + board: { ...profile.board }, + }, + }) + setSelectedProfile(name) + } + + const initialProfile = React.useMemo( + () => ({ + copper: { + speed: lbrnOptions.laserProfile?.copper?.speed ?? 300, + numPasses: lbrnOptions.laserProfile?.copper?.numPasses ?? 100, + frequency: lbrnOptions.laserProfile?.copper?.frequency ?? 20000, + pulseWidth: lbrnOptions.laserProfile?.copper?.pulseWidth ?? 1, + }, + board: { + speed: lbrnOptions.laserProfile?.board?.speed ?? 20, + numPasses: lbrnOptions.laserProfile?.board?.numPasses ?? 100, + frequency: lbrnOptions.laserProfile?.board?.frequency ?? 20000, + pulseWidth: lbrnOptions.laserProfile?.board?.pulseWidth ?? 1, + }, + }), + [lbrnOptions.laserProfile], + ) + // Load laser profile preset const loadLaserProfile = (profileName: string) => { const profile = laserProfiles[profileName as keyof typeof laserProfiles] @@ -132,61 +213,6 @@ export function SettingsPanel() { await processCircuitFile(files[0]) } } - // Helper function for numeric controls - const NumericControl = ({ - value, - onChange, - label, - min = 0, - unit = "", - }: { - value: number - onChange: (value: number) => void - label: string - min?: number - unit?: string - }) => { - const [inputValue, setInputValue] = useState(value.toString()) - - React.useEffect(() => { - setInputValue(value.toString()) - }, [value]) - - const handleInputChange = (e: React.ChangeEvent) => { - const newValue = e.target.value - setInputValue(newValue) - } - - const handleInputBlur = () => { - const numericValue = parseFloat(inputValue) - if (Number.isNaN(numericValue) || numericValue < min) { - // Reset to original value if invalid - setInputValue(value.toString()) - } else { - // Commit the valid value - onChange(Math.max(min, numericValue)) - } - } - - return ( -
- {label} -
- - {unit && ( - {unit} - )} -
-
- ) - } - return (
+ {/* Copper Settings */}
@@ -536,6 +569,14 @@ export function SettingsPanel() {
+ + ) }