mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-06 06:02:05 +02:00
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
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 one or more lines are too long
+1
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user