mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
Control page: new filament path wiring
This commit is contained in:
@@ -39,6 +39,7 @@ interface AMSPanelContentProps {
|
||||
onSelectTray: (trayId: number | null) => void;
|
||||
}
|
||||
|
||||
// Panel content - NO wiring, just slots and info
|
||||
function AMSPanelContent({
|
||||
units,
|
||||
side,
|
||||
@@ -52,12 +53,12 @@ function AMSPanelContent({
|
||||
const slotPrefix = side === 'left' ? 'A' : 'B';
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-0 overflow-visible">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-center text-[11px] font-semibold text-bambu-gray uppercase mb-2">
|
||||
{side === 'left' ? 'Left Nozzle' : 'Right Nozzle'}
|
||||
</div>
|
||||
|
||||
{/* AMS Tab Selectors - only show connected units */}
|
||||
{/* AMS Tab Selectors */}
|
||||
<div className="flex gap-1.5 mb-2.5 p-1.5 bg-bambu-dark/50 rounded-lg w-fit">
|
||||
{units.map((unit, index) => (
|
||||
<button
|
||||
@@ -86,7 +87,7 @@ function AMSPanelContent({
|
||||
|
||||
{/* AMS Content */}
|
||||
{selectedUnit && (
|
||||
<div className="bg-bambu-dark-secondary rounded-[10px] p-2.5 pb-0 overflow-visible">
|
||||
<div className="bg-bambu-dark-secondary rounded-[10px] p-2.5">
|
||||
{/* AMS Header - Humidity & Temp */}
|
||||
<div className="flex items-center gap-2.5 text-xs text-bambu-gray mb-2">
|
||||
{selectedUnit.humidity !== null && (
|
||||
@@ -117,8 +118,8 @@ function AMSPanelContent({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* AMS Slots with integrated wiring */}
|
||||
<div className="flex justify-center gap-1.5 mb-0">
|
||||
{/* AMS Slots - NO wiring here */}
|
||||
<div className="flex justify-center gap-1.5">
|
||||
{selectedUnit.tray.map((tray) => {
|
||||
const globalTrayId = selectedUnit.id * 4 + tray.id;
|
||||
const isSelected = selectedTray === globalTrayId;
|
||||
@@ -126,67 +127,42 @@ function AMSPanelContent({
|
||||
const isLight = isLightColor(tray.tray_color);
|
||||
|
||||
return (
|
||||
<div key={tray.id} className="flex flex-col items-center">
|
||||
<button
|
||||
onClick={() => !isEmpty && onSelectTray(isSelected ? null : globalTrayId)}
|
||||
disabled={isEmpty || isPrinting}
|
||||
className={`w-12 h-[70px] rounded-md border-2 overflow-hidden transition-all bg-bambu-dark ${
|
||||
isSelected
|
||||
? 'border-[#d4a84b]'
|
||||
: 'border-bambu-dark-tertiary hover:border-bambu-gray'
|
||||
} ${isEmpty ? 'opacity-50' : ''} disabled:cursor-not-allowed`}
|
||||
<button
|
||||
key={tray.id}
|
||||
onClick={() => !isEmpty && onSelectTray(isSelected ? null : globalTrayId)}
|
||||
disabled={isEmpty || isPrinting}
|
||||
className={`w-12 h-[70px] rounded-md border-2 overflow-hidden transition-all bg-bambu-dark ${
|
||||
isSelected
|
||||
? 'border-[#d4a84b]'
|
||||
: 'border-bambu-dark-tertiary hover:border-bambu-gray'
|
||||
} ${isEmpty ? 'opacity-50' : ''} disabled:cursor-not-allowed`}
|
||||
>
|
||||
<div
|
||||
className="w-full h-full flex flex-col items-center justify-end pb-[5px]"
|
||||
style={{
|
||||
backgroundColor: isEmpty ? undefined : hexToRgb(tray.tray_color),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-full h-full flex flex-col items-center justify-end pb-[5px]"
|
||||
style={{
|
||||
backgroundColor: isEmpty ? undefined : hexToRgb(tray.tray_color),
|
||||
}}
|
||||
<span
|
||||
className={`text-[11px] font-semibold mb-1 ${
|
||||
isLight ? 'text-gray-800' : 'text-white'
|
||||
} ${isLight ? '' : 'drop-shadow-sm'}`}
|
||||
>
|
||||
<span
|
||||
className={`text-[11px] font-semibold mb-1 ${
|
||||
isLight ? 'text-gray-800' : 'text-white'
|
||||
} ${isLight ? '' : 'drop-shadow-sm'}`}
|
||||
>
|
||||
{isEmpty ? '--' : tray.tray_type}
|
||||
</span>
|
||||
{!isEmpty && (
|
||||
<img
|
||||
src="/icons/eye.svg"
|
||||
alt=""
|
||||
className={`w-3.5 h-3.5 ${isLight ? '' : 'invert'}`}
|
||||
style={{ opacity: 0.8 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
{/* Vertical wire from slot center down */}
|
||||
<div className="w-[2px] h-[14px] bg-[#909090]" />
|
||||
</div>
|
||||
{isEmpty ? '--' : tray.tray_type}
|
||||
</span>
|
||||
{!isEmpty && (
|
||||
<img
|
||||
src="/icons/eye.svg"
|
||||
alt=""
|
||||
className={`w-3.5 h-3.5 ${isLight ? '' : 'invert'}`}
|
||||
style={{ opacity: 0.8 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Wiring visualization - horizontal bar and hub */}
|
||||
<div className="flex justify-center">
|
||||
<div className="relative h-[50px]" style={{ width: '210px' }}>
|
||||
{/* Horizontal bar connecting all slots (spans from first to last slot center) */}
|
||||
<div className="absolute left-[24px] right-[24px] top-0 border-t-2 border-[#909090]" />
|
||||
|
||||
{/* Center hub box on the horizontal bar */}
|
||||
<div className="absolute left-1/2 -translate-x-1/2 top-[-6px] w-[28px] h-[14px] bg-[#c0c0c0] border border-[#909090] rounded-sm" />
|
||||
|
||||
{/* Vertical wire from hub going down */}
|
||||
<div className="absolute left-1/2 -translate-x-[1px] top-[8px] h-[14px] border-l-2 border-[#909090]" />
|
||||
|
||||
{/* Horizontal wire from hub toward the center of the panel (extends beyond panel edge) */}
|
||||
{side === 'left' && (
|
||||
<div className="absolute left-1/2 top-[21px] w-[calc(50%+30px)] border-t-2 border-[#909090]" />
|
||||
)}
|
||||
{side === 'right' && (
|
||||
<div className="absolute right-1/2 top-[21px] w-[calc(50%+30px)] border-t-2 border-[#909090]" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -200,14 +176,88 @@ function AMSPanelContent({
|
||||
);
|
||||
}
|
||||
|
||||
// Unified wiring layer - draws ALL wiring in one place
|
||||
interface WiringLayerProps {
|
||||
isDualNozzle: boolean;
|
||||
}
|
||||
|
||||
function WiringLayer({ isDualNozzle }: WiringLayerProps) {
|
||||
if (!isDualNozzle) return null;
|
||||
|
||||
// All measurements relative to this container
|
||||
// Container spans full width between panels
|
||||
// Left panel wiring: slots → hub → down → right → down to extruder
|
||||
// Right panel wiring: slots → hub → down → left → down to extruder
|
||||
|
||||
return (
|
||||
<div className="relative w-full" style={{ height: '120px' }}>
|
||||
{/* SVG for all wiring - single coordinate system */}
|
||||
<svg
|
||||
className="absolute inset-0 w-full h-full"
|
||||
viewBox="0 0 600 120"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
{/* Left panel wiring */}
|
||||
{/* Vertical lines from 4 slots */}
|
||||
<line x1="68" y1="0" x2="68" y2="14" stroke="#909090" strokeWidth="2" />
|
||||
<line x1="122" y1="0" x2="122" y2="14" stroke="#909090" strokeWidth="2" />
|
||||
<line x1="176" y1="0" x2="176" y2="14" stroke="#909090" strokeWidth="2" />
|
||||
<line x1="230" y1="0" x2="230" y2="14" stroke="#909090" strokeWidth="2" />
|
||||
|
||||
{/* Horizontal bar connecting left slots */}
|
||||
<line x1="68" y1="14" x2="230" y2="14" stroke="#909090" strokeWidth="2" />
|
||||
|
||||
{/* Left hub */}
|
||||
<rect x="135" y="8" width="28" height="14" rx="2" fill="#c0c0c0" stroke="#909090" strokeWidth="1" />
|
||||
|
||||
{/* Vertical from left hub down */}
|
||||
<line x1="149" y1="22" x2="149" y2="36" stroke="#909090" strokeWidth="2" />
|
||||
|
||||
{/* Horizontal from left hub toward center */}
|
||||
<line x1="149" y1="36" x2="288" y2="36" stroke="#909090" strokeWidth="2" />
|
||||
|
||||
{/* Vertical down to left extruder inlet */}
|
||||
<line x1="288" y1="36" x2="288" y2="85" stroke="#909090" strokeWidth="2" />
|
||||
|
||||
{/* Right panel wiring */}
|
||||
{/* Vertical lines from 4 slots */}
|
||||
<line x1="370" y1="0" x2="370" y2="14" stroke="#909090" strokeWidth="2" />
|
||||
<line x1="424" y1="0" x2="424" y2="14" stroke="#909090" strokeWidth="2" />
|
||||
<line x1="478" y1="0" x2="478" y2="14" stroke="#909090" strokeWidth="2" />
|
||||
<line x1="532" y1="0" x2="532" y2="14" stroke="#909090" strokeWidth="2" />
|
||||
|
||||
{/* Horizontal bar connecting right slots */}
|
||||
<line x1="370" y1="14" x2="532" y2="14" stroke="#909090" strokeWidth="2" />
|
||||
|
||||
{/* Right hub */}
|
||||
<rect x="437" y="8" width="28" height="14" rx="2" fill="#c0c0c0" stroke="#909090" strokeWidth="1" />
|
||||
|
||||
{/* Vertical from right hub down */}
|
||||
<line x1="451" y1="22" x2="451" y2="36" stroke="#909090" strokeWidth="2" />
|
||||
|
||||
{/* Horizontal from right hub toward center */}
|
||||
<line x1="312" y1="36" x2="451" y2="36" stroke="#909090" strokeWidth="2" />
|
||||
|
||||
{/* Vertical down to right extruder inlet */}
|
||||
<line x1="312" y1="36" x2="312" y2="85" stroke="#909090" strokeWidth="2" />
|
||||
</svg>
|
||||
|
||||
{/* Extruder image - positioned at bottom center */}
|
||||
<img
|
||||
src="/icons/extruder-left-right.png"
|
||||
alt="Extruder"
|
||||
className="absolute left-1/2 -translate-x-1/2 bottom-0 h-[50px]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDualProps) {
|
||||
const isConnected = status?.connected ?? false;
|
||||
const isPrinting = status?.state === 'RUNNING';
|
||||
const isDualNozzle = nozzleCount > 1;
|
||||
const amsUnits: AMSUnit[] = status?.ams ?? [];
|
||||
|
||||
// For dual nozzle, split AMS units between left and right
|
||||
// In real implementation, this would be based on actual nozzle assignment
|
||||
const leftUnits = isDualNozzle ? amsUnits.filter((_, i) => i % 2 === 0) : amsUnits;
|
||||
const rightUnits = isDualNozzle ? amsUnits.filter((_, i) => i % 2 === 1) : [];
|
||||
|
||||
@@ -236,30 +286,9 @@ export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDua
|
||||
const isLoading = loadMutation.isPending || unloadMutation.isPending;
|
||||
|
||||
return (
|
||||
<div className="bg-bambu-dark-tertiary rounded-[10px] p-3 relative overflow-visible">
|
||||
{/* Center wiring and Extruder - absolutely centered between the two AMS panels */}
|
||||
{isDualNozzle && (
|
||||
<>
|
||||
{/* Center wiring: two vertical lines going down to extruder inlets */}
|
||||
{/* Positioned to connect with horizontal wires from AMS panels */}
|
||||
<div className="absolute left-1/2 -translate-x-1/2 bottom-[62px] pointer-events-none" style={{ width: '24px', height: '30px' }}>
|
||||
{/* Left vertical line - connects to left AMS horizontal wire, goes to left extruder inlet */}
|
||||
<div className="absolute left-0 top-0 h-full border-l-2 border-[#909090]" />
|
||||
{/* Right vertical line - connects to right AMS horizontal wire, goes to right extruder inlet */}
|
||||
<div className="absolute right-0 top-0 h-full border-l-2 border-[#909090]" />
|
||||
</div>
|
||||
{/* Extruder */}
|
||||
<img
|
||||
src="/icons/extruder-left-right.png"
|
||||
alt="Extruder"
|
||||
className="absolute h-[50px] left-1/2 -translate-x-1/2 bottom-[12px]"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Dual Panel Layout */}
|
||||
<div className="flex gap-5 overflow-visible">
|
||||
{/* Left Nozzle Panel */}
|
||||
<div className="bg-bambu-dark-tertiary rounded-[10px] p-3">
|
||||
{/* Dual Panel Layout - just the panels, no wiring */}
|
||||
<div className="flex gap-5">
|
||||
<AMSPanelContent
|
||||
units={leftUnits}
|
||||
side="left"
|
||||
@@ -270,7 +299,6 @@ export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDua
|
||||
onSelectTray={setSelectedTray}
|
||||
/>
|
||||
|
||||
{/* Right Nozzle Panel - only for dual nozzle */}
|
||||
{isDualNozzle && (
|
||||
<AMSPanelContent
|
||||
units={rightUnits}
|
||||
@@ -284,9 +312,11 @@ export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDua
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons Row with Extruder */}
|
||||
<div className="flex items-start pt-2">
|
||||
{/* Left buttons */}
|
||||
{/* Unified Wiring Layer - ALL wiring drawn here */}
|
||||
<WiringLayer isDualNozzle={isDualNozzle} />
|
||||
|
||||
{/* Action Buttons Row - aligned with extruder */}
|
||||
<div className="flex items-start -mt-[50px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="w-10 h-10 rounded-lg bg-bambu-dark-secondary hover:bg-bambu-dark border border-bambu-dark-tertiary flex items-center justify-center">
|
||||
<img src="/icons/ams-settings.svg" alt="Settings" className="w-5 icon-theme" />
|
||||
@@ -296,10 +326,8 @@ export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDua
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Right buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleUnload}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { api, isConfirmationRequired } from '../../api/client';
|
||||
import type { PrinterStatus } from '../../api/client';
|
||||
import { ChevronUp, ChevronDown } from 'lucide-react';
|
||||
import { ConfirmModal } from '../ConfirmModal';
|
||||
|
||||
interface ExtruderControlsProps {
|
||||
printerId: number;
|
||||
@@ -9,18 +11,34 @@ interface ExtruderControlsProps {
|
||||
nozzleCount: number;
|
||||
}
|
||||
|
||||
export function ExtruderControls({ status, nozzleCount }: ExtruderControlsProps) {
|
||||
export function ExtruderControls({ printerId, status, nozzleCount }: ExtruderControlsProps) {
|
||||
const isConnected = status?.connected ?? false;
|
||||
const isPrinting = status?.state === 'RUNNING' || status?.state === 'PAUSE';
|
||||
const isDualNozzle = nozzleCount > 1;
|
||||
|
||||
const [selectedNozzle, setSelectedNozzle] = useState<'left' | 'right'>('left');
|
||||
const [confirmModal, setConfirmModal] = useState<{
|
||||
token: string;
|
||||
warning: string;
|
||||
distance: number;
|
||||
} | null>(null);
|
||||
|
||||
// TODO: Add extrude/retract API calls when available
|
||||
const extrudeMutation = useMutation({
|
||||
mutationFn: async ({ distance }: { distance: number }) => {
|
||||
// Placeholder - implement when API is ready
|
||||
console.log(`Extrude ${distance}mm on ${selectedNozzle} nozzle`);
|
||||
mutationFn: ({ distance, token }: { distance: number; token?: string }) => {
|
||||
// G-code for extrusion: relative mode, extrude, back to absolute
|
||||
// T0/T1 selects the tool for dual nozzle
|
||||
const toolSelect = isDualNozzle ? `T${selectedNozzle === 'left' ? 0 : 1}\n` : '';
|
||||
const gcode = `${toolSelect}G91\nG1 E${distance} F300\nG90`;
|
||||
return api.sendGcode(printerId, gcode, token);
|
||||
},
|
||||
onSuccess: (result, variables) => {
|
||||
if (isConfirmationRequired(result)) {
|
||||
setConfirmModal({
|
||||
token: result.token,
|
||||
warning: result.warning,
|
||||
distance: variables.distance,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -28,71 +46,91 @@ export function ExtruderControls({ status, nozzleCount }: ExtruderControlsProps)
|
||||
extrudeMutation.mutate({ distance });
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (confirmModal) {
|
||||
extrudeMutation.mutate({ distance: confirmModal.distance, token: confirmModal.token });
|
||||
setConfirmModal(null);
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled = !isConnected || isPrinting || extrudeMutation.isPending;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1.5 justify-center">
|
||||
{/* Left/Right Toggle - only for dual nozzle */}
|
||||
{isDualNozzle && (
|
||||
<div className="flex rounded-md overflow-hidden border border-bambu-dark-tertiary mb-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setSelectedNozzle('left')}
|
||||
className={`px-3 py-1.5 text-sm border-r border-bambu-dark-tertiary transition-colors ${
|
||||
selectedNozzle === 'left'
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
|
||||
}`}
|
||||
>
|
||||
Left
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedNozzle('right')}
|
||||
className={`px-3 py-1.5 text-sm transition-colors ${
|
||||
selectedNozzle === 'right'
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
|
||||
}`}
|
||||
>
|
||||
Right
|
||||
</button>
|
||||
<>
|
||||
<div className="flex flex-col items-center gap-1.5 justify-center">
|
||||
{/* Left/Right Toggle - only for dual nozzle */}
|
||||
{isDualNozzle && (
|
||||
<div className="flex rounded-md overflow-hidden border border-bambu-dark-tertiary mb-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setSelectedNozzle('left')}
|
||||
className={`px-3 py-1.5 text-sm border-r border-bambu-dark-tertiary transition-colors ${
|
||||
selectedNozzle === 'left'
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
|
||||
}`}
|
||||
>
|
||||
Left
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedNozzle('right')}
|
||||
className={`px-3 py-1.5 text-sm transition-colors ${
|
||||
selectedNozzle === 'right'
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
|
||||
}`}
|
||||
>
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Extrude Up Button */}
|
||||
<button
|
||||
onClick={() => handleExtrude(5)}
|
||||
disabled={isDisabled}
|
||||
className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Extrude 5mm"
|
||||
>
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Extruder Image */}
|
||||
<div className="h-[120px] flex items-center justify-center">
|
||||
<img
|
||||
src={isDualNozzle ? "/icons/dual-extruder.png" : "/icons/single-extruder1.png"}
|
||||
alt={isDualNozzle ? "Dual Extruder" : "Single Extruder"}
|
||||
className="h-full object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Extrude Up Button */}
|
||||
<button
|
||||
onClick={() => handleExtrude(5)}
|
||||
disabled={isDisabled}
|
||||
className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Extrude 5mm"
|
||||
>
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
</button>
|
||||
{/* Retract Down Button */}
|
||||
<button
|
||||
onClick={() => handleExtrude(-5)}
|
||||
disabled={isDisabled}
|
||||
className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Retract 5mm"
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Extruder Image */}
|
||||
<div className="h-[120px] flex items-center justify-center">
|
||||
<img
|
||||
src={isDualNozzle ? "/icons/dual-extruder.png" : "/icons/single-extruder1.png"}
|
||||
alt={isDualNozzle ? "Dual Extruder" : "Single Extruder"}
|
||||
className="h-full object-contain"
|
||||
onError={(e) => {
|
||||
// Fallback if image doesn't load
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/* Label */}
|
||||
<span className="text-xs text-bambu-gray mt-0.5">Extruder</span>
|
||||
</div>
|
||||
|
||||
{/* Retract Down Button */}
|
||||
<button
|
||||
onClick={() => handleExtrude(-5)}
|
||||
disabled={isDisabled}
|
||||
className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Retract 5mm"
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Label */}
|
||||
<span className="text-xs text-bambu-gray mt-0.5">Extruder</span>
|
||||
</div>
|
||||
{/* Confirmation Modal */}
|
||||
{confirmModal && (
|
||||
<ConfirmModal
|
||||
title="Confirm Extrusion"
|
||||
message={confirmModal.warning}
|
||||
confirmText="Continue"
|
||||
variant="warning"
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={() => setConfirmModal(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { api } from '../../api/client';
|
||||
import type { PrinterStatus } from '../../api/client';
|
||||
|
||||
interface Temperatures {
|
||||
@@ -11,14 +14,102 @@ interface Temperatures {
|
||||
}
|
||||
|
||||
interface TemperatureColumnProps {
|
||||
printerId?: number;
|
||||
printerId: number;
|
||||
status: PrinterStatus | null | undefined;
|
||||
nozzleCount: number;
|
||||
}
|
||||
|
||||
export function TemperatureColumn({ status, nozzleCount }: TemperatureColumnProps) {
|
||||
type EditingField = 'nozzle' | 'nozzle_2' | 'bed' | null;
|
||||
|
||||
export function TemperatureColumn({ printerId, status, nozzleCount }: TemperatureColumnProps) {
|
||||
const temps = (status?.temperatures ?? {}) as Temperatures;
|
||||
const isDualNozzle = nozzleCount > 1;
|
||||
const isConnected = status?.connected ?? false;
|
||||
|
||||
const [editing, setEditing] = useState<EditingField>(null);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
|
||||
const bedMutation = useMutation({
|
||||
mutationFn: (target: number) => api.setBedTemperature(printerId, target),
|
||||
});
|
||||
|
||||
const nozzleMutation = useMutation({
|
||||
mutationFn: ({ target, nozzle }: { target: number; nozzle: number }) =>
|
||||
api.setNozzleTemperature(printerId, target, nozzle),
|
||||
});
|
||||
|
||||
const lightMutation = useMutation({
|
||||
mutationFn: (on: boolean) => api.setChamberLight(printerId, on),
|
||||
});
|
||||
|
||||
const startEditing = (field: EditingField, currentValue: number) => {
|
||||
if (!isConnected) return;
|
||||
setEditing(field);
|
||||
setEditValue(String(Math.round(currentValue)));
|
||||
};
|
||||
|
||||
const cancelEditing = () => {
|
||||
setEditing(null);
|
||||
setEditValue('');
|
||||
};
|
||||
|
||||
const submitEdit = () => {
|
||||
const target = parseInt(editValue, 10);
|
||||
if (isNaN(target) || target < 0) {
|
||||
cancelEditing();
|
||||
return;
|
||||
}
|
||||
|
||||
if (editing === 'bed') {
|
||||
bedMutation.mutate(target);
|
||||
} else if (editing === 'nozzle') {
|
||||
nozzleMutation.mutate({ target, nozzle: 0 });
|
||||
} else if (editing === 'nozzle_2') {
|
||||
nozzleMutation.mutate({ target, nozzle: 1 });
|
||||
}
|
||||
cancelEditing();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
submitEdit();
|
||||
} else if (e.key === 'Escape') {
|
||||
cancelEditing();
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled = !isConnected;
|
||||
|
||||
const renderTargetTemp = (
|
||||
field: EditingField,
|
||||
targetValue: number
|
||||
) => {
|
||||
if (editing === field) {
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onBlur={submitEdit}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
className="w-12 text-sm bg-bambu-dark border border-bambu-green rounded px-1 py-0.5 text-white text-center"
|
||||
min={0}
|
||||
max={350}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
onClick={() => startEditing(field, targetValue)}
|
||||
disabled={isDisabled}
|
||||
className="text-sm text-bambu-gray hover:text-bambu-green disabled:hover:text-bambu-gray disabled:cursor-not-allowed"
|
||||
title={isDisabled ? 'Printer not connected' : 'Click to set target temperature'}
|
||||
>
|
||||
/{Math.round(targetValue)} °C
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-evenly min-w-[150px] pr-5 border-r border-bambu-dark-tertiary">
|
||||
@@ -33,7 +124,7 @@ export function TemperatureColumn({ status, nozzleCount }: TemperatureColumnProp
|
||||
</span>
|
||||
)}
|
||||
<span className="text-lg font-medium text-white">{Math.round(temps.nozzle ?? 0)}</span>
|
||||
<span className="text-sm text-bambu-gray">/{Math.round(temps.nozzle_target ?? 0)} °C</span>
|
||||
{renderTargetTemp('nozzle', temps.nozzle_target ?? 0)}
|
||||
</div>
|
||||
|
||||
{/* Nozzle 2 (Right) - only for dual nozzle */}
|
||||
@@ -46,7 +137,7 @@ export function TemperatureColumn({ status, nozzleCount }: TemperatureColumnProp
|
||||
R
|
||||
</span>
|
||||
<span className="text-lg font-medium text-white">{Math.round(temps.nozzle_2 ?? 0)}</span>
|
||||
<span className="text-sm text-bambu-gray">/{Math.round(temps.nozzle_2_target ?? 0)} °C</span>
|
||||
{renderTargetTemp('nozzle_2', temps.nozzle_2_target ?? 0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -55,38 +146,42 @@ export function TemperatureColumn({ status, nozzleCount }: TemperatureColumnProp
|
||||
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
|
||||
<img src="/icons/heatbed.svg" alt="" className="w-5 icon-theme" />
|
||||
</div>
|
||||
{/* Spacer to align with L/R badge (min-w-[18px]) */}
|
||||
{isDualNozzle && <span className="min-w-[18px] flex-shrink-0" />}
|
||||
<span className="text-lg font-medium text-white">{Math.round(temps.bed ?? 0)}</span>
|
||||
<span className="text-sm text-bambu-gray">/{Math.round(temps.bed_target ?? 0)} °C</span>
|
||||
{renderTargetTemp('bed', temps.bed_target ?? 0)}
|
||||
</div>
|
||||
|
||||
{/* Chamber */}
|
||||
{/* Chamber - read only */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
|
||||
<img src="/icons/chamber.svg" alt="" className="w-5 icon-theme" />
|
||||
</div>
|
||||
{/* Spacer to align with L/R badge */}
|
||||
{isDualNozzle && <span className="min-w-[18px] flex-shrink-0" />}
|
||||
<span className="text-lg font-medium text-white">{Math.round(temps.chamber ?? 0)}</span>
|
||||
<span className="text-sm text-bambu-gray">/{0} °C</span>
|
||||
<span className="text-sm text-bambu-gray">°C</span>
|
||||
</div>
|
||||
|
||||
{/* Air Condition - button */}
|
||||
<button className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
||||
<button
|
||||
disabled={isDisabled}
|
||||
className="flex items-center gap-2 hover:opacity-80 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
|
||||
<img src="/icons/ventilation.svg" alt="" className="w-5 icon-theme" />
|
||||
</div>
|
||||
<span className="text-sm text-bambu-gray">Air Condition</span>
|
||||
</button>
|
||||
|
||||
{/* Lamp - button */}
|
||||
<button className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
||||
{/* Lamp - button (toggle, state not tracked in status yet) */}
|
||||
<button
|
||||
onClick={() => lightMutation.mutate(true)}
|
||||
disabled={isDisabled || lightMutation.isPending}
|
||||
className="flex items-center gap-2 hover:opacity-80 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
|
||||
<img src="/icons/ventilation.svg" alt="" className="w-4 icon-theme" />
|
||||
<img src="/icons/lamp.svg" alt="" className="w-4 icon-theme" />
|
||||
</div>
|
||||
<span className="text-sm text-bambu-gray">Lamp</span>
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-bambu-green" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
|
||||
<script type="module" crossorigin src="/assets/index-BRHFyg0M.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BzyUcjxL.css">
|
||||
<script type="module" crossorigin src="/assets/index-Dg2N9nGX.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Da3EoG5B.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user