diff --git a/README.md b/README.md
index 3f4899375..289b5af18 100644
--- a/README.md
+++ b/README.md
@@ -505,10 +505,6 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
---
-If you like Bambuddy and want to support it, you can buy Martin a coffee.
-
----
-
## 📄 License
MIT License — see [LICENSE](LICENSE) for details.
@@ -523,6 +519,10 @@ MIT License — see [LICENSE](LICENSE) for details.
---
+If you like Bambuddy and want to support it, you can buy Martin a coffee.
+
+---
+
Made with ❤️ for the 3D printing community
diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py
index 8aff35dcb..85241c422 100644
--- a/backend/app/api/routes/archives.py
+++ b/backend/app/api/routes/archives.py
@@ -78,6 +78,7 @@ def archive_to_response(
"nozzle_diameter": archive.nozzle_diameter,
"bed_temperature": archive.bed_temperature,
"nozzle_temperature": archive.nozzle_temperature,
+ "sliced_for_model": archive.sliced_for_model,
"status": archive.status,
"started_at": archive.started_at,
"completed_at": archive.completed_at,
diff --git a/backend/app/api/routes/print_queue.py b/backend/app/api/routes/print_queue.py
index a8ed36a0f..f74e21749 100644
--- a/backend/app/api/routes/print_queue.py
+++ b/backend/app/api/routes/print_queue.py
@@ -42,6 +42,7 @@ def _enrich_response(item: PrintQueueItem) -> PrintQueueItemResponse:
item_dict = {
"id": item.id,
"printer_id": item.printer_id,
+ "target_model": item.target_model,
"archive_id": item.archive_id,
"library_file_id": item.library_file_id,
"position": item.position,
@@ -124,12 +125,24 @@ async def add_to_queue(
if not data.archive_id and not data.library_file_id:
raise HTTPException(400, "Either archive_id or library_file_id must be provided")
+ # Cannot specify both printer_id and target_model
+ if data.printer_id and data.target_model:
+ raise HTTPException(400, "Cannot specify both printer_id and target_model")
+
# Validate printer exists (if assigned)
if data.printer_id is not None:
result = await db.execute(select(Printer).where(Printer.id == data.printer_id))
if not result.scalar_one_or_none():
raise HTTPException(400, "Printer not found")
+ # Validate target_model has active printers
+ if data.target_model:
+ result = await db.execute(
+ select(Printer).where(Printer.model == data.target_model).where(Printer.is_active == True) # noqa: E712
+ )
+ if not result.scalars().first():
+ raise HTTPException(400, f"No active printers for model: {data.target_model}")
+
# Validate archive exists (if provided)
if data.archive_id:
result = await db.execute(select(PrintArchive).where(PrintArchive.id == data.archive_id))
@@ -142,7 +155,7 @@ async def add_to_queue(
if not result.scalar_one_or_none():
raise HTTPException(400, "Library file not found")
- # Get next position for this printer (or for unassigned items)
+ # Get next position for this printer (or for unassigned/model-based items)
if data.printer_id is not None:
result = await db.execute(
select(func.max(PrintQueueItem.position))
@@ -150,7 +163,7 @@ async def add_to_queue(
.where(PrintQueueItem.status == "pending")
)
else:
- # For unassigned items, get max position across all unassigned
+ # For unassigned/model-based items, get max position across all unassigned
result = await db.execute(
select(func.max(PrintQueueItem.position))
.where(PrintQueueItem.printer_id.is_(None))
@@ -160,6 +173,7 @@ async def add_to_queue(
item = PrintQueueItem(
printer_id=data.printer_id,
+ target_model=data.target_model,
archive_id=data.archive_id,
library_file_id=data.library_file_id,
scheduled_time=data.scheduled_time,
@@ -185,7 +199,8 @@ async def add_to_queue(
await db.refresh(item, ["archive", "printer", "library_file"])
source_name = f"archive {data.archive_id}" if data.archive_id else f"library file {data.library_file_id}"
- logger.info(f"Added {source_name} to queue for printer {data.printer_id or 'unassigned'}")
+ target_desc = data.printer_id or (f"model {data.target_model}" if data.target_model else "unassigned")
+ logger.info(f"Added {source_name} to queue for {target_desc}")
# MQTT relay - publish queue job added
try:
@@ -287,12 +302,26 @@ async def update_queue_item(
update_data = data.model_dump(exclude_unset=True)
+ # Cannot specify both printer_id and target_model
+ new_printer_id = update_data.get("printer_id", item.printer_id)
+ new_target_model = update_data.get("target_model", item.target_model)
+ if new_printer_id and new_target_model:
+ raise HTTPException(400, "Cannot specify both printer_id and target_model")
+
# Validate new printer_id if being changed (and not None)
if "printer_id" in update_data and update_data["printer_id"] is not None:
result = await db.execute(select(Printer).where(Printer.id == update_data["printer_id"]))
if not result.scalar_one_or_none():
raise HTTPException(400, "Printer not found")
+ # Validate target_model has active printers
+ if "target_model" in update_data and update_data["target_model"]:
+ result = await db.execute(
+ select(Printer).where(Printer.model == update_data["target_model"]).where(Printer.is_active == True) # noqa: E712
+ )
+ if not result.scalars().first():
+ raise HTTPException(400, f"No active printers for model: {update_data['target_model']}")
+
# Serialize ams_mapping to JSON for TEXT column storage
if "ams_mapping" in update_data:
update_data["ams_mapping"] = json.dumps(update_data["ams_mapping"]) if update_data["ams_mapping"] else None
diff --git a/backend/app/core/database.py b/backend/app/core/database.py
index 4b2506d76..ca4fb74b8 100644
--- a/backend/app/core/database.py
+++ b/backend/app/core/database.py
@@ -760,6 +760,18 @@ async def run_migrations(conn):
except Exception:
pass
+ # Migration: Add sliced_for_model column to print_archives for printer model from 3MF
+ try:
+ await conn.execute(text("ALTER TABLE print_archives ADD COLUMN sliced_for_model VARCHAR(50)"))
+ except Exception:
+ pass
+
+ # Migration: Add target_model column to print_queue for model-based assignment
+ try:
+ await conn.execute(text("ALTER TABLE print_queue ADD COLUMN target_model VARCHAR(50)"))
+ except Exception:
+ pass
+
async def seed_notification_templates():
"""Seed default notification templates if they don't exist."""
diff --git a/backend/app/models/archive.py b/backend/app/models/archive.py
index 6e99ea491..01e230827 100644
--- a/backend/app/models/archive.py
+++ b/backend/app/models/archive.py
@@ -35,6 +35,9 @@ class PrintArchive(Base):
bed_temperature: Mapped[int | None] = mapped_column(Integer)
nozzle_temperature: Mapped[int | None] = mapped_column(Integer)
+ # Printer model this file was sliced for (extracted from 3MF metadata)
+ sliced_for_model: Mapped[str | None] = mapped_column(String(50), nullable=True)
+
# Print result
status: Mapped[str] = mapped_column(String(20), default="completed")
started_at: Mapped[datetime | None] = mapped_column(DateTime)
diff --git a/backend/app/models/print_queue.py b/backend/app/models/print_queue.py
index 64ccc4980..cd33ee121 100644
--- a/backend/app/models/print_queue.py
+++ b/backend/app/models/print_queue.py
@@ -15,6 +15,9 @@ class PrintQueueItem(Base):
# Links
printer_id: Mapped[int | None] = mapped_column(ForeignKey("printers.id", ondelete="CASCADE"), nullable=True)
+ # Target printer model for model-based assignment (mutually exclusive with printer_id)
+ # When set, scheduler assigns to any idle printer of matching model
+ target_model: Mapped[str | None] = mapped_column(String(50), nullable=True)
# Either archive_id OR library_file_id must be set (archive created at print start from library file)
archive_id: Mapped[int | None] = mapped_column(ForeignKey("print_archives.id", ondelete="CASCADE"), nullable=True)
library_file_id: Mapped[int | None] = mapped_column(
diff --git a/backend/app/schemas/archive.py b/backend/app/schemas/archive.py
index f968fca72..42e005007 100644
--- a/backend/app/schemas/archive.py
+++ b/backend/app/schemas/archive.py
@@ -63,6 +63,8 @@ class ArchiveResponse(BaseModel):
bed_temperature: int | None
nozzle_temperature: int | None
+ sliced_for_model: str | None = None # Printer model this file was sliced for
+
status: str
started_at: datetime | None
completed_at: datetime | None
diff --git a/backend/app/schemas/print_queue.py b/backend/app/schemas/print_queue.py
index d5bdbc780..f64a0be4a 100644
--- a/backend/app/schemas/print_queue.py
+++ b/backend/app/schemas/print_queue.py
@@ -17,6 +17,7 @@ UTCDatetime = Annotated[datetime | None, PlainSerializer(serialize_utc_datetime)
class PrintQueueItemCreate(BaseModel):
printer_id: int | None = None # None = unassigned, user assigns later
+ target_model: str | None = None # Target printer model (mutually exclusive with printer_id)
# Either archive_id OR library_file_id must be provided
archive_id: int | None = None
library_file_id: int | None = None
@@ -40,6 +41,7 @@ class PrintQueueItemCreate(BaseModel):
class PrintQueueItemUpdate(BaseModel):
printer_id: int | None = None
+ target_model: str | None = None # Target printer model (mutually exclusive with printer_id)
position: int | None = None
scheduled_time: datetime | None = None
require_previous_success: bool | None = None
@@ -59,6 +61,7 @@ class PrintQueueItemUpdate(BaseModel):
class PrintQueueItemResponse(BaseModel):
id: int
printer_id: int | None # None = unassigned
+ target_model: str | None = None # Target printer model for model-based assignment
archive_id: int | None # None if library_file_id is set (archive created at print start)
library_file_id: int | None # For queue items from library files
position: int
diff --git a/backend/app/services/archive.py b/backend/app/services/archive.py
index ba28004ec..d20906f81 100644
--- a/backend/app/services/archive.py
+++ b/backend/app/services/archive.py
@@ -67,6 +67,19 @@ class ThreeMFParser:
content = zf.read("Metadata/slice_info.config").decode()
root = ET.fromstring(content)
+ # Extract printer_model_id from plate metadata
+ # Format:
+ for meta in root.findall(".//metadata"):
+ key = meta.get("key")
+ value = meta.get("value")
+ if key == "printer_model_id" and value:
+ from backend.app.utils.printer_models import normalize_printer_model_id
+
+ normalized = normalize_printer_model_id(value)
+ if normalized:
+ self.metadata["sliced_for_model"] = normalized
+ break
+
# Find the plate element (single-plate exports only have one plate)
plate = root.find(".//plate")
@@ -156,7 +169,7 @@ class ThreeMFParser:
pass
def _parse_gcode_header(self, zf: zipfile.ZipFile):
- """Parse G-code file header for total layer count."""
+ """Parse G-code file header for total layer count and printer model."""
import re
try:
@@ -165,15 +178,25 @@ class ThreeMFParser:
if not gcode_files:
return
- # Read first 2KB of G-code (header contains the layer count)
+ # Read first 4KB of G-code (header contains metadata)
gcode_path = gcode_files[0]
with zf.open(gcode_path) as f:
- header = f.read(2048).decode("utf-8", errors="ignore")
+ header = f.read(4096).decode("utf-8", errors="ignore")
# Look for "; total layer number: XX" pattern
match = re.search(r";\s*total\s+layer\s+number[:\s]+(\d+)", header, re.IGNORECASE)
if match:
self.metadata["total_layers"] = int(match.group(1))
+
+ # Look for printer_model in gcode header (fallback if not found in slice_info)
+ # Format: "; printer_model = Bambu Lab X1 Carbon" or "; printer_model = X1C"
+ if "sliced_for_model" not in self.metadata:
+ match = re.search(r";\s*printer_model\s*=\s*(.+)", header, re.IGNORECASE)
+ if match:
+ from backend.app.utils.printer_models import normalize_printer_model
+
+ raw_model = match.group(1).strip()
+ self.metadata["sliced_for_model"] = normalize_printer_model(raw_model)
except Exception:
pass
@@ -256,6 +279,12 @@ class ThreeMFParser:
elif isinstance(val, (int, float, str)):
self.metadata["nozzle_temperature"] = int(float(val))
break
+
+ # Printer model (extract and normalize)
+ if "printer_model" in data:
+ from backend.app.utils.printer_models import normalize_printer_model
+
+ self.metadata["sliced_for_model"] = normalize_printer_model(data["printer_model"])
except Exception:
pass
@@ -877,6 +906,7 @@ class ArchiveService:
nozzle_diameter=metadata.get("nozzle_diameter"),
bed_temperature=metadata.get("bed_temperature"),
nozzle_temperature=metadata.get("nozzle_temperature"),
+ sliced_for_model=metadata.get("sliced_for_model"),
makerworld_url=metadata.get("makerworld_url"),
designer=metadata.get("designer"),
status=status,
diff --git a/backend/app/services/print_scheduler.py b/backend/app/services/print_scheduler.py
index 603499d79..1c9728bca 100644
--- a/backend/app/services/print_scheduler.py
+++ b/backend/app/services/print_scheduler.py
@@ -62,13 +62,10 @@ class PrintScheduler:
if not items:
return
- # Group by printer - only process first item per printer
- processed_printers = set()
+ # Track busy printers to avoid assigning multiple items to same printer
+ busy_printers: set[int] = set()
for item in items:
- if item.printer_id in processed_printers:
- continue
-
# Check scheduled time first (scheduled_time is stored in UTC from ISO string)
if item.scheduled_time and item.scheduled_time > datetime.utcnow():
continue
@@ -77,46 +74,91 @@ class PrintScheduler:
if item.manual_start:
continue
- # Check if printer is idle
- printer_idle = self._is_printer_idle(item.printer_id)
- printer_connected = printer_manager.is_connected(item.printer_id)
+ if item.printer_id:
+ # Specific printer assignment (existing behavior)
+ if item.printer_id in busy_printers:
+ continue
- # If printer not connected, try to power on via smart plug
- if not printer_connected:
- plug = await self._get_smart_plug(db, item.printer_id)
- if plug and plug.auto_on and plug.enabled:
- logger.info(f"Printer {item.printer_id} offline, attempting to power on via smart plug")
- powered_on = await self._power_on_and_wait(plug, item.printer_id, db)
- if powered_on:
- printer_connected = True
- printer_idle = self._is_printer_idle(item.printer_id)
+ # Check if printer is idle
+ printer_idle = self._is_printer_idle(item.printer_id)
+ printer_connected = printer_manager.is_connected(item.printer_id)
+
+ # If printer not connected, try to power on via smart plug
+ if not printer_connected:
+ plug = await self._get_smart_plug(db, item.printer_id)
+ if plug and plug.auto_on and plug.enabled:
+ logger.info(f"Printer {item.printer_id} offline, attempting to power on via smart plug")
+ powered_on = await self._power_on_and_wait(plug, item.printer_id, db)
+ if powered_on:
+ printer_connected = True
+ printer_idle = self._is_printer_idle(item.printer_id)
+ else:
+ logger.warning(f"Could not power on printer {item.printer_id} via smart plug")
+ busy_printers.add(item.printer_id)
+ continue
else:
- logger.warning(f"Could not power on printer {item.printer_id} via smart plug")
- processed_printers.add(item.printer_id)
+ # No plug or auto_on disabled
+ busy_printers.add(item.printer_id)
continue
- else:
- # No plug or auto_on disabled
- processed_printers.add(item.printer_id)
+
+ # Check if printer is idle (busy with another print)
+ if not printer_idle:
+ busy_printers.add(item.printer_id)
continue
- # Check if printer is idle (busy with another print)
- if not printer_idle:
- processed_printers.add(item.printer_id)
- continue
+ # Check condition (previous print success)
+ if item.require_previous_success:
+ if not await self._check_previous_success(db, item):
+ item.status = "skipped"
+ item.error_message = "Previous print failed or was aborted"
+ item.completed_at = datetime.now()
+ await db.commit()
+ logger.info(f"Skipped queue item {item.id} - previous print failed")
+ continue
- # Check condition (previous print success)
- if item.require_previous_success:
- if not await self._check_previous_success(db, item):
- item.status = "skipped"
- item.error_message = "Previous print failed or was aborted"
- item.completed_at = datetime.now()
- await db.commit()
- logger.info(f"Skipped queue item {item.id} - previous print failed")
- continue
+ # Start the print
+ await self._start_print(db, item)
+ busy_printers.add(item.printer_id)
- # Start the print
- await self._start_print(db, item)
- processed_printers.add(item.printer_id)
+ elif item.target_model:
+ # Model-based assignment - find any idle printer of matching model
+ printer_id = await self._find_idle_printer_for_model(db, item.target_model, busy_printers)
+ if printer_id:
+ # Check condition (previous print success) before assigning
+ if item.require_previous_success:
+ if not await self._check_previous_success(db, item):
+ item.status = "skipped"
+ item.error_message = "Previous print failed or was aborted"
+ item.completed_at = datetime.now()
+ await db.commit()
+ logger.info(f"Skipped queue item {item.id} - previous print failed")
+ continue
+
+ # Assign printer and start
+ item.printer_id = printer_id
+ logger.info(f"Model-based assignment: queue item {item.id} assigned to printer {printer_id}")
+ await self._start_print(db, item)
+ busy_printers.add(printer_id)
+
+ async def _find_idle_printer_for_model(self, db: AsyncSession, model: str, exclude_ids: set[int]) -> int | None:
+ """Find an idle, connected printer matching the model.
+
+ Args:
+ db: Database session
+ model: Printer model to match (e.g., "X1C", "P1S")
+ exclude_ids: Printer IDs to exclude (already busy)
+
+ Returns:
+ Printer ID if found, None otherwise
+ """
+ result = await db.execute(
+ select(Printer).where(Printer.model == model).where(Printer.is_active == True) # noqa: E712
+ )
+ for printer in result.scalars().all():
+ if printer.id not in exclude_ids:
+ if self._is_printer_idle(printer.id) and printer_manager.is_connected(printer.id):
+ return printer.id
+ return None
def _is_printer_idle(self, printer_id: int) -> bool:
"""Check if a printer is connected and idle."""
diff --git a/backend/app/utils/printer_models.py b/backend/app/utils/printer_models.py
new file mode 100644
index 000000000..f412bf474
--- /dev/null
+++ b/backend/app/utils/printer_models.py
@@ -0,0 +1,86 @@
+"""Printer model normalization utilities.
+
+Converts 3MF printer model names (e.g., "Bambu Lab X1 Carbon") to
+normalized short names (e.g., "X1C") that match database storage.
+"""
+
+# Map from 3MF printer_model strings to normalized short names
+PRINTER_MODEL_MAP = {
+ "Bambu Lab X1 Carbon": "X1C",
+ "Bambu Lab X1": "X1",
+ "Bambu Lab X1E": "X1E",
+ "Bambu Lab P1S": "P1S",
+ "Bambu Lab P1P": "P1P",
+ "Bambu Lab P2S": "P2S",
+ "Bambu Lab A1": "A1",
+ "Bambu Lab A1 Mini": "A1 Mini",
+ "Bambu Lab A1 mini": "A1 Mini",
+ "Bambu Lab H2D": "H2D",
+ "Bambu Lab H2D Pro": "H2D Pro",
+}
+
+# Map from printer_model_id (internal codes in slice_info.config) to short names
+# These are the codes Bambu Studio uses internally
+PRINTER_MODEL_ID_MAP = {
+ # X1 series
+ "C11": "X1C",
+ "C12": "X1",
+ "C13": "X1E",
+ # P1 series
+ "P1P": "P1P",
+ "P1S": "P1S",
+ # P2 series
+ "P2S": "P2S",
+ # A1 series
+ "A11": "A1",
+ "A12": "A1 Mini",
+ "N1": "A1",
+ "N2S": "A1 Mini",
+ "A04": "A1 Mini",
+ # H2D series (Office/H series)
+ "O1D": "H2D",
+ "O2D": "H2D Pro",
+}
+
+
+def normalize_printer_model_id(model_id: str | None) -> str | None:
+ """Convert printer_model_id (internal code) to normalized short name.
+
+ Args:
+ model_id: The printer_model_id from slice_info.config (e.g., "C11", "O1D")
+
+ Returns:
+ Normalized short name (e.g., "X1C", "H2D") or the original ID if unknown.
+ """
+ if not model_id:
+ return None
+
+ # Check known mappings
+ if model_id in PRINTER_MODEL_ID_MAP:
+ return PRINTER_MODEL_ID_MAP[model_id]
+
+ # Return original if unknown (might already be a short name)
+ return model_id
+
+
+def normalize_printer_model(raw_model: str | None) -> str | None:
+ """Convert 3MF printer_model to normalized short name.
+
+ Args:
+ raw_model: The printer_model string from 3MF metadata
+ (e.g., "Bambu Lab X1 Carbon")
+
+ Returns:
+ Normalized short name (e.g., "X1C") or None if input is empty.
+ Unknown models have "Bambu Lab " prefix stripped.
+ """
+ if not raw_model:
+ return None
+
+ # Check known mappings first
+ if raw_model in PRINTER_MODEL_MAP:
+ return PRINTER_MODEL_MAP[raw_model]
+
+ # Strip "Bambu Lab " prefix for unknown models
+ stripped = raw_model.replace("Bambu Lab ", "").strip()
+ return stripped or None
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
index 6ccef4b6e..00f4aef2a 100644
--- a/frontend/src/api/client.ts
+++ b/frontend/src/api/client.ts
@@ -300,6 +300,7 @@ export interface Archive {
nozzle_diameter: number | null;
bed_temperature: number | null;
nozzle_temperature: number | null;
+ sliced_for_model: string | null; // Printer model this file was sliced for
status: string;
started_at: string | null;
completed_at: string | null;
@@ -1000,6 +1001,7 @@ export interface DiscoveredTasmotaDevice {
export interface PrintQueueItem {
id: number;
printer_id: number | null; // null = unassigned
+ target_model: string | null; // Target printer model for model-based assignment
// Either archive_id OR library_file_id must be set (archive created at print start)
archive_id: number | null;
library_file_id: number | null;
@@ -1032,6 +1034,7 @@ export interface PrintQueueItem {
export interface PrintQueueItemCreate {
printer_id?: number | null; // null = unassigned
+ target_model?: string | null; // Target printer model (mutually exclusive with printer_id)
// Either archive_id OR library_file_id must be provided
archive_id?: number | null;
library_file_id?: number | null;
@@ -1052,6 +1055,7 @@ export interface PrintQueueItemCreate {
export interface PrintQueueItemUpdate {
printer_id?: number | null; // null = unassign
+ target_model?: string | null; // Target printer model (mutually exclusive with printer_id)
position?: number;
scheduled_time?: string | null;
require_previous_success?: boolean;
diff --git a/frontend/src/components/PrintModal/PrinterSelector.tsx b/frontend/src/components/PrintModal/PrinterSelector.tsx
index d363e1763..9b618c5b7 100644
--- a/frontend/src/components/PrintModal/PrinterSelector.tsx
+++ b/frontend/src/components/PrintModal/PrinterSelector.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import {
Printer as PrinterIcon,
@@ -9,6 +9,7 @@ import {
Circle,
RefreshCw,
Wand2,
+ Users,
} from 'lucide-react';
import { api } from '../../api/client';
import { getColorName } from '../../utils/colors';
@@ -16,7 +17,7 @@ import {
normalizeColorForCompare,
colorsAreSimilar,
} from '../../utils/amsHelpers';
-import type { PrinterSelectorProps } from './types';
+import type { PrinterSelectorProps, AssignmentMode } from './types';
import type { PrinterMappingResult, PerPrinterConfig } from '../../hooks/useMultiPrinterFilamentMapping';
import type { FilamentRequirement, LoadedFilament } from '../../hooks/useFilamentMapping';
@@ -29,6 +30,16 @@ interface PrinterSelectorWithMappingProps extends PrinterSelectorProps {
onAutoConfigurePrinter?: (printerId: number) => void;
/** Callback to update printer config */
onUpdatePrinterConfig?: (printerId: number, config: Partial) => void;
+ /** Current assignment mode */
+ assignmentMode?: AssignmentMode;
+ /** Handler for assignment mode change */
+ onAssignmentModeChange?: (mode: AssignmentMode) => void;
+ /** Selected target model (when assignmentMode is 'model') */
+ targetModel?: string | null;
+ /** Handler for target model change */
+ onTargetModelChange?: (model: string | null) => void;
+ /** Suggested model from sliced file (for pre-selection) */
+ slicedForModel?: string | null;
}
/**
@@ -212,9 +223,42 @@ export function PrinterSelector({
filamentReqs,
onAutoConfigurePrinter,
onUpdatePrinterConfig,
+ assignmentMode = 'printer',
+ onAssignmentModeChange,
+ targetModel,
+ onTargetModelChange,
+ slicedForModel,
}: PrinterSelectorWithMappingProps) {
+ // State for showing all printers vs only matching model
+ const [showAllPrinters, setShowAllPrinters] = useState(false);
+
// Filter printers based on showInactive flag
- const displayPrinters = showInactive ? printers : printers.filter((p) => p.is_active);
+ const activePrinters = showInactive ? printers : printers.filter((p) => p.is_active);
+
+ // Filter by sliced model (only in printer mode, when slicedForModel is set)
+ const displayPrinters = useMemo(() => {
+ if (assignmentMode !== 'printer' || !slicedForModel || showAllPrinters) {
+ return activePrinters;
+ }
+ // Filter to only show printers matching the sliced model
+ const matching = activePrinters.filter((p) => p.model === slicedForModel);
+ // If no matching printers, show all
+ return matching.length > 0 ? matching : activePrinters;
+ }, [activePrinters, assignmentMode, slicedForModel, showAllPrinters]);
+
+ // Check if there are hidden printers due to model filtering
+ const hiddenPrinterCount = activePrinters.length - displayPrinters.length;
+
+ // Get unique models from available printers (for model-based assignment)
+ const uniqueModels = useMemo(() => {
+ const models = activePrinters
+ .map(p => p.model)
+ .filter((m): m is string => Boolean(m));
+ return [...new Set(models)].sort();
+ }, [activePrinters]);
+
+ // Check if model-based assignment is available (need callbacks and multiple printers of same model)
+ const modelAssignmentAvailable = onAssignmentModeChange && onTargetModelChange && uniqueModels.length > 0;
const showMappingOptions = allowMultiple &&
selectedPrinterIds.length > 1 &&
@@ -285,8 +329,56 @@ export function PrinterSelector({
return (