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

This commit is contained in:
maziggy
2026-01-03 10:26:06 +01:00
parent 55ec08d4d1
commit 5dae3d3021
6 changed files with 115 additions and 81 deletions
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 one or more lines are too long
+1 -1
View File
@@ -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>