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 --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.
- Continuous monitoring preserves FAILED/PAUSE outcome codes on timeout and reports final state on interruption. Footprints correspond to plate 1, matching the inspected job and preview.
Expand Down
79 changes: 79 additions & 0 deletions internal/cmd/settings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cmd

import (
"io"
"reflect"
"testing"

"github.com/alecthomas/kong"

"github.com/voska/bambu/internal/recipe"
"github.com/voska/bambu/internal/slicer"
)

func settingConfigs(t *testing.T, sets []string) *slicer.Configs {
t.Helper()
var cli CLI
parser, err := kong.New(&cli, kong.Writers(io.Discard, io.Discard), kong.Vars{"models": "X1C"})
if err != nil {
t.Fatal(err)
}
args := []string{"slice", "part.stl", "--recipe", "prototype-pla"}
for _, value := range sets {
args = append(args, "--set", value)
}
if _, err := parser.Parse(args); err != nil {
t.Fatal(err)
}
ix, err := slicer.LoadIndex("../slicer/testdata/resources")
if err != nil {
t.Fatal(err)
}
cfg, err := slicer.Build(ix, slicer.Request{Recipe: recipe.Recipe{Process: "0.20mm Standard", Filament: "Bambu PLA Basic"}, MachinePreset: "Test Printer 0.4 nozzle", Plate: "textured_plate", Sets: cli.Slice.Set})
if err != nil {
t.Fatalf("settings %v: %v", sets, err)
}
return cfg
}

func TestSliceSettingLegacyEscapesAndTrailingSeparator(t *testing.T) {
for _, sets := range [][]string{{`machine_start_gcode=G28 ; first\,second,wall_loops=4`}, {"wall_loops=4,"}} {
cfg := settingConfigs(t, sets)
if cfg.Process["wall_loops"] != "4" {
t.Fatalf("lost wall override: %v", cfg.Applied)
}
if len(cfg.Applied) == 2 && cfg.Machine["machine_start_gcode"] != "G28 ; first,second" {
t.Fatalf("escaped comma changed: %v", cfg.Applied)
}
}
}

func TestSliceSettingJSONEscapes(t *testing.T) {
cfg := settingConfigs(t, []string{`nozzle_temperature=["250,260","quoted \"text\"","back\\slash"]`})
if !reflect.DeepEqual(cfg.Filaments[0]["nozzle_temperature"], []any{"250,260", `quoted "text"`, `back\slash`}) {
t.Fatalf("JSON escapes changed: %v", cfg.Applied)
}
}

func TestSliceSettingScalarBrackets(t *testing.T) {
for _, value := range []string{`; [banner`, `; "banner`, `; banner=[text`} {
cfg := settingConfigs(t, []string{"machine_start_gcode=" + value + ",wall_loops=4"})
if cfg.Machine["machine_start_gcode"] != value || cfg.Process["wall_loops"] != "4" {
t.Fatalf("scalar mistaken for JSON: %v", cfg.Applied)
}
}
}

func TestSliceSettingListsAndBatches(t *testing.T) {
for _, sets := range [][]string{
{`nozzle_temperature=["250","250"]`, "wall_loops=4", "sparse_infill_density=30%"},
{`nozzle_temperature=["250","250"],wall_loops=4,sparse_infill_density=30%`},
{`wall_loops=4,sparse_infill_density=30%,nozzle_temperature=["250","250"]`},
{`nozzle_temperature=["250"\,"250"],wall_loops=4,sparse_infill_density=30%`},
} {
cfg := settingConfigs(t, sets)
if !reflect.DeepEqual(cfg.Filaments[0]["nozzle_temperature"], []any{"250", "250"}) || cfg.Process["wall_loops"] != "4" || cfg.Process["sparse_infill_density"] != "30%" {
t.Fatalf("overrides lost: %v", cfg.Applied)
}
}
}
2 changes: 1 addition & 1 deletion internal/cmd/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type SliceCmd struct {
Filament []string `short:"f" sep:"none" help:"Filament preset to use instead of the recipe's (e.g. \"Generic PLA\", \"Bambu PLA Matte\"). Repeat once per filament, in print order, for a multi-colour job."`
Color []string `name:"color" aliases:"colour" sep:"none" placeholder:"RRGGBB" help:"Spool colour per --filament, same order (e.g. FFFFFF). Required with two or more filaments: Bambu Studio sizes the purge between them from the colours."`
ChangeZ []float64 `name:"filament-change-z" sep:"none" placeholder:"Z" help:"The next --filament starts on the first layer above Z mm (the top of the previous filament's last layer); the AMS swaps automatically. Once per filament after the first."`
Set []string `short:"s" help:"Override a Bambu Studio setting: key=value (repeatable)." placeholder:"KEY=VALUE"`
Set []string `short:"s" sep:"none" help:"Override a Bambu Studio setting: key=value (repeatable; comma-separated batches and JSON lists supported)." placeholder:"KEY=VALUE"`
Name string `short:"n" help:"Output base name (default <model>-<recipe>)."`
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)."`
Expand Down
63 changes: 63 additions & 0 deletions internal/slicer/settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package slicer

import "strings"

// splitSets preserves JSON values while retaining Kong's batch and escaped-comma behavior.
func splitSets(values []string) []string {
var out []string
for _, value := range values {
var token strings.Builder
depth := 0
quoted, escaped := false, false
for i, ch := range value {
switch {
case escaped:
if ch != ',' {
token.WriteRune('\\')
}
token.WriteRune(ch)
escaped = false
continue
case ch == '\\' && i < len(value)-1:
escaped = true
continue
}
if quoted {
token.WriteRune(ch)
if ch == '"' {
quoted = false
}
continue
}
switch ch {
case '"':
quoted = depth > 0
case '[':
if depth > 0 {
depth++
} else if _, prefix, ok := strings.Cut(token.String(), "="); ok && strings.TrimSpace(prefix) == "" {
depth = 1 // a JSON list starts at the beginning of the setting value
}
case '{':
if depth > 0 {
depth++
}
case ']', '}':
if depth > 0 {
depth--
}
case ',':
if depth == 0 {
out = append(out, token.String())
token.Reset()
continue
}
}
token.WriteRune(ch)
}
if token.Len() > 0 {
out = append(out, token.String())
}
}
return out
}
2 changes: 1 addition & 1 deletion internal/slicer/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func Build(ix *Index, req Request) (*Configs, error) {
}
apply([]map[string]any{process}, req.Recipe.ProcessOverrides)
apply(c.Filaments, req.Recipe.FilamentOverrides)
for _, s := range req.Sets {
for _, s := range splitSets(req.Sets) {
k, v, ok := strings.Cut(s, "=")
k, v = strings.TrimSpace(k), strings.TrimSpace(v)
if !ok || k == "" {
Expand Down
Loading