mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
fix(slicer): correct multi-color slice output + UX polish across the slice pipeline
The slicer CLI silently substitutes embedded defaults for any AMS slot
the user didn't supply a profile for. When a multi-color project (e.g.
a MakerWorld helmet with white shell + grey support filament configured
project-wide) was sliced for a "single-color" plate, the CLI took the
user's white pick for slot 1 and quietly filled slot 2 with the source
3MF's embedded grey support filament — producing a slice the user never
asked for. Same silent-fallback class as the strip-removal bug.
Backend `/filament-requirements` now returns the FULL project AMS slot
list (from project_settings.config) with a `used_in_plate: bool` flag
per entry. The flag comes from the cached preview slice for unsliced
files; sliced files (where slice_info.config already pre-filters by
used_g > 0) get used_in_plate=true on every entry. SliceModal renders
one dropdown per project slot — slots flagged used_in_plate=true are
editable, slots flagged false are auto-picked from project metadata
via the existing colour-match scoring and disabled with a
"-- not used by this plate" suffix. The wire format always carries a
profile per project slot, so the CLI never falls back to embedded
defaults.
Adjacent UX fixes in the same session, since they all hit the same
flow:
- Persistent slice-progress toast: SliceJobTrackerProvider now opens
a persistent loading toast per active job ("Slicing X -- 47s") with
a 1Hz elapsed-time tick and replaces it with the existing transient
success/error toast on terminal state. The previous start+finish
toast pair left a UX dead zone where users couldn't tell whether a
long slice was still running.
- "Analyzing plate filaments..." spinner now shows elapsed seconds and,
after 5s, a hint that the wait is a one-time preview slice (cached;
re-opens are instant). Addresses "is anything happening?" on first
open of an unsliced complex multi-color 3MF.
- Pre-slice printer-mismatch warning + disabled Slice button: the
plates response now exposes `source_printer_model` from
project_settings.config; SliceModal compares against the picked
printer profile name and surfaces an inline warning + disables Slice
on mismatch. The CLI rejects cross-printer slices (rc=-16) and used
to fall back to embedded settings, producing wrong-printer g-code
that errored at print dispatch.
- Sliced-archive card now reflects the actually-used filament list,
not the source's project-wide AMS config:
slice_and_persist_as_archive reads filament_type / filament_color
from the sliced output's slice_info.config (which already gates on
used_g > 0) instead of inheriting from the source archive. A 16+
swatch card on what was actually a 2-color print was the visible
symptom.
- MakerWorld URL-paste resolver enriches each instance with
`compatibility` + `otherCompatibility` from
design.instances[].extention.modelInfo. The /instances/hits payload
omits this so every instance row used to look identical; users
blindly picked the first one regardless of whether it matched their
printer.
Tests: 27 SliceModal tests (2 new for the disabled-row contract +
3 for printer-mismatch + 4 for multi-color rendering); 4 new
SliceJobTrackerContext tests for the persistent-toast lifecycle;
backend filament-requirements / slice-preview / threemf-tools
suites green.
i18n: new keys slice.queuedToast, slice.runningToast,
slice.analyzingPlateFilamentsHint, slice.notUsedByPlate,
slice.printerMismatch, makerworld.slicedFor, makerworld.alsoCompatible
across all 8 UI languages (English + German fully translated, the six
others seeded with English copies pending native translation, matching
the project's existing flow for newly-added user-facing features).
This commit is contained in:
@@ -103,6 +103,41 @@ function fromRefValue(raw: string): PresetRef | null {
|
||||
return { source, id };
|
||||
}
|
||||
|
||||
// Inline spinner for the filament-requirements query. The backend runs a
|
||||
// preview slice on first open of an unsliced project file (cached after);
|
||||
// on a complex multi-color model that's a real slice — multi-second to
|
||||
// multi-minute. The static "Analyzing plate filaments…" string left
|
||||
// users wondering whether anything was happening, so the spinner now
|
||||
// shows elapsed seconds and a hint that explains the wait. After ~5s it
|
||||
// also surfaces a "this is a one-time slice — repeat opens are instant"
|
||||
// note so users don't worry it'll be slow forever.
|
||||
function FilamentAnalysisSpinner() {
|
||||
const { t } = useTranslation();
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
useEffect(() => {
|
||||
const startedAt = Date.now();
|
||||
const id = setInterval(() => setElapsed(Math.floor((Date.now() - startedAt) / 1000)), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
return (
|
||||
<div className="flex flex-col gap-1 text-bambu-gray text-sm py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
{t('slice.analyzingPlateFilaments', 'Analyzing plate filaments…')}
|
||||
<span className="text-xs tabular-nums">{elapsed}s</span>
|
||||
</div>
|
||||
{elapsed >= 5 && (
|
||||
<div className="text-xs text-bambu-gray/70 pl-6">
|
||||
{t(
|
||||
'slice.analyzingPlateFilamentsHint',
|
||||
'Running a preview slice to discover which AMS slots this plate uses. Cached after — re-opening is instant.',
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { trackJob } = useSliceJobTracker();
|
||||
@@ -371,39 +406,49 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
scoped spinner so the user sees the printer/process
|
||||
dropdowns instead of an opaque "Loading presets…" wait. */}
|
||||
{filamentReqsQuery.isLoading ? (
|
||||
<div className="flex items-center gap-2 text-bambu-gray text-sm py-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
{t('slice.analyzingPlateFilaments', 'Analyzing plate filaments…')}
|
||||
</div>
|
||||
<FilamentAnalysisSpinner />
|
||||
) : (
|
||||
filamentSlots.map((slot, idx) => (
|
||||
<PresetDropdown
|
||||
key={`filament-${idx}`}
|
||||
label={
|
||||
filamentSlots.length > 1
|
||||
? t('slice.filamentSlot', {
|
||||
index: idx + 1,
|
||||
type: slot.type,
|
||||
defaultValue: `Filament ${idx + 1} (${slot.type || ''})`,
|
||||
})
|
||||
: t('slice.filament', 'Filament profile')
|
||||
}
|
||||
slot="filament"
|
||||
data={presetsQuery.data}
|
||||
value={filamentPresets[idx] ?? null}
|
||||
onChange={(ref) =>
|
||||
setFilamentPresets((current) => {
|
||||
const next = current.length === filamentSlots.length
|
||||
? [...current]
|
||||
: filamentSlots.map((_, i) => current[i] ?? null);
|
||||
next[idx] = ref;
|
||||
return next;
|
||||
})
|
||||
}
|
||||
disabled={isEnqueuing}
|
||||
swatchColor={filamentSlots.length > 1 ? slot.color : undefined}
|
||||
/>
|
||||
))
|
||||
filamentSlots.map((slot, idx) => {
|
||||
// Slots flagged by the backend as not used by the
|
||||
// picked plate are auto-picked from project metadata
|
||||
// and disabled — the slicer CLI still needs a
|
||||
// profile per project slot, but the user shouldn't
|
||||
// have to think about slots their plate doesn't
|
||||
// paint with. used_in_plate defaults to true when
|
||||
// missing (sliced 3MFs and the no-flag legacy path).
|
||||
const isUsed = slot.used_in_plate !== false;
|
||||
const baseLabel =
|
||||
filamentSlots.length > 1
|
||||
? t('slice.filamentSlot', {
|
||||
index: idx + 1,
|
||||
type: slot.type,
|
||||
defaultValue: `Filament ${idx + 1} (${slot.type || ''})`,
|
||||
})
|
||||
: t('slice.filament', 'Filament profile');
|
||||
const label = isUsed
|
||||
? baseLabel
|
||||
: `${baseLabel} ${t('slice.notUsedByPlate', '— not used by this plate')}`;
|
||||
return (
|
||||
<PresetDropdown
|
||||
key={`filament-${idx}`}
|
||||
label={label}
|
||||
slot="filament"
|
||||
data={presetsQuery.data}
|
||||
value={filamentPresets[idx] ?? null}
|
||||
onChange={(ref) =>
|
||||
setFilamentPresets((current) => {
|
||||
const next = current.length === filamentSlots.length
|
||||
? [...current]
|
||||
: filamentSlots.map((_, i) => current[i] ?? null);
|
||||
next[idx] = ref;
|
||||
return next;
|
||||
})
|
||||
}
|
||||
disabled={isEnqueuing || !isUsed}
|
||||
swatchColor={filamentSlots.length > 1 ? slot.color : undefined}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user