This commit is contained in:
2025-10-13 13:35:52 +02:00
parent 2494897360
commit bf636cbc66
21 changed files with 331 additions and 276 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -52,8 +52,8 @@ def generate_texture_image(props, width, height):
lanczos_filter = getattr(resampling_module, 'LANCZOS', getattr(Image, 'LANCZOS', Image.BICUBIC)) lanczos_filter = getattr(resampling_module, 'LANCZOS', getattr(Image, 'LANCZOS', Image.BICUBIC))
bicubic_filter = getattr(resampling_module, 'BICUBIC', getattr(Image, 'BICUBIC', Image.BILINEAR)) bicubic_filter = getattr(resampling_module, 'BICUBIC', getattr(Image, 'BICUBIC', Image.BILINEAR))
def _iter_enabled_overlays(): def _iter_enabled_components():
"""Yield (overlay, image_path) tuples for overlays that should be applied.""" """Yield enabled composition components."""
overlays = getattr(props, 'image_overlays', []) overlays = getattr(props, 'image_overlays', [])
try: try:
overlay_list = list(overlays) overlay_list = list(overlays)
@@ -62,17 +62,14 @@ def generate_texture_image(props, width, height):
for overlay in overlay_list: for overlay in overlay_list:
if not getattr(overlay, 'enabled', True): if not getattr(overlay, 'enabled', True):
continue continue
image_path = getattr(overlay, 'image_path', '') yield overlay
if not image_path:
continue
yield overlay, image_path
def _calculate_overlay_position(overlay, overlay_size): def _calculate_overlay_position(overlay, overlay_size):
"""Calculate top-left pixel position for an overlay image.""" """Calculate top-left pixel position for an overlay image."""
overlay_width, overlay_height = overlay_size overlay_width, overlay_height = overlay_size
canvas_width = width canvas_width = width
canvas_height = height canvas_height = height
mode = getattr(overlay, 'positioning_mode', 'ABSOLUTE') or 'ABSOLUTE' mode = getattr(overlay, 'placement_mode', 'ABSOLUTE') or 'ABSOLUTE'
x_norm = float(getattr(overlay, 'x_position', 0.5) or 0.5) x_norm = float(getattr(overlay, 'x_position', 0.5) or 0.5)
y_norm = float(getattr(overlay, 'y_position', 0.5) or 0.5) y_norm = float(getattr(overlay, 'y_position', 0.5) or 0.5)
@@ -95,16 +92,57 @@ def generate_texture_image(props, width, height):
y = max(0, min(max_y, y)) y = max(0, min(max_y, y))
return x, y return x, y
def _apply_image_overlays(pil_canvas): def _apply_composition_components(pil_canvas):
"""Composite enabled image overlays onto the base canvas.""" """Composite enabled components (text or image) onto the base canvas."""
overlays_to_apply = sorted( components = sorted(
_iter_enabled_overlays(), _iter_enabled_components(),
key=lambda item: getattr(item[0], 'z_index', 0) key=lambda comp: getattr(comp, 'z_index', 0)
) )
for overlay, image_path in overlays_to_apply: for component in components:
resolved_path = image_path if getattr(component, 'component_type', 'IMAGE') == 'TEXT':
scale = getattr(overlay, 'scale', 1.0) text_value = getattr(component, 'text_content', '').strip()
if not text_value:
continue
scale = getattr(component, 'scale', 1.0)
try:
scale = float(scale if scale is not None else 1.0)
except (TypeError, ValueError):
scale = 1.0
text_size = max(8, int(round(props.font_size * max(scale, 0.1))))
text_font = load_font_for_size(text_size)
try:
text_bbox = text_font.getbbox(text_value)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
except AttributeError:
try:
text_width, text_height = text_font.getsize(text_value)
except AttributeError:
# Fallback to a temporary draw context
tmp_draw = ImageDraw.Draw(Image.new('RGBA', (1, 1)))
text_width, text_height = tmp_draw.textsize(text_value, font=text_font)
overlay_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0))
overlay_draw = ImageDraw.Draw(overlay_img)
text_color = tuple(int(c * 255) for c in props.text_color[:4])
overlay_draw.text((0, 0), text_value, font=text_font, fill=text_color)
rotation = float(getattr(component, 'rotation', 0.0) or 0.0)
if rotation % 360:
overlay_img = overlay_img.rotate(-rotation, expand=True, resample=bicubic_filter)
x_pos, y_pos = _calculate_overlay_position(component, overlay_img.size)
overlay_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0))
overlay_layer.paste(overlay_img, (x_pos, y_pos), overlay_img)
pil_canvas = Image.alpha_composite(pil_canvas, overlay_layer)
continue
resolved_path = getattr(component, 'image_path', '')
if not resolved_path:
continue
scale = getattr(component, 'scale', 1.0)
try: try:
scale = float(scale if scale is not None else 1.0) scale = float(scale if scale is not None else 1.0)
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -121,7 +159,7 @@ def generate_texture_image(props, width, height):
resolved_path = raster_info.path resolved_path = raster_info.path
scale_to_apply = raster_info.applied_scale scale_to_apply = raster_info.applied_scale
except SVGConversionError as svg_error: except SVGConversionError as svg_error:
print(f"WARNING: Failed to rasterize SVG overlay '{image_path}': {svg_error}") print(f"WARNING: Failed to rasterize SVG overlay '{component.image_path}': {svg_error}")
continue continue
try: try:
@@ -139,11 +177,11 @@ def generate_texture_image(props, width, height):
if new_size != overlay_img.size: if new_size != overlay_img.size:
overlay_img = overlay_img.resize(new_size, lanczos_filter) overlay_img = overlay_img.resize(new_size, lanczos_filter)
rotation = float(getattr(overlay, 'rotation', 0.0) or 0.0) rotation = float(getattr(component, 'rotation', 0.0) or 0.0)
if rotation % 360: if rotation % 360:
overlay_img = overlay_img.rotate(-rotation, expand=True, resample=bicubic_filter) overlay_img = overlay_img.rotate(-rotation, expand=True, resample=bicubic_filter)
x_pos, y_pos = _calculate_overlay_position(overlay, overlay_img.size) x_pos, y_pos = _calculate_overlay_position(component, overlay_img.size)
overlay_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) overlay_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0))
overlay_layer.paste(overlay_img, (x_pos, y_pos), overlay_img) overlay_layer.paste(overlay_img, (x_pos, y_pos), overlay_img)
@@ -358,7 +396,7 @@ def generate_texture_image(props, width, height):
pass pass
# Apply any configured image overlays before converting to Blender pixels # Apply any configured image overlays before converting to Blender pixels
pil_img = _apply_image_overlays(pil_img) pil_img = _apply_composition_components(pil_img)
# Convert PIL image to Blender format (flip vertically to fix mirroring) # Convert PIL image to Blender format (flip vertically to fix mirroring)
pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM) # Fix mirroring issue pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM) # Fix mirroring issue

View File

@@ -139,6 +139,11 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
pass pass
overlay_data = { overlay_data = {
'name': overlay.name, 'name': overlay.name,
'component_type': overlay.component_type,
'placement_mode': overlay.placement_mode,
'text_content': overlay.text_content,
'text_spacing': overlay.text_spacing,
'image_spacing': overlay.image_spacing,
'image_path': overlay.image_path, 'image_path': overlay.image_path,
'x_position': overlay.x_position, 'x_position': overlay.x_position,
'y_position': overlay.y_position, 'y_position': overlay.y_position,
@@ -397,6 +402,11 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
pass pass
overlay = props.image_overlays.add() overlay = props.image_overlays.add()
overlay.name = overlay_data.get('name', 'Overlay') overlay.name = overlay_data.get('name', 'Overlay')
overlay.component_type = overlay_data.get('component_type', overlay.component_type)
overlay.placement_mode = overlay_data.get('placement_mode', 'ABSOLUTE')
overlay.text_content = overlay_data.get('text_content', '')
overlay.text_spacing = overlay_data.get('text_spacing', overlay.text_spacing)
overlay.image_spacing = overlay_data.get('image_spacing', overlay.image_spacing)
overlay.image_path = overlay_data.get('image_path', '') overlay.image_path = overlay_data.get('image_path', '')
overlay.x_position = overlay_data.get('x_position', 0.5) overlay.x_position = overlay_data.get('x_position', 0.5)
overlay.y_position = overlay_data.get('y_position', 0.5) overlay.y_position = overlay_data.get('y_position', 0.5)

View File

@@ -341,18 +341,30 @@ class TEXT_TEXTURE_OT_refresh_presets(Operator):
class TEXT_TEXTURE_OT_add_overlay(Operator): class TEXT_TEXTURE_OT_add_overlay(Operator):
pass pass
"""Add new image overlay""" """Add new composition component"""
bl_idname = "text_texture.add_overlay" bl_idname = "text_texture.add_overlay"
bl_label = "Add Overlay" bl_label = "Add Component"
bl_description = "Add new image overlay" bl_description = "Add new composition component"
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
placement_mode: EnumProperty(
name="Placement Mode",
items=[
('ABSOLUTE', "Absolute", ""),
('PREPEND', "Prepend", ""),
('APPEND', "Append", "")
],
default='ABSOLUTE'
)
def execute(self, context): def execute(self, context):
pass pass
props = context.scene.text_texture_props props = context.scene.text_texture_props
overlay = props.image_overlays.add() overlay = props.image_overlays.add()
overlay.name = f"Overlay {len(props.image_overlays)}" overlay.name = f"Component {len(props.image_overlays)}"
overlay.placement_mode = self.placement_mode
overlay.component_type = 'TEXT' if self.placement_mode in {'PREPEND', 'APPEND'} else 'IMAGE'
props.active_overlay_index = len(props.image_overlays) - 1 props.active_overlay_index = len(props.image_overlays) - 1
# Force UI redraw # Force UI redraw
@@ -360,7 +372,7 @@ class TEXT_TEXTURE_OT_add_overlay(Operator):
pass pass
area.tag_redraw() area.tag_redraw()
self.report({'INFO'}, f"Added overlay: {overlay.name}") self.report({'INFO'}, f"Added component: {overlay.name}")
return {'FINISHED'} return {'FINISHED'}
@@ -435,10 +447,10 @@ class TEXT_TEXTURE_OT_sync_margins(Operator):
class TEXT_TEXTURE_OT_duplicate_overlay(Operator): class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
pass pass
"""Duplicate image overlay""" """Duplicate a composition component"""
bl_idname = "text_texture.duplicate_overlay" bl_idname = "text_texture.duplicate_overlay"
bl_label = "Duplicate Overlay" bl_label = "Duplicate Component"
bl_description = "Duplicate this image overlay" bl_description = "Duplicate this composition component"
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
overlay_index: IntProperty() overlay_index: IntProperty()
@@ -457,7 +469,9 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
# Copy all properties with explicit verification # Copy all properties with explicit verification
new_overlay.name = f"{source_overlay.name} Copy" new_overlay.name = f"{source_overlay.name} Copy"
new_overlay.component_type = source_overlay.component_type
new_overlay.image_path = source_overlay.image_path new_overlay.image_path = source_overlay.image_path
new_overlay.text_content = source_overlay.text_content
new_overlay.x_position = source_overlay.x_position new_overlay.x_position = source_overlay.x_position
new_overlay.y_position = source_overlay.y_position new_overlay.y_position = source_overlay.y_position
new_overlay.scale = source_overlay.scale new_overlay.scale = source_overlay.scale
@@ -465,7 +479,7 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
# Fix z-index assignment for duplicated overlays to ensure proper visibility # Fix z-index assignment for duplicated overlays to ensure proper visibility
# For PREPEND/APPEND overlays, increment z_index to ensure duplicate appears above original # For PREPEND/APPEND overlays, increment z_index to ensure duplicate appears above original
# This prevents the duplicate from being rendered behind the original overlay # This prevents the duplicate from being rendered behind the original overlay
if source_overlay.positioning_mode in ['PREPEND', 'APPEND']: if source_overlay.placement_mode in ['PREPEND', 'APPEND']:
pass pass
# Increment z_index to ensure proper layering order for duplicated overlay # Increment z_index to ensure proper layering order for duplicated overlay
new_overlay.z_index = min(source_overlay.z_index + 1, 10) # Cap at max z_index value new_overlay.z_index = min(source_overlay.z_index + 1, 10) # Cap at max z_index value
@@ -473,7 +487,7 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
pass pass
# For ABSOLUTE overlays, keep same z_index (different positions so no overlap issue) # For ABSOLUTE overlays, keep same z_index (different positions so no overlap issue)
new_overlay.z_index = source_overlay.z_index new_overlay.z_index = source_overlay.z_index
new_overlay.positioning_mode = source_overlay.positioning_mode new_overlay.placement_mode = source_overlay.placement_mode
new_overlay.text_spacing = source_overlay.text_spacing new_overlay.text_spacing = source_overlay.text_spacing
new_overlay.image_spacing = source_overlay.image_spacing new_overlay.image_spacing = source_overlay.image_spacing
new_overlay.enabled = source_overlay.enabled new_overlay.enabled = source_overlay.enabled
@@ -517,7 +531,7 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
props.is_updating = False props.is_updating = False
self.report({'INFO'}, f"✅ Duplicated overlay: {source_overlay.name}") self.report({'INFO'}, f"✅ Duplicated component: {source_overlay.name}")
else: else:
pass pass
self.report({'ERROR'}, "Invalid overlay index") self.report({'ERROR'}, "Invalid overlay index")
@@ -528,10 +542,10 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
class TEXT_TEXTURE_OT_delete_overlay(Operator): class TEXT_TEXTURE_OT_delete_overlay(Operator):
pass pass
"""Delete image overlay""" """Delete a composition component"""
bl_idname = "text_texture.delete_overlay" bl_idname = "text_texture.delete_overlay"
bl_label = "Delete Overlay" bl_label = "Delete Component"
bl_description = "Delete this image overlay" bl_description = "Delete this composition component"
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
overlay_index: IntProperty() overlay_index: IntProperty()
@@ -567,7 +581,7 @@ class TEXT_TEXTURE_OT_delete_overlay(Operator):
pass pass
area.tag_redraw() area.tag_redraw()
self.report({'INFO'}, f"Deleted overlay: {overlay_name}") self.report({'INFO'}, f"Deleted component: {overlay_name}")
else: else:
pass pass
self.report({'ERROR'}, "Invalid overlay index") self.report({'ERROR'}, "Invalid overlay index")
@@ -578,10 +592,10 @@ class TEXT_TEXTURE_OT_delete_overlay(Operator):
class TEXT_TEXTURE_OT_move_overlay(Operator): class TEXT_TEXTURE_OT_move_overlay(Operator):
pass pass
"""Move image overlay up or down in the list""" """Move a composition component within its placement group"""
bl_idname = "text_texture.move_overlay" bl_idname = "text_texture.move_overlay"
bl_label = "Move Overlay" bl_label = "Move Component"
bl_description = "Move overlay up or down in the list" bl_description = "Move component up or down within its placement group"
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
overlay_index: IntProperty() overlay_index: IntProperty()
@@ -601,12 +615,15 @@ class TEXT_TEXTURE_OT_move_overlay(Operator):
pass pass
current_index = self.overlay_index current_index = self.overlay_index
new_index = current_index new_index = current_index
target_mode = props.image_overlays[current_index].placement_mode
if self.direction == 'UP' and current_index > 0: step = -1 if self.direction == 'UP' else 1
pass probe = current_index + step
new_index = current_index - 1 while 0 <= probe < len(props.image_overlays):
elif self.direction == 'DOWN' and current_index < len(props.image_overlays) - 1: if props.image_overlays[probe].placement_mode == target_mode:
new_index = current_index + 1 new_index = probe
break
probe += step
if new_index != current_index: if new_index != current_index:
pass pass
@@ -622,10 +639,10 @@ class TEXT_TEXTURE_OT_move_overlay(Operator):
except Exception as e: except Exception as e:
pass pass
self.report({'INFO'}, f"Moved overlay {self.direction.lower()}") self.report({'INFO'}, f"Moved component {self.direction.lower()}")
else: else:
pass pass
self.report({'INFO'}, f"Overlay is already at {self.direction.lower()} edge") self.report({'INFO'}, f"Component is already at {self.direction.lower()} edge")
else: else:
pass pass
self.report({'ERROR'}, "Invalid overlay index") self.report({'ERROR'}, "Invalid overlay index")
@@ -647,4 +664,4 @@ __all__ = [
'TEXT_TEXTURE_OT_duplicate_overlay', 'TEXT_TEXTURE_OT_duplicate_overlay',
'TEXT_TEXTURE_OT_delete_overlay', 'TEXT_TEXTURE_OT_delete_overlay',
'TEXT_TEXTURE_OT_move_overlay', 'TEXT_TEXTURE_OT_move_overlay',
] ]

View File

@@ -6,13 +6,15 @@ for the text texture generator addon.
""" """
from .property_groups import ( from .property_groups import (
TEXT_TEXTURE_CompositionComponent,
TEXT_TEXTURE_ImageOverlay, TEXT_TEXTURE_ImageOverlay,
TEXT_TEXTURE_Preset, TEXT_TEXTURE_Preset,
TEXT_TEXTURE_Properties TEXT_TEXTURE_Properties
) )
__all__ = [ __all__ = [
'TEXT_TEXTURE_CompositionComponent',
'TEXT_TEXTURE_ImageOverlay', 'TEXT_TEXTURE_ImageOverlay',
'TEXT_TEXTURE_Preset', 'TEXT_TEXTURE_Preset',
'TEXT_TEXTURE_Properties', 'TEXT_TEXTURE_Properties',
] ]

View File

@@ -50,19 +50,77 @@ def get_font_enum_items(self, context):
return items return items
class TEXT_TEXTURE_ImageOverlay(PropertyGroup): class TEXT_TEXTURE_CompositionComponent(PropertyGroup):
pass pass
"""Single image overlay item""" """Single composition component that can be text or image."""
name: StringProperty( name: StringProperty(
name="Image Name", name="Component Name",
description="Name identifier for this image overlay", description="Name identifier for this component",
default="Overlay", default="Component",
update=update_live_preview
)
enabled: BoolProperty(
name="Enabled",
description="Enable this component",
default=True,
update=update_live_preview
)
component_type: EnumProperty(
name="Component Type",
description="Select whether this component renders text or an image",
items=[
('TEXT', "Text", "Render a text snippet as part of the composition"),
('IMAGE', "Image", "Render an image file as part of the composition"),
],
default='IMAGE',
update=update_live_preview
)
placement_mode: EnumProperty(
name="Placement",
description="Control how this component attaches to the main layout",
items=[
('ABSOLUTE', "Absolute", "Position using explicit coordinates"),
('PREPEND', "Prepend", "Place before the main text"),
('APPEND', "Append", "Place after the main text"),
],
default='ABSOLUTE',
update=update_live_preview
)
text_content: StringProperty(
name="Text",
description="Text content for this component when using text mode",
default="",
maxlen=512,
update=update_live_preview
)
text_spacing: FloatProperty(
name="Spacing (%)",
description="Spacing between main text and this component as percentage (0-100%)",
default=10.0,
min=0.0,
max=100.0,
subtype='PERCENTAGE',
update=update_live_preview
)
image_spacing: FloatProperty(
name="Component Spacing (%)",
description="Spacing between consecutive components (0-100%)",
default=5.0,
min=0.0,
max=100.0,
subtype='PERCENTAGE',
update=update_live_preview update=update_live_preview
) )
image_path: StringProperty( image_path: StringProperty(
name="Image File", name="Image File",
description="Path to image file", description="Path to image file for image components",
subtype='FILE_PATH', subtype='FILE_PATH',
default="", default="",
update=update_live_preview update=update_live_preview
@@ -88,7 +146,7 @@ class TEXT_TEXTURE_ImageOverlay(PropertyGroup):
scale: FloatProperty( scale: FloatProperty(
name="Scale", name="Scale",
description="Scale of the image", description="Scale factor for this component",
default=1.0, default=1.0,
min=0.1, min=0.1,
max=5.0, max=5.0,
@@ -97,7 +155,7 @@ class TEXT_TEXTURE_ImageOverlay(PropertyGroup):
rotation: FloatProperty( rotation: FloatProperty(
name="Rotation", name="Rotation",
description="Rotation in degrees", description="Rotation in degrees (image components only)",
default=0.0, default=0.0,
min=-360.0, min=-360.0,
max=360.0, max=360.0,
@@ -112,45 +170,9 @@ class TEXT_TEXTURE_ImageOverlay(PropertyGroup):
max=10, max=10,
update=update_live_preview update=update_live_preview
) )
positioning_mode: EnumProperty( # Backwards compatibility alias (legacy name used throughout codebase)
name="Positioning Mode", TEXT_TEXTURE_ImageOverlay = TEXT_TEXTURE_CompositionComponent
description="How the image overlay is positioned relative to the text",
items=[
('ABSOLUTE', "Absolute", "Position overlay at absolute coordinates (legacy mode)"),
('APPEND', "Append", "Position overlay after the text content"),
('PREPEND', "Prepend", "Position overlay before the text content")
],
default='ABSOLUTE',
update=update_live_preview
)
text_spacing: FloatProperty(
name="Text Spacing (%)",
description="Spacing between text and image as percentage of canvas width (0-100%)",
default=10.0,
min=0.0,
max=100.0,
subtype='PERCENTAGE',
update=update_live_preview
)
image_spacing: FloatProperty(
name="Image Spacing (%)",
description="Spacing between consecutive images as percentage of canvas width (0-100%)",
default=5.0,
min=0.0,
max=100.0,
subtype='PERCENTAGE',
update=update_live_preview
)
enabled: BoolProperty(
name="Enabled",
description="Enable this image overlay",
default=True,
update=update_live_preview
)
class TEXT_TEXTURE_Preset(PropertyGroup): class TEXT_TEXTURE_Preset(PropertyGroup):
pass pass
@@ -281,16 +303,6 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
update=update_live_preview update=update_live_preview
) )
composition_active_tab: EnumProperty(
name="Composition Mode",
description="Select which composition controls to display",
items=[
('TEXT', "Text", "Configure text layering options"),
('IMAGE', "Image", "Configure image overlay options"),
],
default='TEXT'
)
texture_width: IntProperty( texture_width: IntProperty(
name="Width", name="Width",
description="Texture width in pixels", description="Texture width in pixels",
@@ -603,8 +615,8 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
update=update_live_preview update=update_live_preview
) )
# Image overlays # Composition components (legacy name retained for compatibility)
image_overlays: CollectionProperty(type=TEXT_TEXTURE_ImageOverlay) image_overlays: CollectionProperty(type=TEXT_TEXTURE_CompositionComponent)
active_overlay_index: IntProperty(default=0) active_overlay_index: IntProperty(default=0)
# Presets # Presets

View File

@@ -229,163 +229,122 @@ def draw_generate_button(layout):
row.scale_y = 1.5 row.scale_y = 1.5
row.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA') row.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA')
def draw_composition_image_section(layout, props): def _iter_components_by_placement(props, placement_mode):
pass """Yield (index, component) pairs filtered by placement mode."""
"""Draw image overlay controls for the composition panel.""" try:
overlays = list(props.image_overlays)
except TypeError:
overlays = props.image_overlays
for index, component in enumerate(overlays):
if getattr(component, "placement_mode", 'ABSOLUTE') == placement_mode:
yield index, component
def _draw_component_header(row, component):
"""Draw shared header controls for a component."""
row.prop(component, "enabled", text="", icon='CHECKBOX_HLT' if component.enabled else 'CHECKBOX_DEHLT')
name_row = row.row(align=True)
name_row.prop(component, "name", text="")
name_row.prop(component, "component_type", text="")
def _draw_component_footer(row, props, index):
"""Draw the footer action buttons for a component."""
premium_lock = create_premium_lock_function()
dup_op = safe_operator_call(row, "text_texture.duplicate_overlay", premium_lock, text="", icon='DUPLICATE')
if dup_op:
dup_op.overlay_index = index
del_op = safe_operator_call(row, "text_texture.delete_overlay", premium_lock, text="", icon='TRASH')
if del_op:
del_op.overlay_index = index
move_up = safe_operator_call(row, "text_texture.move_overlay", premium_lock, text="", icon='TRIA_UP')
if move_up:
move_up.overlay_index = index
move_up.direction = 'UP'
move_down = safe_operator_call(row, "text_texture.move_overlay", premium_lock, text="", icon='TRIA_DOWN')
if move_down:
move_down.overlay_index = index
move_down.direction = 'DOWN'
def _draw_component_details(container, component):
"""Render component-specific detail controls."""
details_col = container.column(align=True)
details_col.enabled = component.enabled
details_col.prop(component, "placement_mode", text="Placement")
if component.component_type == 'TEXT':
details_col.prop(component, "text_content", text="Text")
details_col.prop(component, "text_spacing", text="Spacing (%)")
else:
details_col.prop(component, "image_path", text="Image File")
details_col.prop(component, "text_spacing", text="Spacing (%)")
details_col.prop(component, "image_spacing", text="Component Spacing (%)")
if component.placement_mode == 'ABSOLUTE':
pos_row = details_col.row(align=True)
pos_row.prop(component, "x_position", text="X")
pos_row.prop(component, "y_position", text="Y")
transform_row = details_col.row(align=True)
transform_row.prop(component, "scale", text="Scale")
transform_row.prop(component, "rotation", text="Rotation")
transform_row.prop(component, "z_index", text="Z-Level")
def draw_component_section(layout, props, placement_mode, title, icon):
"""Render a section for a specific placement mode."""
section = layout.box() section = layout.box()
section.use_property_split = False section.use_property_split = False
section.use_property_decorate = False section.use_property_decorate = False
section.label(text="Image Elements", icon='IMAGE_DATA')
# Check if image overlays are available header = section.row(align=True)
if not has_image_overlays(): header.label(text=title, icon=icon)
pass
draw_feature_lock_indicator(section, 'image_overlays') # Add new component button
add_row = section.row(align=True)
premium_lock = create_premium_lock_function()
add_op = safe_operator_call(
add_row,
"text_texture.add_overlay",
premium_lock,
text="Add Component",
icon='ADD'
)
if add_op:
add_op.placement_mode = placement_mode
components = list(_iter_components_by_placement(props, placement_mode))
if not components:
return return
# Add overlay button - use safe operator call for index, component in components:
row = section.row() box = section.box()
safe_operator_call(row, "text_texture.add_overlay", header_row = box.row(align=True)
lambda layout: draw_feature_lock_indicator(layout, 'image_overlays'), _draw_component_header(header_row, component)
text="Add Image Overlay", icon='ADD') _draw_component_footer(header_row, props, index)
_draw_component_details(box, component)
# Individual overlay controls
if props.image_overlays:
pass
section.separator()
for i, overlay in enumerate(props.image_overlays):
pass
# Create collapsible box for each overlay
box = section.box()
# Header row with controls
header_row = box.row(align=True)
# Enable/Disable checkbox with clear label
header_row.prop(overlay, "enabled", text="", icon='CHECKBOX_HLT' if overlay.enabled else 'CHECKBOX_DEHLT')
# Overlay name
name_row = header_row.row()
name_row.enabled = overlay.enabled # Gray out name if disabled
name_row.prop(overlay, "name", text="")
# Action buttons
# Duplicate button - use safe operator call
premium_lock = create_premium_lock_function()
dup_op = safe_operator_call(header_row, "text_texture.duplicate_overlay", premium_lock,
text="", icon='DUPLICATE')
if dup_op:
dup_op.overlay_index = i
# Delete button - use safe operator call
del_op = safe_operator_call(header_row, "text_texture.delete_overlay", premium_lock,
text="", icon='TRASH')
if del_op:
del_op.overlay_index = i
# Move buttons for reordering - use safe operator calls
if i > 0:
move_up_op = safe_operator_call(header_row, "text_texture.move_overlay", premium_lock,
text="", icon='TRIA_UP')
if move_up_op:
move_up_op.overlay_index = i
move_up_op.direction = 'UP'
if i < len(props.image_overlays) - 1:
move_down_op = safe_operator_call(header_row, "text_texture.move_overlay", premium_lock,
text="", icon='TRIA_DOWN')
if move_down_op:
move_down_op.overlay_index = i
move_down_op.direction = 'DOWN'
# Show details section with enable/disable state
details_col = box.column()
details_col.enabled = overlay.enabled # Gray out all controls if disabled
# Status indicator
if not overlay.enabled:
pass
status_row = details_col.row()
status_row.label(text="⚠️ Overlay Disabled", icon='INFO')
details_col.separator()
# Image file selection
details_col.prop(overlay, "image_path", text="Image File")
# Positioning mode
details_col.prop(overlay, "positioning_mode", text="Position Mode")
if overlay.positioning_mode == 'ABSOLUTE':
pass
# Position controls for absolute mode
pos_row = details_col.row(align=True)
pos_row.prop(overlay, "x_position", text="X Position")
pos_row.prop(overlay, "y_position", text="Y Position")
elif overlay.positioning_mode in ['APPEND', 'PREPEND']:
# Spacing controls for append/prepend modes with percentage labels
details_col.prop(overlay, "text_spacing", text="Text Spacing (%)")
details_col.prop(overlay, "image_spacing", text="Image Spacing (%)")
# Transform controls
trans_row = details_col.row(align=True)
trans_row.prop(overlay, "scale", text="Scale")
trans_row.prop(overlay, "rotation", text="Rotation")
trans_row.prop(overlay, "z_index", text="Z-Level")
def draw_text_composition_section(layout, props):
pass
"""Draw additional text controls consolidated in the composition panel."""
section = layout.box()
section.use_property_split = False
section.use_property_decorate = False
section.label(text="Text Elements", icon='OUTLINER_OB_FONT')
layout_row = section.row(align=True)
layout_row.prop(props, "prepend_append_layout", text="Layout")
section.separator(factor=0.6)
def draw_text_block(label_text, icon, text_prop, margin_prop):
block = section.box()
block.use_property_split = False
block.use_property_decorate = False
header = block.row(align=True)
header.label(text=label_text, icon=icon)
text_row = block.row(align=True)
text_row.prop(props, text_prop, text="")
spacing_row = block.row(align=True)
spacing_row.enabled = bool(getattr(props, text_prop).strip())
spacing_row.prop(props, margin_prop, text="Spacing", slider=True)
draw_text_block("Prepend Text", 'TRIA_RIGHT', "prepend_text", "prepend_margin")
section.separator(factor=0.4)
draw_text_block("Append Text", 'TRIA_LEFT', "append_text", "append_margin")
def draw_composition_section(layout, props): def draw_composition_section(layout, props):
pass pass
"""Draw combined composition controls for text and images.""" """Draw combined composition controls for text and image components."""
if not has_image_overlays():
pass
draw_feature_lock_indicator(layout, 'image_overlays')
return
content = layout.column(align=True) content = layout.column(align=True)
content.use_property_split = False content.use_property_split = False
content.use_property_decorate = False content.use_property_decorate = False
tabs_row = content.row(align=True) draw_component_section(content, props, 'ABSOLUTE', "Absolute Components", 'CURSOR')
tabs_row.prop(props, "composition_active_tab", expand=True) draw_component_section(content, props, 'PREPEND', "Prepended Components", 'TRIA_LEFT_BAR')
draw_component_section(content, props, 'APPEND', "Appended Components", 'TRIA_RIGHT_BAR')
content.separator(factor=0.6)
if props.composition_active_tab == 'IMAGE':
pass
if not has_image_overlays():
pass
draw_feature_lock_indicator(content, 'image_overlays')
return
draw_composition_image_section(content, props)
else:
pass
draw_text_composition_section(content, props)
def draw_positioning_section(layout, props): def draw_positioning_section(layout, props):
pass pass

View File

@@ -485,7 +485,8 @@ class TestGenerateTextureImage:
overlay = MagicMock() overlay = MagicMock()
overlay.enabled = True overlay.enabled = True
overlay.image_path = str(overlay_path) overlay.image_path = str(overlay_path)
overlay.positioning_mode = 'ABSOLUTE' overlay.component_type = 'IMAGE'
overlay.placement_mode = 'ABSOLUTE'
overlay.x_position = 0.5 overlay.x_position = 0.5
overlay.y_position = 0.5 overlay.y_position = 0.5
overlay.scale = 1.0 overlay.scale = 1.0
@@ -537,7 +538,8 @@ class TestGenerateTextureImage:
overlay = MagicMock() overlay = MagicMock()
overlay.enabled = True overlay.enabled = True
overlay.image_path = str(svg_path) overlay.image_path = str(svg_path)
overlay.positioning_mode = 'ABSOLUTE' overlay.component_type = 'IMAGE'
overlay.placement_mode = 'ABSOLUTE'
overlay.x_position = 0.5 overlay.x_position = 0.5
overlay.y_position = 0.5 overlay.y_position = 0.5
overlay.scale = 1.0 overlay.scale = 1.0
@@ -580,7 +582,8 @@ class TestGenerateTextureImage:
overlay = MagicMock() overlay = MagicMock()
overlay.enabled = True overlay.enabled = True
overlay.image_path = "" overlay.image_path = ""
overlay.positioning_mode = 'ABSOLUTE' overlay.component_type = 'IMAGE'
overlay.placement_mode = 'ABSOLUTE'
overlay.x_position = 0.5 overlay.x_position = 0.5
overlay.y_position = 0.5 overlay.y_position = 0.5
overlay.scale = 1.0 overlay.scale = 1.0
@@ -619,7 +622,8 @@ class TestGenerateTextureImage:
overlay = MagicMock() overlay = MagicMock()
overlay.enabled = True overlay.enabled = True
overlay.image_path = str(overlay_path) overlay.image_path = str(overlay_path)
overlay.positioning_mode = 'APPEND' overlay.component_type = 'IMAGE'
overlay.placement_mode = 'APPEND'
overlay.text_spacing = 10.0 # push further right overlay.text_spacing = 10.0 # push further right
overlay.image_spacing = 0.0 overlay.image_spacing = 0.0
overlay.scale = 1.0 overlay.scale = 1.0
@@ -676,7 +680,8 @@ class TestGenerateTextureImage:
overlay = MagicMock() overlay = MagicMock()
overlay.enabled = True overlay.enabled = True
overlay.image_path = str(overlay_path) overlay.image_path = str(overlay_path)
overlay.positioning_mode = 'PREPEND' overlay.component_type = 'IMAGE'
overlay.placement_mode = 'PREPEND'
overlay.text_spacing = 10.0 # push further left overlay.text_spacing = 10.0 # push further left
overlay.image_spacing = 0.0 overlay.image_spacing = 0.0
overlay.scale = 1.0 overlay.scale = 1.0

View File

@@ -94,7 +94,6 @@ def make_default_props(**overrides):
manual_position_x=50.0, manual_position_x=50.0,
manual_position_y=50.0, manual_position_y=50.0,
constrain_to_canvas=True, constrain_to_canvas=True,
composition_active_tab="TEXT",
image_overlays=[], image_overlays=[],
active_overlay_index=0, active_overlay_index=0,
) )
@@ -134,14 +133,24 @@ def test_positioning_section_drops_additional_text(monkeypatch):
assert "append_text" not in prop_names assert "append_text" not in prop_names
def test_composition_section_shows_text_controls(monkeypatch): def test_composition_section_renders_text_component(monkeypatch):
"""Composition panel should surface text controls when text tab is active.""" """Composition panel should show text component controls within the proper section."""
layout = FakeLayout() layout = FakeLayout()
props = make_default_props( text_component = SimpleNamespace(
composition_active_tab="TEXT", enabled=True,
prepend_text="Intro", name="Intro Text",
append_text="Outro", component_type="TEXT",
placement_mode="PREPEND",
text_content="Intro",
text_spacing=10.0,
image_spacing=5.0,
x_position=0.5,
y_position=0.5,
scale=1.0,
rotation=0.0,
z_index=0,
) )
props = make_default_props(image_overlays=[text_component])
monkeypatch.setattr(panels, "safe_operator_call", lambda *args, **kwargs: SimpleNamespace()) monkeypatch.setattr(panels, "safe_operator_call", lambda *args, **kwargs: SimpleNamespace())
monkeypatch.setattr(panels, "has_image_overlays", lambda: True) monkeypatch.setattr(panels, "has_image_overlays", lambda: True)
@@ -151,21 +160,24 @@ def test_composition_section_shows_text_controls(monkeypatch):
calls = collect_calls(layout) calls = collect_calls(layout)
prop_names = [data["name"] for kind, data in calls if kind == "prop"] prop_names = [data["name"] for kind, data in calls if kind == "prop"]
labels = [data["text"] for kind, data in calls if kind == "label"] labels = [data["text"] for kind, data in calls if kind == "label"]
sections = [text for text in labels if "Components" in text]
assert "composition_active_tab" in prop_names assert "component_type" in prop_names
assert "prepend_text" in prop_names assert "text_content" in prop_names
assert "append_text" in prop_names assert "placement_mode" in prop_names
assert any("Text" in text for text in labels) assert "text_spacing" in prop_names
assert "Prepended Components" in sections
def test_composition_section_shows_overlay_controls(monkeypatch): def test_composition_section_renders_image_component(monkeypatch):
"""Composition panel should surface overlay controls when image tab is active.""" """Composition panel should show image component controls within the proper section."""
layout = FakeLayout() layout = FakeLayout()
overlay = SimpleNamespace( image_component = SimpleNamespace(
enabled=True, enabled=True,
name="Logo", name="Logo",
component_type="IMAGE",
placement_mode="ABSOLUTE",
image_path="/tmp/logo.png", image_path="/tmp/logo.png",
positioning_mode="ABSOLUTE",
x_position=0.5, x_position=0.5,
y_position=0.5, y_position=0.5,
text_spacing=10.0, text_spacing=10.0,
@@ -174,10 +186,7 @@ def test_composition_section_shows_overlay_controls(monkeypatch):
rotation=0.0, rotation=0.0,
z_index=1, z_index=1,
) )
props = make_default_props( props = make_default_props(image_overlays=[image_component])
composition_active_tab="IMAGE",
image_overlays=[overlay],
)
monkeypatch.setattr(panels, "safe_operator_call", lambda *args, **kwargs: SimpleNamespace()) monkeypatch.setattr(panels, "safe_operator_call", lambda *args, **kwargs: SimpleNamespace())
monkeypatch.setattr(panels, "has_image_overlays", lambda: True) monkeypatch.setattr(panels, "has_image_overlays", lambda: True)
@@ -187,7 +196,10 @@ def test_composition_section_shows_overlay_controls(monkeypatch):
calls = collect_calls(layout) calls = collect_calls(layout)
prop_names = [data["name"] for kind, data in calls if kind == "prop"] prop_names = [data["name"] for kind, data in calls if kind == "prop"]
labels = [data["text"] for kind, data in calls if kind == "label"] labels = [data["text"] for kind, data in calls if kind == "label"]
sections = [text for text in labels if "Components" in text]
assert "composition_active_tab" in prop_names assert "component_type" in prop_names
assert "positioning_mode" in prop_names assert "image_path" in prop_names
assert any("Image" in text for text in labels) assert "placement_mode" in prop_names
assert "x_position" in prop_names
assert "Absolute Components" in sections