Repair shipped default materials and obsolete settings, and validate them (#15741)

* add orca profile skill

* add default material check

Improve validation for default materials and filament profiles

* Fix default materials and obsolete keys

* clarifying orca-profiles skill
This commit is contained in:
SoftFever
2026-09-18 00:39:46 +08:00
committed by GitHub
parent 60b4a61854
commit f520e9221f
2135 changed files with 85633 additions and 87858 deletions
+61 -50
View File
@@ -24,9 +24,11 @@ options shared by several commands:
since the snapshot describes resources/profiles alone
After adding, renaming or deleting profile files, run:
normalize -> trim -> update-index -> generate-id -> update-snapshot -> check
Each step feeds the next: normalize writes the "type" update-index files a
profile by, and trim judges against the index update-index is about to rebuild.
normalize -> update-index -> generate-id -> update-snapshot -> check
normalize supplies missing types; update-index registers presets before id
generation. update-snapshot is needed when filament ids or claims change.
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
the current index and can delete newly added, unindexed presets.
Run from anywhere; "python scripts/orca_profile_tool.py --help" repeats this list
and "... <command> --help" documents one command in full.
@@ -128,6 +130,9 @@ BAMBU_MAP_PATH = os.path.normpath(
os.path.join(SCRIPTS_DIR, "..", "resources", "printers", "bambu_filament_ids.json"))
OFL = "OrcaFilamentLibrary"
# The validator's data dir, created under resources/profiles by a local run;
# not a vendor bundle, so an unscoped pass leaves it alone.
USER_DIR = "user"
# Bambu (BBL) is the only vendor exempt from the setting_id rule: it keeps its
# authoritative "G*" cloud ids. No vendor is exempt from the filament_id rule.
@@ -146,7 +151,9 @@ PROFILE_TYPES = ("machine_model", "process", "filament", "machine")
# Data files that sit under a vendor bundle but are not presets: no name, no type.
NON_PROFILE_FILES = {"filaments_color_codes.json", "cli_config.json"}
# Settings dropped from PrintConfig.cpp. Reported by "check --obsolete-keys".
# Mirror PrintConfigDef::handle_legacy's ignore set in PrintConfig.cpp; a test
# checks parity. Used by normalize and check. Active options and
# legacy aliases that the loader migrates do not belong here.
OBSOLETE_KEYS = {
"acceleration", "scale", "rotate", "duplicate", "duplicate_grid",
"bed_size", "print_center", "g0", "wipe_tower_per_color_wipe",
@@ -158,10 +165,11 @@ OBSOLETE_KEYS = {
"bed_temperature_initial_layer", "can_switch_nozzle_type", "can_add_auxiliary_fan",
"extra_flush_volume", "spaghetti_detector", "adaptive_layer_height",
"z_hop_type", "z_lift_type", "bed_temperature_difference", "long_retraction_when_cut",
"retraction_distance_when_cut", "extruder_type", "internal_bridge_support_thickness",
"extruder_clearance_max_radius", "top_area_threshold", "reduce_wall_solid_infill",
"retraction_distance_when_cut", "internal_bridge_support_thickness",
"top_area_threshold", "reduce_wall_solid_infill",
"filament_load_time", "filament_unload_time", "smooth_coefficient",
"overhang_totally_speed", "silent_mode", "overhang_speed_classic"
"overhang_totally_speed", "silent_mode", "overhang_speed_classic",
"anisotropic_surfaces",
}
# Keys renamed at some point, whose old and new spellings must never co-exist:
@@ -1050,13 +1058,12 @@ def load_available_filament_profiles(profiles_dir, vendor):
def check_machine_default_materials(profiles_dir, vendor):
"""Every default material a machine names must exist, in the bundle or in OFL.
Returns (errors, warnings); the warning is the bundle having no machine/ at all.
Returns (errors, warnings); a bundle with no machine/ has nothing to check.
"""
error_count = 0
machine_dir = Path(profiles_dir) / vendor / "machine"
if not machine_dir.exists():
print_warning(f"No machine profiles found for vendor: {vendor}")
return 0, 1
return 0, 0
available = (load_available_filament_profiles(profiles_dir, vendor)
| load_available_filament_profiles(profiles_dir, OFL))
@@ -1246,7 +1253,7 @@ def check_filament_id_length(profiles_dir, vendor):
def check_obsolete_keys(profiles_dir, vendor):
"""Warn about settings PrintConfig.cpp no longer defines. Returns the count."""
"""Warn about settings PrintConfig.cpp explicitly discards. Returns the count."""
warn_count = 0
profiles_path = Path(profiles_dir)
vendor_path = profiles_path / vendor / "filament"
@@ -1397,16 +1404,17 @@ def check_normalized(profiles_dir, vendor):
# check
# ---------------------------------------------------------------------------
def check_profiles(profiles_dir=PROFILES_DIR, vendors=None, snapshot_path=SNAPSHOT_PATH,
materials=False, obsolete_keys=False):
def check_profiles(profiles_dir=PROFILES_DIR, vendors=None, snapshot_path=SNAPSHOT_PATH):
"""Validate the whole profile tree. Returns the error count.
The per-vendor checks honour `vendors`; the setting_id and filament_id checks are
cross-vendor properties a narrowed run cannot answer, so they always cover the
whole tree. With no `vendors`, OrcaFilamentLibrary is left out of the per-vendor
pass: it is the shared base bundle, its filaments are generic by design, and they
are checked through the vendors that inherit them. Naming it explicitly checks it.
The normalization pass covers it either way - see the comment on that loop.
whole tree. With no `vendors`, every bundle is checked except the `user` directory
a local validator run leaves behind, being its data dir rather than a bundle;
naming it explicitly checks it. OrcaFilamentLibrary is checked like any other
bundle, its only exemption being that a library filament may leave
compatible_printers empty - what check_filament_compatible_printers applies. The
normalization pass takes its own vendor list - see the comment on that loop.
"""
print_info("Checking profiles ...")
errors_found = 0
@@ -1416,19 +1424,17 @@ def check_profiles(profiles_dir=PROFILES_DIR, vendors=None, snapshot_path=SNAPSH
if vendors:
checked = list(vendors)
else:
checked = [v for v in list_profile_dirs(profiles_dir) if v != OFL]
checked = [v for v in list_profile_dirs(profiles_dir) if v != USER_DIR]
for vendor in checked:
errors_found += check_preset_name_uniqueness(profiles_dir, vendor)
errors_found += check_filament_compatible_printers(profiles_dir, vendor)
if materials:
new_errors, new_warnings = check_machine_default_materials(profiles_dir, vendor)
errors_found += new_errors
warnings_found += new_warnings
new_errors, new_warnings = check_machine_default_materials(profiles_dir, vendor)
errors_found += new_errors
warnings_found += new_warnings
if obsolete_keys:
warnings_found += check_obsolete_keys(profiles_dir, vendor)
warnings_found += check_obsolete_keys(profiles_dir, vendor)
new_errors, new_warnings = check_name_consistency(profiles_dir, vendor)
errors_found += new_errors
@@ -1445,12 +1451,11 @@ def check_profiles(profiles_dir=PROFILES_DIR, vendors=None, snapshot_path=SNAPSH
errors_found += new_errors
remedies.update(gaps)
# normalize and update-index know nothing of the OrcaFilamentLibrary exemption
# above - that bundle sits out the per-vendor pass because its filaments are
# generic by design, which says nothing about the shape of its files - so this pass
# takes its own vendor list. Unscoped that is the bundles with an index, exactly
# what those two commands take; a --vendor is passed through as given, so a bundle
# whose index has not landed yet still has its files held to what normalize writes.
# normalize and update-index judge file and index shape, not the preset-content
# rules above, so this pass takes its own vendor list. Unscoped that is the
# bundles with an index, exactly what those two commands take; a --vendor is
# passed through as given, so a bundle whose index has not landed yet still has
# its files held to what normalize writes.
for vendor in (vendors or list_vendor_names(profiles_dir)):
new_errors, gaps = check_normalized(profiles_dir, vendor)
errors_found += new_errors
@@ -2061,6 +2066,10 @@ def _normalize_profile(data, sub):
del data[field]
changes.append(f"remove {field}")
for field in sorted(OBSOLETE_KEYS.intersection(data)):
del data[field]
changes.append(f"remove {field}")
# BBS renamed extruder_clearance_radius to extruder_clearance_max_radius, but some
# profiles carry both with different values, and the slicer cannot tell which one
# to obey - a toolhead collision waiting to happen. Keep the larger one only.
@@ -2306,8 +2315,9 @@ def build_index_sections(profiles_dir, vendor, profile_types=None):
one message each, for the caller to report. `sections` is None when two files claim
one preset name: the bundle can only hold one profile under a name, so rebuilding
would pick a winner by directory order and quietly drop the other, and the index has
to be left alone instead. Deleting the stale copy is trim's job, which is why it
runs before this.
to be left alone instead. Identify the intended preset and delete or rename the
duplicate before retrying. Use trim only for deliberate unindexed-file cleanup,
previewed with --dry-run.
"""
vendor_dir = os.path.join(profiles_dir, vendor)
sections = {}
@@ -2360,8 +2370,8 @@ def build_index_sections(profiles_dir, vendor, profile_types=None):
problems.append(f'{vendor}.json: {len(subs)} profiles are named "{name}" '
f'({", ".join(sorted(subs))}); only one can be indexed under '
f"that name, so delete or rename the others - "
f'"python scripts/orca_profile_tool.py trim" removes an '
f"unindexed copy")
f"preview unindexed-file cleanup with "
f'"python scripts/orca_profile_tool.py trim --dry-run"')
return (None if clashes else sections), problems
@@ -2433,9 +2443,11 @@ examples:
re-record the sanctioned filament_id state after a generate-id run
after adding, renaming or deleting profile files, run in this order:
normalize -> trim -> update-index -> generate-id -> update-snapshot -> check
each step feeds the next: normalize writes the "type" update-index files a
profile by, and trim judges against the index update-index is about to rebuild.
normalize -> update-index -> generate-id -> update-snapshot -> check
normalize supplies missing types; update-index registers presets before id
generation. update-snapshot is needed when filament ids or claims change.
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
the current index and can delete newly added, unindexed presets.
"""
@@ -2483,23 +2495,18 @@ def build_parser():
name, parents=parents, help=help_text, description=description,
allow_abbrev=False, formatter_class=argparse.RawDescriptionHelpFormatter)
check_cmd = add(
add(
"check", [vendor_opt, snapshot_opt, profiles_opt],
"validate the whole profile tree -- what CI runs",
"Validate the whole profile tree: preset name uniqueness, index coverage\n"
"both ways, compatible_printers, conflicting and vector-typed keys,\n"
"filament_id length, that normalize and update-index would leave every\n"
"bundle alone, and the tree-wide setting_id and filament_id state.\n"
"Exits nonzero on errors.\n"
"both ways, compatible_printers, default-material references, obsolete,\n"
"conflicting and vector-typed keys, filament_id length, that normalize and\n"
"update-index would leave every bundle alone, and the tree-wide setting_id\n"
"and filament_id state. Exits nonzero on errors.\n"
"\n"
"--vendor narrows the per-vendor checks only: setting_id uniqueness and the\n"
"filament_id state are cross-vendor properties a narrowed run cannot answer,\n"
"so they always cover the whole tree.")
check_cmd.add_argument("--materials", action="store_true",
help="also check that every default material a machine names "
"exists")
check_cmd.add_argument("--obsolete-keys", action="store_true", dest="obsolete_keys",
help="also warn about settings the slicer no longer defines")
generate_cmd = add(
"generate-id", [vendor_opt, dry_run_opt, profiles_opt],
@@ -2533,6 +2540,9 @@ def build_parser():
"loader only ever reads the sub_paths listed there, so an unindexed preset\n"
"never loads.\n"
"\n"
"Use only for deliberate cleanup, previewed with --dry-run. Newly added,\n"
"unindexed presets can be deleted too; omit trim from the authoring workflow.\n"
"\n"
"Assets and data files are kept, a file that cannot be parsed is kept and\n"
"reported, and so is one a surviving profile inherits from that no indexed\n"
"profile provides -- but a stale copy of an indexed profile goes, since\n"
@@ -2546,7 +2556,9 @@ def build_parser():
"A profile is indexed under the section its own \"type\" names, so run\n"
"normalize first: it is what writes a missing type. Two files claiming one\n"
"preset name leave that index alone, because a rebuild could only keep one\n"
"of them; run trim first, which is what clears a stale copy.")
"of them. Identify the intended preset and delete or rename the duplicate.\n"
"Use trim only for deliberate unindexed-file cleanup, previewed with\n"
"--dry-run; it can also delete newly authored presets.")
add("update-snapshot", [dry_run_opt, snapshot_opt, profiles_opt],
"re-record scripts/filament_id_snapshot.json",
@@ -2591,8 +2603,7 @@ def main(argv=None):
snapshot_path = SNAPSHOT_PATH
if args.command == "check":
errors = check_profiles(profiles_dir, vendors, snapshot_path,
materials=args.materials, obsolete_keys=args.obsolete_keys)
errors = check_profiles(profiles_dir, vendors, snapshot_path)
return 1 if errors else 0
if args.command == "generate-id":