Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ JSON field names and exit codes are part of the public API.
- `preflight` `single_filament` now fails only when the number of `--slot`s differs from the number of filaments in the plate. Single-filament jobs send the same payload as before.

### Fixed
- `slice --strict` rejects slicer warnings, including floating regions, with exit 11 before publishing new output files. JSON errors include the warnings and slicer log path; slicing without `--strict` still reports warnings in the summary.
- `slice --set` now preserves JSON list values, including commas inside quoted entries; comma-separated batches and repeated overrides remain supported.
- `print pause` and `print stop` now verify PAUSE or an idle state before exiting 0; `--wait` defaults to 30s and must be positive.
- A failed retry cannot reuse a prior successful slice. Every slice verifies its requested filament changes, including an empty change list.
Expand Down
4 changes: 3 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,9 @@ All recipes use `brim_type = no_brim`. **Brims are opt-in** (`--set brim_type=ou
`preflight` and `print send` accept `--strict`, retaining the Python operator's gates while keeping the existing Go
default: material variants must match exactly, temperature bounds must both be finite and valid, and unknown nozzle
material produces a WARN rather than an assumed match. `preflight.strict` records the selected mode. `slice --strict`
requires an extracted plate preview before publishing the new 3MF. All slices reject unexpected tool changes and stale
requires an extracted plate preview and rejects slicer warnings (including floating regions) with exit 11 before
publishing any new slice outputs. The JSON error includes the warning text and slicer log path. Without `--strict`,
warnings remain in the successful slice summary. All slices reject unexpected tool changes and stale
output from a previous invocation.

| gate | FAIL when | WARN when |
Expand Down
3 changes: 2 additions & 1 deletion docs/operator-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ remain in place. A multi-filament `slice --slot` records one suggested slot; act

Use `--strict` for operator slice/preflight/send. Default Go gate behavior stays compatible, including same-family
material warnings and omitted temperature bounds. Strict mode fails unequal material types and unknown or malformed
temperature bounds. A different product/profile remains a WARN in both tools. The strict slice requires a preview.
temperature bounds. A different product/profile remains a WARN in both tools. The strict slice requires a preview
and rejects slicer warnings with exit 11 before publishing new outputs.

Go's slice JSON is the summary directly, without Python's `slice` wrapper. It includes
`objects[].footprint_incl_brim_mm`, the first plate's dimensions, and `suggested_slot`. `status` keeps a `status` wrapper
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type SliceCmd struct {
Out string `short:"o" help:"Output directory (default: config output_dir, else current dir)." type:"path"`
AutoOrient bool `name:"auto-orient" help:"Let the slicer re-orient the model (default: keep the model's orientation)."`
Slot string `help:"Record the intended AMS slot in the summary; send still requires --slot."`
Strict bool `help:"Require a plate preview as part of the slice evidence."`
Strict bool `help:"Require a plate preview and reject slicer warnings before publishing outputs."`
}

// Run executes the command.
Expand Down Expand Up @@ -82,7 +82,7 @@ func (c *SliceCmd) Run(g *Globals) error {
Model: c.Model, Recipe: r, MachinePreset: m.MachinePreset(p.NozzleString()), Plate: p.Plate,
Filaments: c.Filament, Colors: c.Color, ChangeZs: c.ChangeZ, Sets: c.Set, Name: name, OutDir: out,
WorkDir: filepath.Join(cache, "bambu", "slice", name), AutoOrient: c.AutoOrient,
StepPython: expand(cfg.Slicer.StepPython), SuggestedSlot: c.Slot, RequirePreview: c.Strict,
StepPython: expand(cfg.Slicer.StepPython), SuggestedSlot: c.Slot, Strict: c.Strict,
})
if err != nil {
return err
Expand Down
75 changes: 74 additions & 1 deletion internal/slicer/parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"

"github.com/voska/bambu/internal/errfmt"
"github.com/voska/bambu/internal/testutil"
)

func TestRunDoesNotReuseStaleSlice(t *testing.T) {
Expand Down Expand Up @@ -84,7 +86,7 @@ func TestStrictSliceNeedsPreview(t *testing.T) {
t.Skip("shell fake")
}
_, rq, err := twoFilamentRun(t, func(r *Request) {
r.RequirePreview = true
r.Strict = true
zr, err := zip.OpenReader(os.Getenv("FAKE_3MF"))
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -126,6 +128,77 @@ func TestStrictSliceNeedsPreview(t *testing.T) {
}
}

func TestStrictSliceWarnings(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell fake")
}
const floating = "It seems object CadQuery Component has floating regions. Please re-orient the object or enable support generation."
for _, tc := range []struct {
name string
strict bool
warning string
}{
{"strict floating regions", true, floating},
{"strict other warning", true, "Unsupported regions detected."},
{"strict clean slice", true, ""},
{"default floating regions", false, floating},
} {
t.Run(tc.name, func(t *testing.T) {
sum, rq, err := twoFilamentRun(t, func(r *Request) {
r.Strict = tc.strict
r.Filaments, r.Colors, r.ChangeZs = nil, nil, nil
t.Setenv("FAKE_3MF", testutil.Write(t, t.TempDir(), "prepared", testutil.Opts{}))
b, err := os.ReadFile(os.Getenv("FAKE_RESULT"))
if err != nil {
t.Fatal(err)
}
var res map[string]any
if err := json.Unmarshal(b, &res); err != nil {
t.Fatal(err)
}
res["sliced_plates"].([]any)[0].(map[string]any)["warning_message"] = tc.warning
b, err = json.Marshal(res)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "warning-result.json")
if err := os.WriteFile(path, b, 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("FAKE_RESULT", path)
})
if tc.strict && tc.warning != "" {
if err == nil {
t.Fatal("strict slice accepted a slicer warning")
}
e := errfmt.As(err)
if e.Code != errfmt.ExitSliceFailed || !strings.Contains(e.Message, tc.warning) ||
!reflect.DeepEqual(e.Data["warnings"], []string{tc.warning}) || e.Data["log"] == "" {
t.Fatalf("warning must be reported as a structured slice failure: %+v", e)
}
if sum != nil {
t.Fatal("rejected slice returned a success summary")
}
entries, err := os.ReadDir(rq.OutDir)
if err != nil || len(entries) != 0 {
t.Fatalf("rejected slice must not publish any output: %v %v", entries, err)
}
return
}
if err != nil {
t.Fatal(err)
}
want := []string{}
if tc.warning != "" {
want = append(want, tc.warning)
}
if !reflect.DeepEqual(sum.Warnings, want) || !exists(sum.Gcode3MF) || !exists(sum.SummaryJSON) {
t.Fatalf("successful slice must preserve warnings and publish outputs: %+v", sum)
}
})
}
}

func TestSTEPConversion(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell fake")
Expand Down
48 changes: 28 additions & 20 deletions internal/slicer/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,21 @@ var CLIErrors = map[int]string{

// Request is one slice job.
type Request struct {
Model string
Recipe recipe.Recipe
MachinePreset string // e.g. "Bambu Lab X1 Carbon 0.4 nozzle"
Plate string // installed plate id, e.g. "textured_plate"
Filaments []string // filament presets in print order (default: the recipe's)
Colors []string // spool colour per filament (RRGGBB); Studio sizes the purge between filaments from them
ChangeZs []float64 // filament n+1 starts on the first layer above ChangeZs[n-1]
Sets []string // key=value overrides
Name string // output base name
OutDir string // where the .gcode.3mf/.png/.summary.json go
WorkDir string // scratch dir for configs and slicer output
AutoOrient bool
StepPython string // interpreter with CadQuery for STEP input (default python3)
SuggestedSlot string
RequirePreview bool
Model string
Recipe recipe.Recipe
MachinePreset string // e.g. "Bambu Lab X1 Carbon 0.4 nozzle"
Plate string // installed plate id, e.g. "textured_plate"
Filaments []string // filament presets in print order (default: the recipe's)
Colors []string // spool colour per filament (RRGGBB); Studio sizes the purge between filaments from them
ChangeZs []float64 // filament n+1 starts on the first layer above ChangeZs[n-1]
Sets []string // key=value overrides
Name string // output base name
OutDir string // where the .gcode.3mf/.png/.summary.json go
WorkDir string // scratch dir for configs and slicer output
AutoOrient bool
StepPython string // interpreter with CadQuery for STEP input (default python3)
SuggestedSlot string
Strict bool // require a preview and reject slicer warnings before publishing outputs
}

// Summary is the slice result (the --json contract).
Expand Down Expand Up @@ -498,12 +498,23 @@ func Run(ctx context.Context, st *Studio, ix *Index, req Request) (*Summary, err
WithHint("%s; fix the recipe/--set values; log: %s", or(CLIErrors[res.ReturnCode], "see log"), logPath).
WithData("return_code", res.ReturnCode).WithData("details", details).WithData("log", logPath)
}
warnings := []string{}
for _, p := range res.SlicedPlates {
if p.WarningMessage != "" {
warnings = append(warnings, p.WarningMessage)
}
}
if req.Strict && len(warnings) > 0 {
return nil, errfmt.New(errfmt.ExitSliceFailed, "strict slice rejected slicer warnings: %s", strings.Join(warnings, "; ")).
WithHint("no new slice outputs were published; resolve the warnings and inspect the G-code before printing; log: %s", logPath).
WithData("warnings", warnings).WithData("log", logPath)
}
if err := checkChanges(filepath.Join(work, threemf), tops, logPath); err != nil {
return nil, err
}
png := filepath.Join(out, name+".png")
if err := extract(filepath.Join(work, threemf), "Metadata/plate_1.png", png); err != nil {
if req.RequirePreview {
if req.Strict {
return nil, errfmt.Wrap(errfmt.ExitSliceFailed, err, "slicer did not produce a plate preview")
}
png = ""
Expand All @@ -522,7 +533,7 @@ func Run(ctx context.Context, st *Studio, ix *Index, req Request) (*Summary, err
TimeS: j.PredictionS, Time: j.TotalTime, WeightG: j.WeightG, Layers: j.Layers, Filaments: j.Filaments,
FilamentChanges: j.FilamentChanges, FilamentPresets: cfgs.FilamentPresets,
BedType: j.BedType, BedTemp: j.BedTemp, NozzleTemp: j.NozzleTemp, NozzleFirst: j.NozzleTempFirst,
Presets: cfgs.Presets, Overrides: cfgs.Applied, Notes: cfgs.Notes, Warnings: []string{}, Settings: j.Settings,
Presets: cfgs.Presets, Overrides: cfgs.Applied, Notes: cfgs.Notes, Warnings: warnings, Settings: j.Settings,
AutoOrient: req.AutoOrient, SlicerLog: logPath, StudioVersion: st.Version,
SuggestedSlot: req.SuggestedSlot, Objects: []Object{},
}
Expand All @@ -538,9 +549,6 @@ func Run(ctx context.Context, st *Studio, ix *Index, req Request) (*Summary, err
}
}
}
if p.WarningMessage != "" {
sum.Warnings = append(sum.Warnings, p.WarningMessage)
}
sum.PrimeTower = sum.PrimeTower || p.FeatureTypeTimes["Prime tower"] > 0
for _, u := range p.Filaments {
for i := range sum.Filaments {
Expand Down
Loading