#!/usr/bin/env python3 """ OrcaSlicer Config Code Generator Reads compiled protobuf descriptor set and generates C++ source files that replace hand-written config registration, preset lists, and invalidation chains. Usage: # Step 1: Compile .proto files to a descriptor set protoc --proto_path=src/PrintConfigs --descriptor_set_out=config.desc \ --include_imports src/PrintConfigs/*.proto # Step 2: Generate Python bindings (one-time, or when config_metadata.proto changes) protoc --proto_path=src/PrintConfigs --python_out=tools/ config_metadata.proto # Step 3: Run codegen python tools/config_codegen.py config.desc codegen/generated/ Outputs: - PrintConfigDef_generated.cpp (init_fff_params body) - Preset_options_generated.cpp (s_Preset_*_options arrays) - Invalidation_generated.cpp (opt_key -> steps map) - OptionKeys_generated.cpp (extruder/filament key lists) """ import sys import os import re import argparse from pathlib import Path # Add tools/ to path so we can import generated config_metadata_pb2 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) try: from google.protobuf import descriptor_pb2 # Import the generated bindings - this registers extensions globally import config_metadata_pb2 as meta_pb2 except ImportError as e: print(f"ERROR: {e}") print("Ensure google-protobuf is installed: pip install protobuf") print("And that config_metadata_pb2.py exists in tools/") print("Generate it with: protoc --proto_path=src/PrintConfigs --python_out=tools/ config_metadata.proto") sys.exit(1) # Proto FieldDescriptorProto.Type enum values TYPE_DOUBLE = 1 TYPE_FLOAT = 2 TYPE_INT64 = 3 TYPE_UINT64 = 4 TYPE_INT32 = 5 TYPE_FIXED64 = 6 TYPE_FIXED32 = 7 TYPE_BOOL = 8 TYPE_STRING = 9 TYPE_MESSAGE = 11 TYPE_UINT32 = 13 TYPE_ENUM = 14 TYPE_SINT32 = 17 TYPE_SINT64 = 18 # Proto label LABEL_OPTIONAL = 1 LABEL_REQUIRED = 2 LABEL_REPEATED = 3 def mode_to_cpp(mode_val): """Convert mode enum value to C++ constant.""" return { meta_pb2.MODE_SIMPLE: "comSimple", meta_pb2.MODE_ADVANCED: "comAdvanced", meta_pb2.MODE_DEVELOP: "comDevelop", }.get(mode_val, "comAdvanced") _PRINT_STEPS = None # set after meta_pb2 import resolves enum values _OBJECT_STEPS = None def _init_step_sets(): global _PRINT_STEPS, _OBJECT_STEPS if _PRINT_STEPS is None: _PRINT_STEPS = {meta_pb2.STEP_GCODE_EXPORT, meta_pb2.STEP_SKIRT_BRIM, meta_pb2.STEP_WIPE_TOWER} _OBJECT_STEPS = {meta_pb2.STEP_SLICE, meta_pb2.STEP_PERIMETERS, meta_pb2.STEP_INFILL, meta_pb2.STEP_SUPPORT} def step_to_cpp(step_val): """Convert invalidation step to C++ constant.""" return { meta_pb2.STEP_GCODE_EXPORT: "psGCodeExport", meta_pb2.STEP_SKIRT_BRIM: "psSkirtBrim", meta_pb2.STEP_WIPE_TOWER: "psWipeTower", meta_pb2.STEP_SLICE: "posSlice", meta_pb2.STEP_PERIMETERS: "posPerimeters", meta_pb2.STEP_INFILL: "posInfill", meta_pb2.STEP_SUPPORT: "posSupportMaterial", meta_pb2.STEP_NONE: "", }.get(step_val, "") def proto_type_to_co_type(field_desc, is_nullable=False): """ Map a protobuf field descriptor to OrcaSlicer's coXXX type constant and ConfigOptionXXX class name. Returns: (co_type_str, config_option_class, is_vector) """ ftype = field_desc.type is_repeated = (field_desc.label == LABEL_REPEATED) type_name = field_desc.type_name # For message types # Handle message types (FloatOrPercent, Point2D) if ftype == TYPE_MESSAGE: if "FloatOrPercent" in type_name: if is_repeated: return ("coFloatsOrPercents", "ConfigOptionFloatsOrPercents", True) return ("coFloatOrPercent", "ConfigOptionFloatOrPercent", False) elif "Point2D" in type_name: if is_repeated: return ("coPoints", "ConfigOptionPoints", True) return ("coPoint", "ConfigOptionPoint", False) # Handle enum types if ftype == TYPE_ENUM: if is_repeated: return ("coEnums", "ConfigOptionEnumsGeneric", True) return ("coEnum", "ConfigOptionEnum", False) # Scalar/vector types if ftype in (TYPE_FLOAT, TYPE_DOUBLE): if is_repeated: if is_nullable: return ("coFloats", "ConfigOptionFloatsNullable", True) return ("coFloats", "ConfigOptionFloats", True) return ("coFloat", "ConfigOptionFloat", False) if ftype in (TYPE_INT32, TYPE_INT64, TYPE_SINT32, TYPE_SINT64, TYPE_UINT32, TYPE_UINT64, TYPE_FIXED32, TYPE_FIXED64): if is_repeated: if is_nullable: return ("coInts", "ConfigOptionIntsNullable", True) return ("coInts", "ConfigOptionInts", True) return ("coInt", "ConfigOptionInt", False) if ftype == TYPE_BOOL: if is_repeated: if is_nullable: return ("coBools", "ConfigOptionBoolsNullable", True) return ("coBools", "ConfigOptionBools", True) return ("coBool", "ConfigOptionBool", False) if ftype == TYPE_STRING: if is_repeated: return ("coStrings", "ConfigOptionStrings", True) return ("coString", "ConfigOptionString", False) return ("coNone", "ConfigOption", False) def parse_field_options(field_desc_proto): """ Re-parse FieldOptions from a FieldDescriptorProto with extensions registered. This is needed because the FileDescriptorSet parser doesn't know about our custom extensions, so they end up as unknown fields. Re-parsing with the extensions registered (via config_metadata_pb2 import) resolves them. """ from google.protobuf import descriptor_pb2 opts = field_desc_proto.options if not opts.ByteSize(): return descriptor_pb2.FieldOptions() # Re-parse the serialized options with extensions registered reparsed = descriptor_pb2.FieldOptions() reparsed.ParseFromString(opts.SerializeToString()) return reparsed class FieldInfo: """Parsed information about a single config field from proto descriptor.""" def __init__(self, field_desc): self.name = field_desc.name self.field_desc = field_desc # Re-parse options with extensions registered opts = parse_field_options(field_desc) # Read extensions using the proper protobuf API self.label = opts.Extensions[meta_pb2.label] or None self.full_label = opts.Extensions[meta_pb2.full_label] or None self.tooltip = opts.Extensions[meta_pb2.tooltip] or None self.category = opts.Extensions[meta_pb2.category] or None self.sidetext = opts.Extensions[meta_pb2.sidetext] or None self.min_value = opts.Extensions[meta_pb2.min_value] if opts.HasExtension(meta_pb2.min_value) else None self.max_value = opts.Extensions[meta_pb2.max_value] if opts.HasExtension(meta_pb2.max_value) else None self.max_literal = opts.Extensions[meta_pb2.max_literal] if opts.HasExtension(meta_pb2.max_literal) else None self.mode = opts.Extensions[meta_pb2.mode] # 0 = MODE_SIMPLE (default) self.has_mode = opts.HasExtension(meta_pb2.mode) self.ratio_over = opts.Extensions[meta_pb2.ratio_over] or None self.multiline = opts.Extensions[meta_pb2.multiline] self.full_width = opts.Extensions[meta_pb2.full_width] self.height = opts.Extensions[meta_pb2.height] or None self.is_nullable = opts.Extensions[meta_pb2.is_nullable] self.gui_type = opts.Extensions[meta_pb2.gui_type] or None self.gui_flags = opts.Extensions[meta_pb2.gui_flags] or None self.enum_keys_map = opts.Extensions[meta_pb2.enum_keys_map_ref] or None self.no_cli = opts.Extensions[meta_pb2.no_cli] self.readonly = opts.Extensions[meta_pb2.readonly] self.preset = opts.Extensions[meta_pb2.preset] # 0 = PRESET_PRINT self.invalidates = list(opts.Extensions[meta_pb2.invalidates]) self.list_membership = list(opts.Extensions[meta_pb2.list_membership]) self.legacy_name = opts.Extensions[meta_pb2.legacy_name] or None # Default value and enum metadata self.has_default = opts.Extensions[meta_pb2.has_default] self.default_value = opts.Extensions[meta_pb2.default_value] if self.has_default else None self.enum_value_entries = list(opts.Extensions[meta_pb2.enum_value_entries]) self.enum_label_entries = list(opts.Extensions[meta_pb2.enum_label_entries]) self.co_type_hint = opts.Extensions[meta_pb2.co_type_hint] or None # Resolve C++ type info - co_type_hint overrides auto-detection co_type, option_class, is_vec = proto_type_to_co_type( field_desc, self.is_nullable) if self.co_type_hint: co_type = self.co_type_hint # Fix up option_class for hint-overridden types hint_class_map = { "coPercent": "ConfigOptionPercent", "coPercents": "ConfigOptionPercents", "coEnum": "ConfigOptionEnum", "coEnums": "ConfigOptionEnumsGeneric", } if self.co_type_hint in hint_class_map: option_class = hint_class_map[self.co_type_hint] self.co_type = co_type self.option_class = option_class self.is_vector = is_vec class CodeGenerator: """Generates C++ source files from parsed proto descriptors.""" def __init__(self, descriptor_set): self.descriptor_set = descriptor_set self.fields = [] # All FieldInfo objects self.virtual_keys_by_preset = { # virtual_preset_keys per preset type meta_pb2.PRESET_PRINT: [], meta_pb2.PRESET_FILAMENT: [], meta_pb2.PRESET_PRINTER: [], } self._parse_all_fields() @staticmethod def _preset_type_from_filename(name: str) -> int: """Infer preset type from proto filename (printer/filament/print).""" n = name.lower() if "printer" in n: return meta_pb2.PRESET_PRINTER if "filament" in n: return meta_pb2.PRESET_FILAMENT return meta_pb2.PRESET_PRINT def _parse_all_fields(self): """Parse all message fields from all proto files in the descriptor set.""" for file_desc in self.descriptor_set.file: # Skip google/protobuf imports if file_desc.name.startswith("google/"): continue # Skip config_metadata.proto (it's just extensions, no settings) if "config_metadata" in file_desc.name: continue preset_type = self._preset_type_from_filename(file_desc.name) for msg_desc in file_desc.message_type: # Skip wrapper messages (FloatOrPercent, Point2D) if msg_desc.name in ("FloatOrPercent", "Point2D"): continue # Collect message-level virtual_preset_keys vkeys = list(msg_desc.options.Extensions[meta_pb2.virtual_preset_keys]) self.virtual_keys_by_preset[preset_type].extend(vkeys) for field_desc in msg_desc.field: self.fields.append(FieldInfo(field_desc)) def generate_init_fff_params(self) -> str: """ Generate the body of PrintConfigDef::init_fff_params(). Output: C++ code that's a drop-in replacement for the hand-written registrations. """ lines = [] lines.append("// ===== AUTO-GENERATED by tools/config_codegen.py =====") lines.append("// DO NOT EDIT MANUALLY. Edit .proto files and re-run codegen.") lines.append("") for field in self.fields: lines.append(f' def = this->add("{field.name}", {field.co_type});') if field.label: lines.append(f' def->label = L("{self._escape_cpp(field.label)}");') if field.full_label: lines.append(f' def->full_label = L("{self._escape_cpp(field.full_label)}");') if field.category: lines.append(f' def->category = L("{self._escape_cpp(field.category)}");') if field.tooltip: tooltip_escaped = self._escape_cpp(field.tooltip) # Split long tooltips across lines if len(tooltip_escaped) > 80: lines.append(f' def->tooltip = L("{tooltip_escaped}");') else: lines.append(f' def->tooltip = L("{tooltip_escaped}");') if field.sidetext: lines.append(f' def->sidetext = L("{self._escape_cpp(field.sidetext)}");') if field.min_value is not None: lines.append(f' def->min = {self._format_number(field.min_value)};') if field.max_value is not None: lines.append(f' def->max = {self._format_number(field.max_value)};') if field.max_literal is not None: lines.append(f' def->max_literal = {self._format_number(field.max_literal)};') if field.ratio_over: lines.append(f' def->ratio_over = "{field.ratio_over}";') if field.has_mode: lines.append(f' def->mode = {mode_to_cpp(field.mode)};') if field.is_nullable: lines.append(f' def->nullable = true;') if field.readonly: lines.append(f' def->readonly = true;') if field.multiline: lines.append(f' def->multiline = true;') if field.full_width: lines.append(f' def->full_width = true;') if field.height: lines.append(f' def->height = {field.height};') if field.gui_type: lines.append(f' def->gui_type = ConfigOptionDef::GUIType::{field.gui_type};') if field.gui_flags: lines.append(f' def->gui_flags = "{field.gui_flags}";') if field.no_cli: lines.append(f' def->cli = ConfigOptionDef::nocli;') if field.enum_keys_map: lines.append(f' def->enum_keys_map = &{field.enum_keys_map};') # Enum values/labels for ev in field.enum_value_entries: lines.append(f' def->enum_values.push_back("{self._escape_cpp(ev)}");') for el in field.enum_label_entries: lines.append(f' def->enum_labels.push_back(L("{self._escape_cpp(el)}"));') # Default value - reconstruct full C++ from co_type + default_value if field.has_default: cpp_expr = self._reconstruct_default_cpp( field.default_value or "", field.co_type, field.enum_keys_map, field.is_nullable) lines.append(f' def->set_default_value({cpp_expr});') lines.append("") return "\n".join(lines) def generate_preset_options(self) -> str: """Generate s_Preset_print_options, s_Preset_filament_options, etc.""" lines = [] lines.append("// ===== AUTO-GENERATED by tools/config_codegen.py =====") lines.append("") for var_name, preset_type in [ ("s_Preset_print_options", meta_pb2.PRESET_PRINT), ("s_Preset_filament_options", meta_pb2.PRESET_FILAMENT), ("s_Preset_printer_options", meta_pb2.PRESET_PRINTER), ]: # Field-derived keys + message-level virtual keys, deduplicated and sorted field_names = [f.name for f in self.fields if f.preset == preset_type] virtual_names = self.virtual_keys_by_preset[preset_type] all_names = sorted(set(field_names) | set(virtual_names)) lines.append(f"static const std::vector {var_name} = {{") for name in all_names: lines.append(f' "{name}",') lines.append("};") lines.append("") return "\n".join(lines) def generate_invalidation_map(self) -> str: """Generate opt_key -> invalidation steps mapping, split by PrintStep vs PrintObjectStep.""" _init_step_sets() lines = [] lines.append("// ===== AUTO-GENERATED by tools/config_codegen.py =====") lines.append("") lines.append("static const std::unordered_map> " "s_print_steps_map = {") for field in sorted(self.fields, key=lambda x: x.name): if field.invalidates: steps = [step_to_cpp(s) for s in field.invalidates if s in _PRINT_STEPS and step_to_cpp(s)] if steps: lines.append(f' {{"{field.name}", {{{", ".join(steps)}}}}},') lines.append("};") lines.append("") lines.append("static const std::unordered_map> " "s_object_steps_map = {") for field in sorted(self.fields, key=lambda x: x.name): if field.invalidates: steps = [step_to_cpp(s) for s in field.invalidates if s in _OBJECT_STEPS and step_to_cpp(s)] if steps: lines.append(f' {{"{field.name}", {{{", ".join(steps)}}}}},') lines.append("};") return "\n".join(lines) def generate_option_key_lists(self) -> str: """Generate extruder_option_keys, filament_option_keys, etc.""" lines = [] lines.append("// ===== AUTO-GENERATED by tools/config_codegen.py =====") lines.append("") extruder_keys = [f for f in self.fields if meta_pb2.LIST_EXTRUDER_OPTION_KEYS in f.list_membership] filament_keys = [f for f in self.fields if meta_pb2.LIST_FILAMENT_OPTION_KEYS in f.list_membership] for var_name, keys in [ ("s_extruder_option_keys", extruder_keys), ("s_filament_option_keys", filament_keys), ]: lines.append(f"static const std::vector {var_name} = {{") for f in sorted(keys, key=lambda x: x.name): lines.append(f' "{f.name}",') lines.append("};") lines.append("") return "\n".join(lines) @staticmethod def _reconstruct_default_cpp(default_value, co_type, enum_keys_map=None, is_nullable=False): """Reconstruct full C++ default expression from co_type + extracted value args. Maps (co_type, args) -> 'new ConfigOptionXxx(args)' or 'new ConfigOptionXxx{args}'. """ import re as _re # Type -> C++ class mappings SCALAR_CLASS = { "coFloat": "ConfigOptionFloat", "coBool": "ConfigOptionBool", "coInt": "ConfigOptionInt", "coString": "ConfigOptionString", "coPercent": "ConfigOptionPercent", "coFloatOrPercent": "ConfigOptionFloatOrPercent", "coPoint": "ConfigOptionPoint", "coPoint3": "ConfigOptionPoint3", } LIST_CLASS = { "coFloats": "ConfigOptionFloats", "coInts": "ConfigOptionInts", "coBools": "ConfigOptionBools", "coStrings": "ConfigOptionStrings", "coPercents": "ConfigOptionPercents", "coFloatsOrPercents": "ConfigOptionFloatsOrPercents", "coPoints": "ConfigOptionPoints", } # Unescape escaped quotes, then re-escape actual newlines so they remain valid # in C++ string literals (proto \n is parsed as actual newline by protobuf). args = default_value.replace('\\"', '"').replace('\n', '\\n') # Empty args -> default constructor for any type if not args: if co_type == "coEnum": enum_type = "int" if enum_keys_map: m = _re.match(r'ConfigOptionEnum<(\w+)>::', enum_keys_map) if m: enum_type = m.group(1) else: m2 = _re.match(r's_keys_map_(\w+)$', enum_keys_map) if m2: enum_type = m2.group(1) return f"new ConfigOptionEnum<{enum_type}>()" if co_type == "coEnums": cls = "ConfigOptionEnumsGenericNullable" if is_nullable else "ConfigOptionEnumsGeneric" return f"new {cls}{{}}" NULLABLE_LIST_CLASS = { "coFloats": "ConfigOptionFloatsNullable", "coInts": "ConfigOptionIntsNullable", "coBools": "ConfigOptionBoolsNullable", "coPercents": "ConfigOptionPercentsNullable", } all_classes = {**SCALAR_CLASS, **LIST_CLASS} if is_nullable and co_type in NULLABLE_LIST_CLASS: cls = NULLABLE_LIST_CLASS[co_type] else: cls = all_classes.get(co_type, "ConfigOption") return f"new {cls}()" if co_type in SCALAR_CLASS: return f"new {SCALAR_CLASS[co_type]}({args})" if co_type in LIST_CLASS: NULLABLE_LIST_CLASS = { "coFloats": "ConfigOptionFloatsNullable", "coInts": "ConfigOptionIntsNullable", "coBools": "ConfigOptionBoolsNullable", "coPercents": "ConfigOptionPercentsNullable", } cls = NULLABLE_LIST_CLASS[co_type] if (is_nullable and co_type in NULLABLE_LIST_CLASS) else LIST_CLASS[co_type] return f"new {cls}{{{args}}}" if co_type == "coEnum": # Extract enum type from two possible enum_keys_map formats: # "ConfigOptionEnum::get_enum_values()" -> "BedType" # "s_keys_map_BedType" -> "BedType" enum_type = "int" if enum_keys_map: m = _re.match(r'ConfigOptionEnum<(\w+)>::', enum_keys_map) if m: enum_type = m.group(1) else: m2 = _re.match(r's_keys_map_(\w+)$', enum_keys_map) if m2: enum_type = m2.group(1) return f"new ConfigOptionEnum<{enum_type}>({args})" if co_type == "coEnums": cls = "ConfigOptionEnumsGenericNullable" if is_nullable else "ConfigOptionEnumsGeneric" return f"new {cls}{{ {args} }}" # Fallback: try generic return f"new ConfigOption({args})" @staticmethod def _escape_cpp(s): """Escape a string for C++ string literal. Proto strings already contain C++ escape sequences (\\n, \\", etc.) as literal backslash + char. We pass those through and only escape unescaped quotes and actual newlines. """ if not s: return "" # Replace actual newline characters (rare) with \n escape s = s.replace('\n', '\\n') # Don't double-escape backslashes that are already part of escape sequences. # The proto strings store them as literal \n, \", \t etc. return s @staticmethod def _format_number(val): """Format a number for C++ (int vs float).""" if val is None: return "0" if isinstance(val, float) and val == int(val): return str(int(val)) return str(val) def _group_name_to_hook(name): """Convert group name to a C++ hook method suffix: 'Cooling Fan' -> 'cooling_fan'.""" return re.sub(r'[^a-z0-9]+', '_', name.lower()).strip('_') def _extract_field_paths(tab_layout_cpp): """ Read existing TabLayout_generated.cpp to build a field -> doc-path lookup. Used as a bootstrap so the yaml doesn't need to repeat every path. Returns dict: field_key -> path_string """ mapping = {} if not Path(tab_layout_cpp).exists(): return mapping with open(tab_layout_cpp, 'r', encoding='utf-8', errors='replace') as f: content = f.read() for key, path in re.findall( r'append_single_option_line\("([^"]+)",\s*"([^"]+)"\)', content): mapping[key] = path return mapping def generate_tab_layout(layout_yaml_path, output_path, existing_cpp_path=None): """ Generate TabLayout_generated.cpp from layout.yaml. YAML group fields can be: - key string → append_single_option_line(key, ) - {key: "path"} dict → append_single_option_line(key, "path") - [k1, k2, ...] list → multi-option Line - _separator_ string → optgroup->append_separator() Group attributes: hook: true → tab.layout_hook_(optgroup.get()) (no field generation) gcode: true → validate_custom_gcode_cb + edit_custom_gcode lambdas + gcode fields icon: "..." → second arg to new_optgroup() """ try: import yaml except ImportError: print(" ERROR: PyYAML not installed. Run: pip install pyyaml") return False with open(layout_yaml_path, 'r', encoding='utf-8') as f: layout = yaml.safe_load(f) # Bootstrap: extract existing field→path mappings so yaml doesn't need them all path_map = _extract_field_paths(existing_cpp_path) if existing_cpp_path else {} lines = [ "// ===== AUTO-GENERATED by tools/config_codegen.py from layout.yaml =====", "// DO NOT EDIT MANUALLY. Edit layout.yaml and re-run: python tools/run_codegen.py", "//", "// Included inside namespace Slic3r::GUI in Tab.cpp after validate_custom_gcode_cb", "// forward declaration. No namespace wrapper needed here.", "", "namespace { constexpr int gcode_field_height = 15; }", "namespace { constexpr int notes_field_height = 25; }", "", ] for tab in layout.get('tabs', []): tab_name = tab['name'] # e.g. TabPrint, TabFilament, TabPrinter pages = tab.get('pages', []) if not pages: continue # One inline function per page (or per tab if single page makes sense) # Convention: TabPrint_build_layout, TabFilament_build_main_layout, # TabPrinter_build_basic_info_layout, TabPrinter_build_gcode_layout, etc. for page in pages: page_name = page['name'] page_icon = page.get('icon', '') # Derive function name: TabPrint/"Quality" → TabPrint_build_quality_layout fn_suffix = _group_name_to_hook(page_name) fn_name = f"{tab_name}_build_{fn_suffix}_layout" lines.append(f"inline void {fn_name}({tab_name}& tab)") lines.append("{") lines.append(f" PageShp page = tab.add_options_page(L(\"{page_name}\"), \"{page_icon}\");") for group in page.get('groups', []): gname = group['name'] gicon = group.get('icon', '') is_hook = group.get('hook', False) is_gcode = group.get('gcode', False) fields = group.get('fields', []) indent_n = group.get('indent', 15) icon_arg = f', L"{gicon}"' if gicon else '' indent_arg = f', {indent_n}' if (is_gcode and indent_n != 15) else '' if is_gcode: icon_arg = f', L"{gicon}"' if gicon else ', L"param_gcode"' indent_arg = ', 0' lines.append(" {") lines.append(f" auto optgroup = page->new_optgroup(L(\"{gname}\"){icon_arg}{indent_arg});") if is_hook: hook_method = f"layout_hook_{_group_name_to_hook(gname)}" lines.append(f" tab.{hook_method}(optgroup.get());") elif is_gcode: # Standard g-code group: validate callback + edit button + gcode fields lines.append(" optgroup->m_on_change = [&tab, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) {") lines.append(" validate_custom_gcode_cb(&tab, optgroup_title, opt_key, value);") lines.append(" };") lines.append(" optgroup->edit_custom_gcode = [&tab](const t_config_option_key& opt_key) { tab.edit_custom_gcode(opt_key); };") for field in fields: key, path = _resolve_field(field, path_map) if key: path_arg = f', "{path}"' if path else '' lines.append(" {") lines.append(f" Option option = optgroup->get_option(\"{key}\");") lines.append(" option.opt.full_width = true;") lines.append(" option.opt.is_code = true;") lines.append(" option.opt.height = gcode_field_height;") lines.append(f" optgroup->append_single_option_line(option{path_arg});") lines.append(" }") else: # Regular group: generate append_single_option_line / multi-option line for field in fields: if isinstance(field, list): # Multi-option line: [key1, key2, ...] lines.append(" {") first = field[0] lines.append(f" Line line_{{optgroup->get_option(\"{first}\").opt.label, optgroup->get_option(\"{first}\").opt.tooltip}};") for k in field: lines.append(f" line_.append_option(optgroup->get_option(\"{k}\"));") lines.append(" optgroup->append_line(line_);") lines.append(" }") elif isinstance(field, str): if field == '_separator_': lines.append(" optgroup->append_separator();") else: path = path_map.get(field, '') path_arg = f', "{path}"' if path else '' lines.append(f" optgroup->append_single_option_line(\"{field}\"{path_arg});") elif isinstance(field, dict): # {key: path} explicit path for key, path in field.items(): path_arg = f', "{path}"' if path else '' lines.append(f" optgroup->append_single_option_line(\"{key}\"{path_arg});") lines.append(" }") lines.append("}") lines.append("") # Add backward-compatible wrapper functions that aggregate per-page functions # so Tab.cpp can call e.g. TabPrint_build_layout(*this) as before. wrappers = _build_wrappers(layout) lines.extend(wrappers) content = "\n".join(lines) + "\n" output_dir = Path(output_path).parent output_dir.mkdir(parents=True, exist_ok=True) with open(output_path, 'w', encoding='utf-8') as f: f.write(content) print(f"Generated: {output_path}") return True def _build_wrappers(layout): """Generate aggregate wrapper functions for backward compatibility with Tab.cpp.""" lines = ["// ── Aggregate wrappers (backward-compatible with Tab.cpp call sites) ──", ""] # Known wrappers: map from legacy function name → (tab_type, [page_names_to_include]) # The tab_name and page_names determine which per-page functions get called. wrapper_specs = { "TabPrint": ("TabPrint_build_layout", None), # all pages "TabFilament": ("TabFilament_build_main_layout", ["Filament", "Cooling", "Multimaterial"]), } for tab in layout.get('tabs', []): tab_name = tab['name'] pages = tab.get('pages', []) if tab_name == "TabPrinter": # Per-page wrappers for printer tab basic_info = [p for p in pages if p['name'] == "Basic information"] gcode_pages = [p for p in pages if p['name'] in ("Machine G-code", "Notes")] if basic_info: fn = f"TabPrinter_build_basic_info_layout" lines.append(f"inline void {fn}(TabPrinter& tab)") lines.append("{") for p in basic_info: pf = f"TabPrinter_build_{_group_name_to_hook(p['name'])}_layout" lines.append(f" {pf}(tab);") lines.append("}") lines.append("") if gcode_pages: fn = "TabPrinter_build_gcode_layout" lines.append(f"inline void {fn}(TabPrinter& tab)") lines.append("{") for p in gcode_pages: pf = f"TabPrinter_build_{_group_name_to_hook(p['name'])}_layout" lines.append(f" {pf}(tab);") lines.append("}") lines.append("") elif tab_name in wrapper_specs: legacy_fn, page_filter = wrapper_specs[tab_name] filtered = [p for p in pages if page_filter is None or p['name'] in page_filter] if not filtered: continue lines.append(f"inline void {legacy_fn}({tab_name}& tab)") lines.append("{") for p in filtered: pf = f"{tab_name}_build_{_group_name_to_hook(p['name'])}_layout" lines.append(f" {pf}(tab);") lines.append("}") lines.append("") return lines def _resolve_field(field, path_map): """Return (key, path) from a field entry in various yaml formats.""" if isinstance(field, str): return field, path_map.get(field, '') elif isinstance(field, dict): for k, v in field.items(): return k, (v or path_map.get(k, '')) return None, '' def main(): parser = argparse.ArgumentParser( description="Generate C++ config code from protobuf descriptors") parser.add_argument("descriptor_set", help="Path to compiled .desc file (protoc --descriptor_set_out)") parser.add_argument("output_dir", help="Directory to write generated C++ files") args = parser.parse_args() # Read descriptor set desc_path = Path(args.descriptor_set) if not desc_path.exists(): print(f"ERROR: Descriptor file not found: {desc_path}") sys.exit(1) with open(desc_path, 'rb') as f: raw = f.read() file_descriptor_set = descriptor_pb2.FileDescriptorSet() file_descriptor_set.ParseFromString(raw) print(f"Loaded {len(file_descriptor_set.file)} proto files") for fd in file_descriptor_set.file: if not fd.name.startswith("google/"): print(f" - {fd.name}: {len(fd.message_type)} messages") # Generate code gen = CodeGenerator(file_descriptor_set) output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) outputs = { "PrintConfigDef_generated.cpp": gen.generate_init_fff_params(), "Preset_options_generated.cpp": gen.generate_preset_options(), "Invalidation_generated.cpp": gen.generate_invalidation_map(), "OptionKeys_generated.cpp": gen.generate_option_key_lists(), } for filename, content in outputs.items(): out_path = output_dir / filename with open(out_path, 'w', encoding='utf-8') as f: f.write(content) print(f"Generated: {out_path}") print(f"\nDone. {len(gen.fields)} settings processed.") # Generate tab layout from layout.yaml layout_yaml = desc_path.parent.parent / "src" / "PrintConfigs" / "layout.yaml" if not layout_yaml.exists(): # Try repo root relative path layout_yaml = Path(__file__).resolve().parent.parent / "src" / "PrintConfigs" / "layout.yaml" if layout_yaml.exists(): tab_layout_out = output_dir / "TabLayout_generated.cpp" existing = tab_layout_out if tab_layout_out.exists() else None generate_tab_layout(str(layout_yaml), str(tab_layout_out), str(existing) if existing else None) else: print(f" NOTE: layout.yaml not found at {layout_yaml}, skipping tab layout generation") if __name__ == "__main__": main()