feat: initial commit for cable normalizer blender addon
This commit is contained in:
257
migration.py
Normal file
257
migration.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user