Browse Source

Summary of Changes

  Backend (backend/app/api/routes/archives.py):
  - Added filament_colors extraction from slice_info.config in 3MF files
  - Maps filament IDs (1-based) to tool numbers (0-based)

  Frontend (frontend/src/components/GcodeViewer.tsx):
  1. Multi-color support: Pass extrusionColor as array of CSS color strings for multi-color prints
  2. Bambu T-command filtering: Filter out special Bambu commands (T255, T1000, T1001, T65535, T65279) that corrupt tool state
  3. Initial tool: Prepend T0 to ensure initial tool is set correctly
  4. Disable gradient: Set disableGradient: true for multi-color to preserve actual filament colors (the gradient was turning black into gray/white)
  5. Disable topLayerColor: Don't set topLayerColor for multi-color (it overrides per-tool colors)

  Key fixes for Bambu G-code compatibility:
  - Bambu uses special T commands (T1000, T65535, etc.) that aren't real tool changes
  - The gcode-preview library's brightness gradient was modifying colors based on layer index

  Commit message suggestion:
  Add multi-color filament support to G-code viewer

  - Extract filament colors from 3MF slice_info.config
  - Pass color array to gcode-preview for tool-based coloring
  - Filter Bambu special T commands (T1000, T65535, etc.)
  - Disable gradient to preserve actual filament colors
maziggy 5 months ago
parent
commit
c8b9a8e3d6

+ 42 - 0
backend/app/api/routes/archives.py

@@ -1325,6 +1325,7 @@ async def get_qrcode(
 async def get_archive_capabilities(archive_id: int, db: AsyncSession = Depends(get_db)):
     """Check what viewing capabilities are available for this 3MF file."""
     import json
+    import xml.etree.ElementTree as ET
 
     service = ArchiveService(db)
     archive = await service.get_archive(archive_id)
@@ -1338,6 +1339,7 @@ async def get_archive_capabilities(archive_id: int, db: AsyncSession = Depends(g
     has_model = False
     has_gcode = False
     build_volume = {"x": 256, "y": 256, "z": 256}  # Default to X1/P1 size
+    filament_colors: list[str] = []
 
     try:
         with zipfile.ZipFile(file_path, "r") as zf:
@@ -1358,6 +1360,45 @@ async def get_archive_capabilities(archive_id: int, db: AsyncSession = Depends(g
                     except Exception:
                         pass
 
+            # Extract filament colors from slice_info.config
+            # These are the actual filaments used in the print, indexed by tool/extruder
+            if "Metadata/slice_info.config" in names:
+                try:
+                    slice_content = zf.read("Metadata/slice_info.config").decode("utf-8")
+                    root = ET.fromstring(slice_content)
+
+                    # Get all filaments with their IDs and colors
+                    # <filament id="1" type="PLA" color="#FFFFFF" used_g="100" />
+                    # ID corresponds to the tool number in G-code (T0, T1, etc.)
+                    filaments = root.findall(".//filament")
+                    filament_map: dict[int, str] = {}
+                    for f in filaments:
+                        fid = f.get("id")
+                        fcolor = f.get("color")
+                        used_g = f.get("used_g", "0")
+                        try:
+                            used_amount = float(used_g)
+                        except (ValueError, TypeError):
+                            used_amount = 0
+
+                        # Include all filaments, but mark unused ones
+                        if fid is not None and fcolor:
+                            try:
+                                # IDs are 1-based in slice_info, tools are 0-based
+                                tool_id = int(fid) - 1
+                                if tool_id >= 0 and used_amount > 0:
+                                    filament_map[tool_id] = fcolor
+                            except ValueError:
+                                pass
+
+                    # Convert to ordered list (tool 0, tool 1, etc.)
+                    if filament_map:
+                        max_tool = max(filament_map.keys())
+                        for i in range(max_tool + 1):
+                            filament_colors.append(filament_map.get(i, "#00AE42"))
+                except Exception:
+                    pass
+
             # Extract build volume from project settings
             if "Metadata/project_settings.config" in names:
                 try:
@@ -1401,6 +1442,7 @@ async def get_archive_capabilities(archive_id: int, db: AsyncSession = Depends(g
         "has_model": has_model,
         "has_gcode": has_gcode,
         "build_volume": build_volume,
+        "filament_colors": filament_colors,
     }
 
 

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

@@ -1380,6 +1380,7 @@ export const api = {
       has_model: boolean;
       has_gcode: boolean;
       build_volume: { x: number; y: number; z: number };
+      filament_colors: string[];
     }>(`/archives/${id}/capabilities`),
   // Project Page
   getArchiveProjectPage: (id: number) =>

+ 43 - 5
frontend/src/components/GcodeViewer.tsx

@@ -11,10 +11,11 @@ interface BuildVolume {
 interface GcodeViewerProps {
   gcodeUrl: string;
   buildVolume?: BuildVolume;
+  filamentColors?: string[];
   className?: string;
 }
 
-export function GcodeViewer({ gcodeUrl, buildVolume = { x: 256, y: 256, z: 256 }, className = '' }: GcodeViewerProps) {
+export function GcodeViewer({ gcodeUrl, buildVolume = { x: 256, y: 256, z: 256 }, filamentColors, className = '' }: GcodeViewerProps) {
   const canvasRef = useRef<HTMLCanvasElement>(null);
   const previewRef = useRef<WebGLPreview | null>(null);
   const [loading, setLoading] = useState(true);
@@ -28,14 +29,26 @@ export function GcodeViewer({ gcodeUrl, buildVolume = { x: 256, y: 256, z: 256 }
 
     const canvas = canvasRef.current;
 
+    const hasColors = filamentColors && filamentColors.length > 0;
+    const hasMultipleColors = filamentColors && filamentColors.length > 1;
+
+    // First color or default bambu green
+    const primaryColor = hasColors ? filamentColors[0] : '#00ae42';
+
     // Initialize the preview
+    // For multi-color: pass array of CSS color strings to extrusionColor
+    // The library uses index to match tool number (T0, T1, T2...)
     const preview = init({
       canvas,
       buildVolume: buildVolume,
       backgroundColor: 0x1a1a1a,
       travelColor: 0x444444,
-      extrusionColor: 0x00ae42,
-      topLayerColor: 0x00ff5a,
+      // Pass array for multi-color, single value for single color
+      extrusionColor: hasMultipleColors ? filamentColors : primaryColor,
+      // Disable topLayerColor for multi-color (it overrides per-tool colors)
+      ...(hasMultipleColors ? {} : { topLayerColor: primaryColor }),
+      // Disable gradient for multi-color to preserve actual filament colors
+      ...(hasMultipleColors ? { disableGradient: true } : {}),
       lastSegmentColor: 0xffffff,
       lineWidth: 2,
       renderTravel: false,
@@ -64,8 +77,33 @@ export function GcodeViewer({ gcodeUrl, buildVolume = { x: 256, y: 256, z: 256 }
         return response.text();
       })
       .then(gcode => {
+        let processedGcode = gcode;
+
+        if (hasMultipleColors) {
+          // Bambu G-code uses special T commands that confuse the parser:
+          // T255, T1000, T1001, T65535, T65279 etc. are not real tool changes
+          // Filter these out and keep only valid tool numbers (T0-T15)
+          processedGcode = gcode
+            .split('\n')
+            .map(line => {
+              const match = line.match(/^(\s*)T(\d+)(\s*;.*)?$/i);
+              if (match) {
+                const toolNum = parseInt(match[2], 10);
+                // Keep only valid tool numbers (0-15), comment out others
+                if (toolNum > 15) {
+                  return `${match[1]}; FILTERED: T${toolNum}${match[3] || ''}`;
+                }
+              }
+              return line;
+            })
+            .join('\n');
+
+          // Prepend T0 to ensure initial tool is set
+          processedGcode = `T0\n${processedGcode}`;
+        }
+
         // Parse G-code
-        preview.processGCode(gcode);
+        preview.processGCode(processedGcode);
 
         // Get layer count
         const layers = preview.layers?.length || 0;
@@ -98,7 +136,7 @@ export function GcodeViewer({ gcodeUrl, buildVolume = { x: 256, y: 256, z: 256 }
       window.removeEventListener('resize', handleResize);
       preview.dispose();
     };
-  }, [gcodeUrl, buildVolume]);
+  }, [gcodeUrl, buildVolume, filamentColors]);
 
   const handleLayerChange = (layer: number) => {
     if (!previewRef.current) return;

+ 3 - 1
frontend/src/components/ModelViewerModal.tsx

@@ -17,6 +17,7 @@ interface Capabilities {
   has_model: boolean;
   has_gcode: boolean;
   build_volume: { x: number; y: number; z: number };
+  filament_colors: string[];
 }
 
 export function ModelViewerModal({ archiveId, title, onClose }: ModelViewerModalProps) {
@@ -47,7 +48,7 @@ export function ModelViewerModal({ archiveId, title, onClose }: ModelViewerModal
       })
       .catch(() => {
         // Fallback to 3D model tab if capabilities check fails
-        setCapabilities({ has_model: true, has_gcode: false, build_volume: { x: 256, y: 256, z: 256 } });
+        setCapabilities({ has_model: true, has_gcode: false, build_volume: { x: 256, y: 256, z: 256 }, filament_colors: [] });
         setActiveTab('3d');
         setLoading(false);
       });
@@ -136,6 +137,7 @@ export function ModelViewerModal({ archiveId, title, onClose }: ModelViewerModal
             <GcodeViewer
               gcodeUrl={api.getArchiveGcode(archiveId)}
               buildVolume={capabilities.build_volume}
+              filamentColors={capabilities.filament_colors}
               className="w-full h-full"
             />
           ) : (

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-RbSkJqNe.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-AT42oZVR.js"></script>
+    <script type="module" crossorigin src="/assets/index-RbSkJqNe.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-CbCN6LSA.css">
   </head>
   <body>

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