mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Record the colour a slice is actually printed in (issue #2977)
Every file the internal slicer produced came back with filament_colour = #00AE42 whatever filament was picked: a green plate thumbnail, green metadata, and a "Color mismatch" in the Print dialog against the AMS slot the job had just been correctly mapped to. A colour is not a property of a filament preset in either slicer. It belongs to the project, and Bambu Studio and OrcaSlicer set it from the plate in their GUIs -- so nothing was attached to the preset Bambuddy sends by name, and the CLI fell back to its own compiled-in default, which is Bambu green. None of the shipped BBL filament profiles define filament_colour; the whole tree has zero occurrences. default_filament_colour is not the answer on its own. Measured against a 02.08.02.61 sidecar, a profile carrying only that still slices to filament_colour ["#00AE42"] -- Bambu Studio consumes it in the GUI when a project is created, not in --load-filaments. So it is read and rewritten as filament_colour, which the same sidecar does honour: the reporter's exact triplet returns ["#E8B00C"] when patched this way, and the colour lands in both project_settings.config and slice_info.config, which is what the thumbnail and the AMS mapping actually read. Each filament row in the slice dialog gets a colour control, and the resolved value flows through a chain: the user's pick, then the preset's own default_filament_colour, then the colour that slot was designed with in the source 3MF. A slot with none of the three is left untouched rather than given a guess. The designed colour is read from project_settings.config, not slice_info.config. The latter records what the file was last sliced with, which for a source that never carried a colour is #00AE42 itself -- measured -- so using it would have been circular. The control is offered on single-filament sources too, because an STL, and equally a mesh-only 3MF exported from CAD, has no colour anywhere else to inherit. That is the case this exists for, and it took three shapes to make it visible: a swatch in the label row was identical to the read-only dot multi-colour rows have carried for releases, and adding the hex beside it only made it look like a caption. It now sits beside the dropdown, styled like it and the same height, with the swatch and hex wrapped in one label bound to the input so a click anywhere on it opens the picker. An untouched slot with no designed colour submits an empty string rather than the control's displayed default. A sent colour outranks the preset's own, so pinning the placeholder would silently discard the real colour of an imported OrcaSlicer profile that carries one. Slicer Pipelines pick up the same chain without carrying a colour of their own. Also adds a warning for a defect found while investigating this: a filament preset whose name the sidecar's bundle cannot resolve is not rejected. The CLI inherits nothing, falls back to its defaults for every field, and returns a well-formed success -- measured, an unresolvable name slices as filament_type ["PLA"] at nozzle_temperature ["200"] with filament_ids [""] and filament_vendor ["(Undefined)"], so a PETG preset a sidecar image predates prints at PLA temperatures with no diagnostic anywhere. Both signals are required together, which keeps it off the two legitimate lookalikes: a hand-written profile that never named a vendor still carries a real filament id, and a user's own cloud preset carries a vendor while legitimately having no bundled id. The file is kept rather than refused, unlike the missing start G-code of whoever can see the temperatures.
This commit is contained in:
@@ -189,6 +189,23 @@ function formatElapsed(seconds: number): string {
|
||||
return `${h}h ${remM}m`;
|
||||
}
|
||||
|
||||
// The slicer's own default when nothing supplies a colour — Bambu green. Shown
|
||||
// in the picker for a source that carries no colour of its own (an STL, or a
|
||||
// mesh-only 3MF) so the swatch tells the truth about what the slice will
|
||||
// produce rather than showing an invented placeholder.
|
||||
const SLICER_DEFAULT_COLOUR = '#00AE42';
|
||||
|
||||
// `<input type="color">` accepts only `#RRGGBB`. Source colours reach us in
|
||||
// both that form and the 8-digit `#RRGGBBAA` the AMS reports, so the alpha byte
|
||||
// is trimmed for display only — an untouched slot still submits the original
|
||||
// string, alpha included.
|
||||
function colourInputValue(raw: string | null | undefined): string {
|
||||
const value = (raw || '').trim();
|
||||
return /^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(value)
|
||||
? value.slice(0, 7).toUpperCase()
|
||||
: SLICER_DEFAULT_COLOUR;
|
||||
}
|
||||
|
||||
export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { trackJob } = useSliceJobTracker();
|
||||
@@ -201,6 +218,13 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
// entry per AMS slot the plate uses. Pre-pick (effect below) initialises
|
||||
// each slot from the source plate's required (type, colour).
|
||||
const [filamentPresets, setFilamentPresets] = useState<(PresetRef | null)[]>([]);
|
||||
// Per-slot colour override, plate-slot-ordered alongside `filamentPresets`.
|
||||
// `null` means "not overridden" rather than "no colour": the slot then falls
|
||||
// through to the source plate's own colour, and — when the source has none
|
||||
// either — to the backend's remaining fallbacks. Storing an explicit colour
|
||||
// for every slot up front would defeat that chain, because a sent colour
|
||||
// outranks the preset's own `default_filament_colour` (#2977).
|
||||
const [filamentColours, setFilamentColours] = useState<(string | null)[]>([]);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
// null = plate not yet picked (or single-plate / non-3MF — picker is skipped
|
||||
// and we'll backfill 1 at submit time). Set to a 1-indexed plate number once
|
||||
@@ -569,6 +593,16 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
});
|
||||
}, [presetsQuery.data, filamentSlots, selectedPrinterName, compatIndex]);
|
||||
|
||||
// Drop colour overrides when the slot count changes. A plate switch renumbers
|
||||
// the slots, so keeping index-keyed overrides would paint slot 2's colour
|
||||
// onto whatever the new plate happens to call slot 2. Same slot count keeps
|
||||
// them: that is a re-pick of presets, not a different plate layout.
|
||||
useEffect(() => {
|
||||
setFilamentColours((current) =>
|
||||
current.length === filamentSlots.length ? current : filamentSlots.map(() => null),
|
||||
);
|
||||
}, [filamentSlots]);
|
||||
|
||||
const enqueueMutation = useMutation({
|
||||
mutationFn: async (plate: number | null) => {
|
||||
const body = buildSliceBody(plate);
|
||||
@@ -604,6 +638,14 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
process_preset: processPreset,
|
||||
filament_preset: filamentPresets[0] as PresetRef,
|
||||
filament_presets: filamentPresets as PresetRef[],
|
||||
// An untouched slot submits the source plate's own colour, and an empty
|
||||
// string when the source has none — which is what lets the backend fall
|
||||
// back to the preset's `default_filament_colour` for the Orca-imported
|
||||
// profiles that carry one. Sending a placeholder here instead would win
|
||||
// that comparison and silently discard the preset's real colour.
|
||||
filament_colours: filamentSlots.map(
|
||||
(slot, i) => filamentColours[i] ?? slot.color ?? '',
|
||||
),
|
||||
...(plate != null ? { plate } : {}),
|
||||
...(bedType != null ? { bed_type: bedType } : {}),
|
||||
// The preset refs above are still sent (the backend validator requires
|
||||
@@ -1002,7 +1044,20 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
})
|
||||
}
|
||||
disabled={isEnqueuing || !isUsed || useEmbedded}
|
||||
swatchColor={filamentSlots.length > 1 ? slot.color : undefined}
|
||||
// Shown for every filament slot now that it is editable,
|
||||
// not just multi-color ones: a single-slot STL is exactly
|
||||
// the case with no colour anywhere else to inherit.
|
||||
swatchColor={filamentColours[idx] ?? slot.color}
|
||||
swatchColorLabel={t('slice.filamentColour')}
|
||||
onSwatchColorChange={(colour) =>
|
||||
setFilamentColours((current) => {
|
||||
const next = current.length === filamentSlots.length
|
||||
? [...current]
|
||||
: filamentSlots.map((_, i) => current[i] ?? null);
|
||||
next[idx] = colour;
|
||||
return next;
|
||||
})
|
||||
}
|
||||
selectedPrinterName={selectedPrinterName}
|
||||
compatIndex={compatIndex}
|
||||
/>
|
||||
@@ -1255,7 +1310,7 @@ function BedTypeDropdown({
|
||||
value={value ?? ''}
|
||||
onChange={(e) => onChange(e.target.value === '' ? null : e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full px-3 py-2 rounded-md bg-bambu-dark border border-bambu-dark-tertiary text-white text-sm focus:outline-none focus:border-bambu-gray disabled:opacity-50"
|
||||
className="flex-1 min-w-0 px-3 py-2 rounded-md bg-bambu-dark border border-bambu-dark-tertiary text-white text-sm focus:outline-none focus:border-bambu-gray disabled:opacity-50"
|
||||
>
|
||||
<option value="">{t('slice.bedType.auto')}</option>
|
||||
{BED_TYPE_OPTIONS.map((opt) => (
|
||||
@@ -1279,6 +1334,14 @@ interface PresetDropdownProps {
|
||||
// filament slots so the user can see at a glance which slot they're
|
||||
// configuring against the source 3MF's per-slot colour.
|
||||
swatchColor?: string;
|
||||
// When set, the swatch becomes an editable colour input for this slot
|
||||
// (#2977). Filament presets carry no colour of their own, so for an STL —
|
||||
// or any source whose plate was never given one — this is the only place the
|
||||
// colour recorded in the sliced file can come from.
|
||||
onSwatchColorChange?: (colour: string) => void;
|
||||
// Accessible name for that input. Required alongside the handler because the
|
||||
// visible label belongs to the preset <select>, not to the swatch.
|
||||
swatchColorLabel?: string;
|
||||
// Selected printer context (#1325). When provided for a process / filament
|
||||
// slot, presets that resolve to a different printer (per compatIndex) are
|
||||
// held back behind a "Show all" link instead of padding out the main list.
|
||||
@@ -1294,6 +1357,8 @@ function PresetDropdown({
|
||||
onChange,
|
||||
disabled,
|
||||
swatchColor,
|
||||
onSwatchColorChange,
|
||||
swatchColorLabel,
|
||||
selectedPrinterName,
|
||||
compatIndex,
|
||||
}: PresetDropdownProps) {
|
||||
@@ -1306,6 +1371,9 @@ function PresetDropdown({
|
||||
// nested. Filament slots render several of these, so the id must be unique
|
||||
// per instance rather than derived from the slot name.
|
||||
const selectId = useId();
|
||||
// The colour input needs its own id so the hex beside it can be a <label>
|
||||
// for it rather than for the preset <select> it sits next to.
|
||||
const colourId = `${selectId}-colour`;
|
||||
|
||||
// Tier sections (imported → cloud → standard), plus — for a process /
|
||||
// filament slot with a selected printer — a trailing group of presets that
|
||||
@@ -1375,10 +1443,12 @@ function PresetDropdown({
|
||||
// well as being invalid HTML. The label is bound to the select by id.
|
||||
<div className="block">
|
||||
<div className="flex items-center gap-2 text-xs text-bambu-gray mb-1">
|
||||
{swatchColor && (
|
||||
{/* Read-only dot for slots with no editable colour. Filament rows
|
||||
carry a real control beside the dropdown instead — see below. */}
|
||||
{!onSwatchColorChange && swatchColor && (
|
||||
<span
|
||||
className="inline-block w-3 h-3 rounded-full border border-bambu-dark-tertiary"
|
||||
style={{ backgroundColor: swatchColor || 'transparent' }}
|
||||
style={{ backgroundColor: swatchColor }}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
@@ -1403,6 +1473,7 @@ function PresetDropdown({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-stretch gap-2">
|
||||
<select
|
||||
id={selectId}
|
||||
value={toRefValue(value)}
|
||||
@@ -1434,6 +1505,45 @@ function PresetDropdown({
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
{/* The colour control sits beside the dropdown, styled like it and the
|
||||
same height, because that is what makes it read as a control at all.
|
||||
Two earlier shapes did not: a bare swatch in the label row looked
|
||||
exactly like the read-only dot multi-colour rows had carried for
|
||||
releases, and adding the hex beside it only made it look like a
|
||||
caption. On a single-filament STL — the one source with no colour to
|
||||
inherit, and so the case this exists for — neither said anything was
|
||||
settable. Clicking the label opens the picker, so the hex is a hit
|
||||
target and not a caption. */}
|
||||
{onSwatchColorChange && (
|
||||
<label
|
||||
htmlFor={colourId}
|
||||
title={swatchColorLabel}
|
||||
className={`flex items-center gap-2 px-2.5 rounded-md bg-bambu-dark border border-bambu-dark-tertiary text-sm ${
|
||||
disabled
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'cursor-pointer hover:border-bambu-gray transition-colors'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
id={colourId}
|
||||
type="color"
|
||||
value={colourInputValue(swatchColor)}
|
||||
onChange={(e) => onSwatchColorChange(e.target.value.toUpperCase())}
|
||||
disabled={disabled}
|
||||
aria-label={swatchColorLabel}
|
||||
// Painted explicitly as well as through the native swatch: every
|
||||
// engine fills ::-webkit-color-swatch / ::-moz-color-swatch from
|
||||
// the value, but one that did not would leave an empty ring, and
|
||||
// an unlit swatch is indistinguishable from no swatch at all.
|
||||
style={{ backgroundColor: colourInputValue(swatchColor) }}
|
||||
className="slice-colour-swatch w-4 h-4 shrink-0 rounded-full border border-bambu-dark-tertiary p-0 disabled:cursor-not-allowed enabled:cursor-pointer"
|
||||
/>
|
||||
<span className="font-mono text-xs text-white tracking-tight">
|
||||
{colourInputValue(swatchColor)}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user