Browse Source

Fixed bug in skip objects modal, where the object ID markers are not correctly positioned over their corresponding objects in the build plate preview

maziggy 4 months ago
parent
commit
5dae3d3021

+ 3 - 1
backend/app/api/routes/printers.py

@@ -1181,9 +1181,10 @@ async def get_printable_objects(
                 if downloaded and temp_path.exists():
                     with open(temp_path, "rb") as f:
                         data = f.read()
-                    objects = extract_printable_objects_from_3mf(data, include_positions=True)
+                    objects, bbox_all = extract_printable_objects_from_3mf(data, include_positions=True)
                     if objects:
                         client.state.printable_objects = objects
+                        client.state.printable_objects_bbox_all = bbox_all
                         logger.info(f"Reloaded {len(objects)} objects for printer {printer_id}")
             except Exception as e:
                 logger.debug(f"Failed to reload objects from printer: {e}")
@@ -1219,6 +1220,7 @@ async def get_printable_objects(
         "total": len(objects),
         "skipped_count": len(client.state.skipped_objects),
         "is_printing": client.state.state in ("RUNNING", "PAUSE"),
+        "bbox_all": getattr(client.state, "printable_objects_bbox_all", None),
     }
 
 

+ 36 - 22
backend/app/services/archive.py

@@ -342,7 +342,7 @@ class ThreeMFParser:
 
 def extract_printable_objects_from_3mf(
     data: bytes, plate_number: int | None = None, include_positions: bool = False
-) -> dict[int, str] | dict[int, dict]:
+) -> dict[int, str] | dict[int, dict] | tuple[dict[int, dict], list | None]:
     """Extract printable objects from 3MF file bytes.
 
     This is a lightweight function used during print start to get the list
@@ -351,16 +351,17 @@ def extract_printable_objects_from_3mf(
     Args:
         data: Raw bytes of the 3MF file
         plate_number: Which plate was printed (1-based), or None for first plate
-        include_positions: If True, return dict with name and position info
+        include_positions: If True, return tuple of (objects dict, bbox_all)
 
     Returns:
         If include_positions=False: Dictionary mapping identify_id (int) to object name (str)
-        If include_positions=True: Dictionary mapping identify_id to {name, x, y} dict
+        If include_positions=True: Tuple of (dict mapping identify_id to {name, x, y}, bbox_all list or None)
     """
     import json
     from io import BytesIO
 
     printable_objects: dict = {}
+    bbox_all: list | None = None
 
     try:
         with zipfile.ZipFile(BytesIO(data), "r") as zf:
@@ -371,7 +372,6 @@ def extract_printable_objects_from_3mf(
             root = ET.fromstring(content)
 
             # Find the correct plate
-            plate_idx = plate_number or 1
             if plate_number:
                 plate = root.find(f".//plate[@plate_idx='{plate_number}']")
                 if plate is None:
@@ -382,19 +382,35 @@ def extract_printable_objects_from_3mf(
             if plate is None:
                 return printable_objects
 
+            # Get actual plate index from metadata (sliced files only have one plate)
+            plate_idx = plate_number or 1
+            for meta in plate.findall("metadata"):
+                if meta.get("key") == "index":
+                    try:
+                        plate_idx = int(meta.get("value", "1"))
+                    except ValueError:
+                        pass
+                    break
+
             # Load position data from plate_N.json if we need positions
-            bbox_objects = []
+            # Build a lookup by name since bbox_objects.id != slice_info identify_id
+            bbox_by_name: dict[str, list] = {}
             if include_positions:
                 plate_json_path = f"Metadata/plate_{plate_idx}.json"
                 if plate_json_path in zf.namelist():
                     try:
                         plate_json = json.loads(zf.read(plate_json_path).decode())
-                        bbox_objects = plate_json.get("bbox_objects", [])
+                        # Get bbox_all - the bounding box of all objects (used for image bounds)
+                        bbox_all = plate_json.get("bbox_all")
+                        for bbox_obj in plate_json.get("bbox_objects", []):
+                            obj_name = bbox_obj.get("name")
+                            bbox = bbox_obj.get("bbox", [])
+                            if obj_name and len(bbox) >= 4:
+                                bbox_by_name[obj_name] = bbox
                     except (json.JSONDecodeError, KeyError):
                         pass
 
             # Extract objects from slice_info.config
-            objects_list = []
             for obj in plate.findall("object"):
                 identify_id = obj.get("identify_id")
                 name = obj.get("name")
@@ -403,27 +419,25 @@ def extract_printable_objects_from_3mf(
                 if identify_id and name and skipped.lower() != "true":
                     try:
                         obj_id = int(identify_id)
-                        objects_list.append((obj_id, name))
+                        if include_positions:
+                            x, y = None, None
+                            # Match by name to get bbox coordinates
+                            bbox = bbox_by_name.get(name)
+                            if bbox:
+                                # Calculate center from bbox [x_min, y_min, x_max, y_max]
+                                x = (bbox[0] + bbox[2]) / 2
+                                y = (bbox[1] + bbox[3]) / 2
+                            printable_objects[obj_id] = {"name": name, "x": x, "y": y}
+                        else:
+                            printable_objects[obj_id] = name
                     except ValueError:
                         pass
 
-            # Match objects with positions by index (both lists are in same order)
-            for idx, (obj_id, name) in enumerate(objects_list):
-                if include_positions:
-                    x, y = None, None
-                    if idx < len(bbox_objects):
-                        bbox = bbox_objects[idx].get("bbox", [])
-                        if len(bbox) >= 4:
-                            # Calculate center from bbox [x_min, y_min, x_max, y_max]
-                            x = (bbox[0] + bbox[2]) / 2
-                            y = (bbox[1] + bbox[3]) / 2
-                    printable_objects[obj_id] = {"name": name, "x": x, "y": y}
-                else:
-                    printable_objects[obj_id] = name
-
     except Exception:
         pass
 
+    if include_positions:
+        return printable_objects, bbox_all
     return printable_objects
 
 

+ 1 - 0
frontend/src/api/client.ts

@@ -1326,6 +1326,7 @@ export const api = {
       total: number;
       skipped_count: number;
       is_printing: boolean;
+      bbox_all: [number, number, number, number] | null;
     }>(`/printers/${printerId}/print/objects`),
 
   skipObjects: (printerId: number, objectIds: number[]) =>

+ 23 - 6
frontend/src/pages/PrintersPage.tsx

@@ -2523,15 +2523,32 @@ function PrinterCard({
                     {objectsData.objects.length > 0 && (
                       <div className="absolute inset-0 pointer-events-none">
                         {objectsData.objects.map((obj, idx) => {
-                          // Build plate is typically 256x256mm for X1C
-                          const buildPlateSize = 256;
                           let x: number, y: number;
 
                           // Use position data if available, otherwise fall back to grid
-                          if (obj.x != null && obj.y != null) {
-                            // Convert mm position to percentage (0-100)
-                            // Clamp to valid range and add padding
-                            // Y axis is inverted: 3D printing Y goes back, image Y goes down
+                          if (obj.x != null && obj.y != null && objectsData.bbox_all) {
+                            // bbox_all is [x_min, y_min, x_max, y_max] - the bounds of all objects
+                            // The top_N.png image is rendered to show this area with ~10% padding
+                            const [xMin, yMin, xMax, yMax] = objectsData.bbox_all;
+                            const bboxWidth = xMax - xMin;
+                            const bboxHeight = yMax - yMin;
+
+                            // Calculate position relative to bbox, with padding
+                            // The image has roughly 10% padding on each side
+                            const padding = 10;
+                            const contentArea = 100 - (padding * 2);
+
+                            x = padding + ((obj.x - xMin) / bboxWidth) * contentArea;
+                            // Y axis: in 3D coords Y increases toward back, in image Y increases down
+                            // So we need to flip: high Y in 3D = low Y in image (top)
+                            y = padding + ((yMax - obj.y) / bboxHeight) * contentArea;
+
+                            // Clamp to valid range
+                            x = Math.max(5, Math.min(95, x));
+                            y = Math.max(5, Math.min(95, y));
+                          } else if (obj.x != null && obj.y != null) {
+                            // Fallback: use full build plate (256mm for X1C)
+                            const buildPlateSize = 256;
                             x = Math.max(10, Math.min(90, (obj.x / buildPlateSize) * 100));
                             y = Math.max(10, Math.min(90, 100 - (obj.y / buildPlateSize) * 100));
                           } else {

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-ByHF_LDf.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-KNsnljef.js"></script>
+    <script type="module" crossorigin src="/assets/index-ByHF_LDf.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-j3n1gAEX.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff