"""Migration and conversion utilities for Cable Normalizer. Provides operators to: - Convert the selected object to a cable (adds 'Cable' GeoNode) - Batch-migrate all legacy cables at once - Migrate text/font settings from the Text Texture Generator addon """ import bpy from . import utils # Mapping from kabeck.blend scene names to their correct TTG label text. # Derived from scanning all source .blend files' text_texture_props. # Only entries where scene name differs from the actual label text need # to be listed; for all others the scene name IS the label text. _TTG_LABEL_TEXT = { 'N2XSF2Y': 'N2XS(F)2Y', 'NA2XSF2Y': 'NA2XS(F)2Y', 'NA2XSFL2Y': 'NA2XS(FL)2Y', 'NA2XY': '(N)A2XY', } def _get_ttg_data_for_scene(scene): """Extract text and font from Text Texture Generator properties. Checks both the scene's own text_texture_props and, if the object is linked, attempts to read from the source library's scene. Returns dict with keys: text, font_path (or None if TTG not found). """ if not hasattr(scene, 'text_texture_props'): return None tp = scene.text_texture_props text = tp.prepend_text + tp.text + tp.append_text font_path = '' if tp.use_custom_font and tp.custom_font_path: font_path = tp.custom_font_path elif tp.font_path and tp.font_path != 'default': font_path = tp.font_path return {'text': text, 'font_path': font_path} def _find_ttg_font_from_libraries(): """Scan linked library scenes for Text Texture Generator font settings. Since the kabeck.blend links objects from source blends (NYY.blend etc.), the TTG settings live in those source files. We can't open them during migration, but the linked data may carry scene references. As a pragmatic fallback, we check if any scene in the current file has non-empty TTG settings. If not, we look for commonly used fonts on disk based on what we know from the source blend analysis. """ # First: Check all scenes in current file for TTG data for scn in bpy.data.scenes: data = _get_ttg_data_for_scene(scn) if data and data['font_path']: return data['font_path'] # Fallback: Check known font path from the source blends # (all source blends use "Windows Dots.ttf" per analysis) import os known_font = "/Volumes/Alpha SSD/Fonts/Windows Dots.ttf" if os.path.exists(known_font): return known_font return '' def _find_ttg_prefix(): """Find the PREPEND composition text from TTG overlays. The TTG composition system stores prepend text as overlay components with placement_mode='PREPEND'. All source blends use 'Kabeck' as the prefix, set as a TEXT overlay component. """ # Check all scenes for TTG overlay data for scn in bpy.data.scenes: if not hasattr(scn, 'text_texture_props'): continue tp = scn.text_texture_props for ov in tp.image_overlays: if ov.component_type == 'TEXT' and ov.placement_mode == 'PREPEND': if ov.text_content: return ov.text_content # Fallback: known prefix from source blend analysis return 'Kabeck' def _add_cable_node(obj, label_text=''): """Add a Cable node modifier to an object and populate defaults. The Cable node group is created once and shared across all cables. This is effectively 'linked' behavior — all cables reference the same node group definition. """ cable_ng = utils.ensure_cable_node_group() # Avoid duplicates if utils.is_cable(obj): return None mod = obj.modifiers.new(name=utils.CABLE_NODE_NAME, type='NODES') mod.node_group = cable_ng # Auto-detect label text with priority chain: # 1. TTG mapping table (known mismatches) # 2. TTG scene properties (prepend + text + append) # 3. Scene name as fallback if not label_text: for scn in bpy.data.scenes: if obj.name in scn.objects: scene_name = scn.name # Priority 1: Known TTG label text mapping if scene_name in _TTG_LABEL_TEXT: label_text = _TTG_LABEL_TEXT[scene_name] else: # Priority 2: TTG properties on the scene ttg = _get_ttg_data_for_scene(scn) if ttg and ttg['text']: label_text = ttg['text'] else: # Priority 3: Scene name label_text = scene_name break # Read current diameter from legacy structure try: params = utils.get_cable_params(obj) outer_diam = utils.compute_outer_diameter(params) current_diam = outer_diam * obj.scale[0] except ValueError: current_diam = 13.8 # Populate sockets for item in cable_ng.interface.items_tree: if item.item_type != 'SOCKET' or item.in_out != 'INPUT': continue if item.name == 'Label Text': mod[item.identifier] = label_text elif item.name == 'Target Diameter': mod[item.identifier] = current_diam return mod class KABECK_OT_convert_to_cable(bpy.types.Operator): """Convert the selected object to a cable by adding the Cable node.""" bl_idname = "kabeck.convert_to_cable" bl_label = "Convert to Cable" bl_description = ( "Adds the 'Cable' Geometry Node to the selected object, " "marking it as a cable and enabling per-cable label settings" ) bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): obj = context.active_object return obj is not None and obj.type == 'MESH' and not utils.is_cable(obj) def execute(self, context): obj = context.active_object mod = _add_cable_node(obj) if mod is None: self.report({'WARNING'}, f"'{obj.name}' is already a cable") return {'CANCELLED'} self.report({'INFO'}, f"Converted '{obj.name}' to cable") return {'FINISHED'} class KABECK_OT_migrate_cables(bpy.types.Operator): """Batch-convert legacy cables and update existing Cable nodes. Migrates Text Texture Generator settings: - Label Text from TTG text/prepend/append fields (or mapping table) - Font path set globally from TTG custom_font_path Can be re-run to update Label Text on already-migrated cables. """ bl_idname = "kabeck.migrate_cables" bl_label = "Migrate / Update Cable Labels" bl_description = ( "Add or update 'Cable' GeoNode on all cables, " "migrate text/font from Text Texture Generator" ) bl_options = {'REGISTER', 'UNDO'} def execute(self, context): migrated = 0 updated = 0 # Migrate font setting globally from TTG ttg_font = _find_ttg_font_from_libraries() if ttg_font: context.scene.normalizer_font_path = ttg_font # Migrate prefix from TTG composition overlays ttg_prefix = _find_ttg_prefix() if ttg_prefix: context.scene.normalizer_label_prefix = ttg_prefix for obj in bpy.data.objects: if utils.is_cable(obj): # Already has Cable node → update Label Text self._update_label_text(obj) updated += 1 continue if not utils.is_legacy_cable(obj): continue _add_cable_node(obj) migrated += 1 font_msg = f", font: {ttg_font}" if ttg_font else "" self.report( {'INFO'}, f"Migrated {migrated}, updated {updated} cables{font_msg}", ) return {'FINISHED'} def _update_label_text(self, obj): """Update the Label Text on an existing Cable node from TTG data.""" mod = utils._get_cable_modifier(obj) if mod is None: return # Resolve the correct label text label_text = '' for scn in bpy.data.scenes: if obj.name in scn.objects: scene_name = scn.name if scene_name in _TTG_LABEL_TEXT: label_text = _TTG_LABEL_TEXT[scene_name] else: ttg = _get_ttg_data_for_scene(scn) if ttg and ttg['text']: label_text = ttg['text'] else: label_text = scene_name break if not label_text: return # Write to the Cable node socket cable_ng = mod.node_group for item in cable_ng.interface.items_tree: if item.item_type == 'SOCKET' and item.in_out == 'INPUT' and item.name == 'Label Text': mod[item.identifier] = label_text break