Files
cable-normalizer/migration.py

258 lines
8.7 KiB
Python

"""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