mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 07:25:44 +02:00
Wired motion and extruder controls
This commit is contained in:
@@ -105,6 +105,10 @@ class SpeedRequest(BaseModel):
|
||||
mode: int = Field(..., ge=1, le=4, description="Speed mode: 1=silent, 2=standard, 3=sport, 4=ludicrous")
|
||||
|
||||
|
||||
class ExtruderRequest(BaseModel):
|
||||
extruder: int = Field(..., ge=0, le=1, description="Extruder index (0=right, 1=left for H2D)")
|
||||
|
||||
|
||||
class FanRequest(BaseModel):
|
||||
speed: int = Field(..., ge=0, le=100, description="Fan speed percentage (0-100)")
|
||||
|
||||
@@ -324,6 +328,28 @@ async def set_print_speed(
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Extruder Control Endpoint
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/{printer_id}/control/extruder", response_model=ControlResponse)
|
||||
async def select_extruder(
|
||||
printer_id: int,
|
||||
request: ExtruderRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Select the active extruder for dual-nozzle printers."""
|
||||
await get_printer_or_404(printer_id, db)
|
||||
client = get_mqtt_client_or_503(printer_id)
|
||||
|
||||
extruder_names = {0: "Right", 1: "Left"}
|
||||
success = client.select_extruder(request.extruder)
|
||||
return ControlResponse(
|
||||
success=success,
|
||||
message=f"Selected {extruder_names[request.extruder]} extruder" if success else "Failed to select extruder"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Fan Control Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@@ -244,6 +244,7 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
airduct_mode=state.airduct_mode,
|
||||
speed_level=state.speed_level,
|
||||
chamber_light=state.chamber_light,
|
||||
active_extruder=state.active_extruder,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -118,3 +118,5 @@ class PrinterStatus(BaseModel):
|
||||
speed_level: int = 2
|
||||
# Chamber light on/off
|
||||
chamber_light: bool = False
|
||||
# Active extruder for dual nozzle (0=right, 1=left)
|
||||
active_extruder: int = 0
|
||||
|
||||
@@ -109,6 +109,8 @@ class PrinterState:
|
||||
speed_level: int = 2
|
||||
# Chamber light on/off
|
||||
chamber_light: bool = False
|
||||
# Active extruder for dual nozzle (0=right, 1=left) - from device.extruder.info[X].hnow
|
||||
active_extruder: int = 0
|
||||
|
||||
|
||||
# Stage name mapping from BambuStudio DeviceManager.cpp
|
||||
@@ -278,6 +280,10 @@ class BambuMQTTClient:
|
||||
# Track last message time - receiving a message proves we're connected
|
||||
self._last_message_time = time.time()
|
||||
self.state.connected = True
|
||||
# TEMP: Dump full payload once to find extruder state field
|
||||
if not hasattr(self, '_payload_dumped'):
|
||||
self._payload_dumped = True
|
||||
logger.info(f"[{self.serial_number}] FULL MQTT PAYLOAD DUMP:\n{json.dumps(payload, indent=2)}")
|
||||
# Log message if logging is enabled
|
||||
if self._logging_enabled:
|
||||
self._message_log.append(MQTTLogEntry(
|
||||
@@ -647,6 +653,33 @@ class BambuMQTTClient:
|
||||
if nozzle_fields and not hasattr(self, '_nozzle_fields_logged'):
|
||||
logger.info(f"[{self.serial_number}] Nozzle/hardware fields in MQTT data: {nozzle_fields}")
|
||||
self._nozzle_fields_logged = True
|
||||
# Parse active extruder from device.extruder.state bit 8
|
||||
# bit 8 = 0 → RIGHT extruder (active_extruder=0)
|
||||
# bit 8 = 1 → LEFT extruder (active_extruder=1)
|
||||
if "device" in data and isinstance(data.get("device"), dict):
|
||||
device = data["device"]
|
||||
if "extruder" in device and "state" in device["extruder"]:
|
||||
state_val = device["extruder"]["state"]
|
||||
# Extract bit 8 for extruder position
|
||||
new_extruder = (state_val >> 8) & 0x1
|
||||
if new_extruder != self.state.active_extruder:
|
||||
logger.info(f"[{self.serial_number}] ACTIVE EXTRUDER CHANGED (state bit 8): {self.state.active_extruder} -> {new_extruder} (0=right, 1=left) [state={state_val}]")
|
||||
self.state.active_extruder = new_extruder
|
||||
|
||||
# Log device.extruder structure for active extruder
|
||||
if "device" in data and isinstance(data.get("device"), dict):
|
||||
device = data["device"]
|
||||
if "extruder" in device:
|
||||
ext_data = device["extruder"]
|
||||
# Log 'state' field - OrcaSlicer uses bits 12-14 for switch state
|
||||
if "state" in ext_data:
|
||||
state_val = ext_data["state"]
|
||||
# Extract bits 12-14 (3 bits) for switch state
|
||||
switch_state = (state_val >> 12) & 0x7
|
||||
logger.info(f"[{self.serial_number}] device.extruder.state={state_val} (switch_state bits 12-14: {switch_state})")
|
||||
# Log 'cur' field if present (might indicate current/active extruder)
|
||||
if "cur" in ext_data:
|
||||
logger.info(f"[{self.serial_number}] device.extruder.cur: {ext_data['cur']}")
|
||||
if "bed_temper" in data:
|
||||
temps["bed"] = float(data["bed_temper"])
|
||||
if "bed_target_temper" in data:
|
||||
@@ -2058,6 +2091,39 @@ class BambuMQTTClient:
|
||||
logger.info(f"[{self.serial_number}] Set chamber lights {'on' if on else 'off'} (seq={self._sequence_id})")
|
||||
return True
|
||||
|
||||
def select_extruder(self, extruder: int) -> bool:
|
||||
"""Select the active extruder for dual-nozzle printers (H2D).
|
||||
|
||||
Args:
|
||||
extruder: Extruder index (0=right, 1=left for H2D)
|
||||
|
||||
Returns:
|
||||
True if command was sent, False otherwise
|
||||
"""
|
||||
if extruder not in (0, 1):
|
||||
logger.warning(f"[{self.serial_number}] Invalid extruder: {extruder}")
|
||||
return False
|
||||
|
||||
if not self._client or not self.state.connected:
|
||||
logger.warning(f"[{self.serial_number}] Cannot switch extruder: not connected")
|
||||
return False
|
||||
|
||||
# H2D extruder switching via select_extruder command
|
||||
# Command format captured from OrcaSlicer:
|
||||
# {"print": {"command": "select_extruder", "extruder_index": 0, "sequence_id": "..."}}
|
||||
# extruder_index: 0 = RIGHT, 1 = LEFT
|
||||
self._sequence_id += 1
|
||||
command = {
|
||||
"print": {
|
||||
"command": "select_extruder",
|
||||
"extruder_index": extruder,
|
||||
"sequence_id": str(self._sequence_id)
|
||||
}
|
||||
}
|
||||
self._client.publish(self.topic_publish, json.dumps(command), qos=1)
|
||||
logger.info(f"[{self.serial_number}] Sent select_extruder command: extruder_index={extruder} (0=right, 1=left)")
|
||||
return True
|
||||
|
||||
def home_axes(self, axes: str = "XYZ") -> bool:
|
||||
"""Home the specified axes.
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -121,6 +121,8 @@ export interface PrinterStatus {
|
||||
speed_level: number;
|
||||
// Chamber light on/off
|
||||
chamber_light: boolean;
|
||||
// Active extruder for dual nozzle (0=right, 1=left)
|
||||
active_extruder: number;
|
||||
}
|
||||
|
||||
export interface PrinterCreate {
|
||||
@@ -1179,6 +1181,11 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ on }),
|
||||
}),
|
||||
selectExtruder: (printerId: number, extruder: number) =>
|
||||
request<ControlResponse>(`/printers/${printerId}/control/extruder`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ extruder }),
|
||||
}),
|
||||
homeAxes: (printerId: number, axes = 'XYZ', confirmToken?: string) =>
|
||||
request<ControlResult>(`/printers/${printerId}/control/home`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -50,35 +50,35 @@ export function BedControls({ printerId, status, disabled = false }: BedControls
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleMove(10)}
|
||||
onClick={() => handleMove(-10)}
|
||||
disabled={isDisabled}
|
||||
className="px-3.5 py-2 rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Z+10"
|
||||
title="Bed up 10mm"
|
||||
>
|
||||
↑10
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleMove(1)}
|
||||
onClick={() => handleMove(-1)}
|
||||
disabled={isDisabled}
|
||||
className="px-3.5 py-2 rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Z+1"
|
||||
title="Bed up 1mm"
|
||||
>
|
||||
↑1
|
||||
</button>
|
||||
<span className="px-2 py-2 text-sm text-bambu-gray">Bed</span>
|
||||
<button
|
||||
onClick={() => handleMove(-1)}
|
||||
onClick={() => handleMove(1)}
|
||||
disabled={isDisabled}
|
||||
className="px-3.5 py-2 rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Z-1"
|
||||
title="Bed down 1mm"
|
||||
>
|
||||
↓1
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleMove(-10)}
|
||||
onClick={() => handleMove(10)}
|
||||
disabled={isDisabled}
|
||||
className="px-3.5 py-2 rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Z-10"
|
||||
title="Bed down 10mm"
|
||||
>
|
||||
↓10
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api, isConfirmationRequired } from '../../api/client';
|
||||
import type { PrinterStatus } from '../../api/client';
|
||||
import { ChevronUp, ChevronDown } from 'lucide-react';
|
||||
@@ -13,23 +13,43 @@ interface ExtruderControlsProps {
|
||||
}
|
||||
|
||||
export function ExtruderControls({ printerId, status, nozzleCount, disabled = false }: ExtruderControlsProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const isConnected = (status?.connected ?? false) && !disabled;
|
||||
const isPrinting = status?.state === 'RUNNING' || status?.state === 'PAUSE';
|
||||
const isDualNozzle = nozzleCount > 1;
|
||||
|
||||
const [selectedNozzle, setSelectedNozzle] = useState<'left' | 'right'>('left');
|
||||
// Active extruder from live status: 0=right, 1=left
|
||||
const activeExtruder = status?.active_extruder ?? 0;
|
||||
|
||||
const [confirmModal, setConfirmModal] = useState<{
|
||||
token: string;
|
||||
warning: string;
|
||||
distance: number;
|
||||
} | null>(null);
|
||||
|
||||
const selectExtruderMutation = useMutation({
|
||||
mutationFn: (extruder: number) => {
|
||||
console.log('selectExtruder called with:', extruder);
|
||||
return api.selectExtruder(printerId, extruder);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
console.log('selectExtruder success:', data);
|
||||
// Invalidate printer statuses to refresh the active extruder display
|
||||
// Add a small delay to allow the printer to process the switch and MQTT to update
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['printerStatuses'] });
|
||||
}, 2000);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('selectExtruder error:', error);
|
||||
},
|
||||
});
|
||||
|
||||
const extrudeMutation = useMutation({
|
||||
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`;
|
||||
// Uses currently active extruder
|
||||
const gcode = `G91\nG1 E${distance} F300\nG90`;
|
||||
return api.sendGcode(printerId, gcode, token);
|
||||
},
|
||||
onSuccess: (result, variables) => {
|
||||
@@ -55,18 +75,26 @@ export function ExtruderControls({ printerId, status, nozzleCount, disabled = fa
|
||||
};
|
||||
|
||||
const isDisabled = !isConnected || isPrinting || extrudeMutation.isPending;
|
||||
const isSwitching = selectExtruderMutation.isPending;
|
||||
|
||||
// Get extruder image based on active state
|
||||
const getExtruderImage = () => {
|
||||
if (!isDualNozzle) return "/icons/single-extruder1.png";
|
||||
// activeExtruder: 0=right, 1=left
|
||||
return activeExtruder === 1 ? "/icons/dual-extruder-left.png" : "/icons/dual-extruder-right.png";
|
||||
};
|
||||
|
||||
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 ${isDisabled ? 'opacity-50' : ''}`}>
|
||||
<div className={`flex rounded-md overflow-hidden border border-bambu-dark-tertiary mb-1 flex-shrink-0 ${isDisabled || isSwitching ? 'opacity-50' : ''}`}>
|
||||
<button
|
||||
onClick={() => setSelectedNozzle('left')}
|
||||
disabled={isDisabled}
|
||||
onClick={() => selectExtruderMutation.mutate(1)}
|
||||
disabled={isDisabled || isSwitching}
|
||||
className={`px-3 py-1.5 text-sm border-r border-bambu-dark-tertiary transition-colors disabled:cursor-not-allowed ${
|
||||
selectedNozzle === 'left'
|
||||
activeExtruder === 1
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary disabled:hover:bg-bambu-dark-secondary'
|
||||
}`}
|
||||
@@ -74,10 +102,10 @@ export function ExtruderControls({ printerId, status, nozzleCount, disabled = fa
|
||||
Left
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedNozzle('right')}
|
||||
disabled={isDisabled}
|
||||
onClick={() => selectExtruderMutation.mutate(0)}
|
||||
disabled={isDisabled || isSwitching}
|
||||
className={`px-3 py-1.5 text-sm transition-colors disabled:cursor-not-allowed ${
|
||||
selectedNozzle === 'right'
|
||||
activeExtruder === 0
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary disabled:hover:bg-bambu-dark-secondary'
|
||||
}`}
|
||||
@@ -100,8 +128,8 @@ export function ExtruderControls({ printerId, status, nozzleCount, disabled = fa
|
||||
{/* 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"}
|
||||
src={getExtruderImage()}
|
||||
alt={isDualNozzle ? `${activeExtruder === 1 ? 'Left' : 'Right'} Extruder Active` : "Single Extruder"}
|
||||
className="h-full object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { api, isConfirmationRequired } from '../../api/client';
|
||||
import type { PrinterStatus } from '../../api/client';
|
||||
import { useState } from 'react';
|
||||
import { useState, useId } from 'react';
|
||||
import { ConfirmModal } from '../ConfirmModal';
|
||||
|
||||
interface JogPadProps {
|
||||
@@ -10,8 +10,57 @@ interface JogPadProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// Image map coordinates for 220x220 jog pad
|
||||
// The jog pad has concentric rings: outer (10mm), inner (1mm), center (home)
|
||||
const SIZE = 220;
|
||||
const CENTER = SIZE / 2; // 110
|
||||
|
||||
// Ring radii (approximate based on typical jog pad design)
|
||||
const OUTER_RADIUS = 108; // Outer edge
|
||||
const OUTER_INNER = 72; // Inner edge of outer ring (10mm zone)
|
||||
const INNER_INNER = 35; // Inner edge of inner ring (1mm zone)
|
||||
const HOME_RADIUS = 28; // Home button radius
|
||||
|
||||
// Generate polygon points for a ring segment (pie slice)
|
||||
function ringSegment(
|
||||
cx: number, cy: number,
|
||||
innerR: number, outerR: number,
|
||||
startAngle: number, endAngle: number,
|
||||
steps: number = 8
|
||||
): string {
|
||||
const points: string[] = [];
|
||||
|
||||
// Outer arc (clockwise)
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const angle = startAngle + (endAngle - startAngle) * (i / steps);
|
||||
const x = Math.round(cx + outerR * Math.cos(angle));
|
||||
const y = Math.round(cy + outerR * Math.sin(angle));
|
||||
points.push(`${x},${y}`);
|
||||
}
|
||||
|
||||
// Inner arc (counter-clockwise)
|
||||
for (let i = steps; i >= 0; i--) {
|
||||
const angle = startAngle + (endAngle - startAngle) * (i / steps);
|
||||
const x = Math.round(cx + innerR * Math.cos(angle));
|
||||
const y = Math.round(cy + innerR * Math.sin(angle));
|
||||
points.push(`${x},${y}`);
|
||||
}
|
||||
|
||||
return points.join(',');
|
||||
}
|
||||
|
||||
// Angle definitions (in radians, 0 = right, going clockwise)
|
||||
// Each direction covers 90 degrees (π/2), offset by 45 degrees (π/4)
|
||||
const ANGLES = {
|
||||
up: { start: -Math.PI * 3/4, end: -Math.PI / 4 }, // Top: -135° to -45°
|
||||
right: { start: -Math.PI / 4, end: Math.PI / 4 }, // Right: -45° to 45°
|
||||
down: { start: Math.PI / 4, end: Math.PI * 3/4 }, // Bottom: 45° to 135°
|
||||
left: { start: Math.PI * 3/4, end: Math.PI * 5/4 }, // Left: 135° to 225° (or -135°)
|
||||
};
|
||||
|
||||
export function JogPad({ printerId, status, disabled = false }: JogPadProps) {
|
||||
const isConnected = (status?.connected ?? false) && !disabled;
|
||||
const mapId = useId();
|
||||
|
||||
const [confirmModal, setConfirmModal] = useState<{
|
||||
action: string;
|
||||
@@ -56,10 +105,12 @@ export function JogPad({ printerId, status, disabled = false }: JogPadProps) {
|
||||
});
|
||||
|
||||
const handleHome = () => {
|
||||
if (isDisabled) return;
|
||||
homeMutation.mutate({ axes: 'XY' });
|
||||
};
|
||||
|
||||
const handleMove = (axis: string, distance: number) => {
|
||||
if (isDisabled) return;
|
||||
moveMutation.mutate({ axis, distance });
|
||||
};
|
||||
|
||||
@@ -73,76 +124,94 @@ export function JogPad({ printerId, status, disabled = false }: JogPadProps) {
|
||||
const isLoading = homeMutation.isPending || moveMutation.isPending;
|
||||
const isDisabled = !isConnected || isLoading;
|
||||
|
||||
// Generate coordinates for circle (home button)
|
||||
const homeCoords = Array.from({ length: 16 }, (_, i) => {
|
||||
const angle = (i / 16) * Math.PI * 2;
|
||||
const x = Math.round(CENTER + HOME_RADIUS * Math.cos(angle));
|
||||
const y = Math.round(CENTER + HOME_RADIUS * Math.sin(angle));
|
||||
return `${x},${y}`;
|
||||
}).join(',');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative w-[220px] h-[220px] mb-3.5">
|
||||
{/* Use the actual jogpad.svg from mockup */}
|
||||
<img
|
||||
src="/icons/jogpad.svg"
|
||||
alt="Jog Pad"
|
||||
useMap={`#${mapId}`}
|
||||
className="w-full h-full jogpad-theme"
|
||||
/>
|
||||
|
||||
{/* Invisible clickable areas overlaid on the SVG */}
|
||||
{/* Outer ring - 10mm moves */}
|
||||
<button
|
||||
onClick={() => handleMove('Y', 10)}
|
||||
disabled={isDisabled}
|
||||
className="absolute top-[8px] left-1/2 -translate-x-1/2 w-[40px] h-[30px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
|
||||
title="Y+10"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleMove('Y', -10)}
|
||||
disabled={isDisabled}
|
||||
className="absolute bottom-[8px] left-1/2 -translate-x-1/2 w-[40px] h-[30px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
|
||||
title="Y-10"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleMove('X', -10)}
|
||||
disabled={isDisabled}
|
||||
className="absolute left-[8px] top-1/2 -translate-y-1/2 w-[30px] h-[40px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
|
||||
title="X-10"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleMove('X', 10)}
|
||||
disabled={isDisabled}
|
||||
className="absolute right-[8px] top-1/2 -translate-y-1/2 w-[30px] h-[40px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
|
||||
title="X+10"
|
||||
/>
|
||||
<map name={mapId}>
|
||||
{/* Outer ring - 10mm moves */}
|
||||
<area
|
||||
shape="poly"
|
||||
coords={ringSegment(CENTER, CENTER, OUTER_INNER, OUTER_RADIUS, ANGLES.up.start, ANGLES.up.end)}
|
||||
onClick={() => handleMove('Y', 10)}
|
||||
title="Y+10mm"
|
||||
style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
<area
|
||||
shape="poly"
|
||||
coords={ringSegment(CENTER, CENTER, OUTER_INNER, OUTER_RADIUS, ANGLES.down.start, ANGLES.down.end)}
|
||||
onClick={() => handleMove('Y', -10)}
|
||||
title="Y-10mm"
|
||||
style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
<area
|
||||
shape="poly"
|
||||
coords={ringSegment(CENTER, CENTER, OUTER_INNER, OUTER_RADIUS, ANGLES.left.start, ANGLES.left.end)}
|
||||
onClick={() => handleMove('X', -10)}
|
||||
title="X-10mm"
|
||||
style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
<area
|
||||
shape="poly"
|
||||
coords={ringSegment(CENTER, CENTER, OUTER_INNER, OUTER_RADIUS, ANGLES.right.start, ANGLES.right.end)}
|
||||
onClick={() => handleMove('X', 10)}
|
||||
title="X+10mm"
|
||||
style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
|
||||
{/* Inner ring - 1mm moves */}
|
||||
<button
|
||||
onClick={() => handleMove('Y', 1)}
|
||||
disabled={isDisabled}
|
||||
className="absolute top-[42px] left-1/2 -translate-x-1/2 w-[35px] h-[25px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
|
||||
title="Y+1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleMove('Y', -1)}
|
||||
disabled={isDisabled}
|
||||
className="absolute bottom-[42px] left-1/2 -translate-x-1/2 w-[35px] h-[25px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
|
||||
title="Y-1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleMove('X', -1)}
|
||||
disabled={isDisabled}
|
||||
className="absolute left-[42px] top-1/2 -translate-y-1/2 w-[25px] h-[35px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
|
||||
title="X-1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleMove('X', 1)}
|
||||
disabled={isDisabled}
|
||||
className="absolute right-[42px] top-1/2 -translate-y-1/2 w-[25px] h-[35px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
|
||||
title="X+1"
|
||||
/>
|
||||
{/* Inner ring - 1mm moves */}
|
||||
<area
|
||||
shape="poly"
|
||||
coords={ringSegment(CENTER, CENTER, INNER_INNER, OUTER_INNER, ANGLES.up.start, ANGLES.up.end)}
|
||||
onClick={() => handleMove('Y', 1)}
|
||||
title="Y+1mm"
|
||||
style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
<area
|
||||
shape="poly"
|
||||
coords={ringSegment(CENTER, CENTER, INNER_INNER, OUTER_INNER, ANGLES.down.start, ANGLES.down.end)}
|
||||
onClick={() => handleMove('Y', -1)}
|
||||
title="Y-1mm"
|
||||
style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
<area
|
||||
shape="poly"
|
||||
coords={ringSegment(CENTER, CENTER, INNER_INNER, OUTER_INNER, ANGLES.left.start, ANGLES.left.end)}
|
||||
onClick={() => handleMove('X', -1)}
|
||||
title="X-1mm"
|
||||
style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
<area
|
||||
shape="poly"
|
||||
coords={ringSegment(CENTER, CENTER, INNER_INNER, OUTER_INNER, ANGLES.right.start, ANGLES.right.end)}
|
||||
onClick={() => handleMove('X', 1)}
|
||||
title="X+1mm"
|
||||
style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
|
||||
{/* Home button in center - clickable overlay */}
|
||||
<button
|
||||
onClick={handleHome}
|
||||
disabled={isDisabled}
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[50px] h-[50px] rounded-full opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
|
||||
title="Home XY"
|
||||
/>
|
||||
{/* Center - Home button */}
|
||||
<area
|
||||
shape="poly"
|
||||
coords={homeCoords}
|
||||
onClick={handleHome}
|
||||
title="Home XY"
|
||||
style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
</map>
|
||||
</div>
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
|
||||
@@ -63,7 +63,7 @@ export function SpeedModal({ printer, status, onClose }: SpeedModalProps) {
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="relative w-full max-w-md bg-bambu-dark-secondary rounded-2xl shadow-2xl border border-bambu-dark-tertiary overflow-hidden"
|
||||
className="relative w-full max-w-lg bg-bambu-dark-secondary rounded-2xl shadow-2xl border border-bambu-dark-tertiary overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
|
||||
@@ -120,5 +120,5 @@ body {
|
||||
}
|
||||
|
||||
.dark .jogpad-theme {
|
||||
filter: brightness(0.7) contrast(1.1);
|
||||
filter: brightness(0.35) contrast(1.2);
|
||||
}
|
||||
|
||||
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
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
+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-BGXbFbC2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DyjgQh1m.css">
|
||||
<script type="module" crossorigin src="/assets/index-KY24LvJj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BAV1KG6Z.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user