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))
bicubic_filter = getattr(resampling_module, 'BICUBIC', getattr(Image, 'BICUBIC', Image.BILINEAR))
def _iter_enabled_overlays():
"""Yield (overlay, image_path) tuples for overlays that should be applied."""
def _iter_enabled_components():
"""Yield enabled composition components."""
overlays = getattr(props, 'image_overlays', [])
try:
overlay_list = list(overlays)
@@ -62,17 +62,14 @@ def generate_texture_image(props, width, height):
for overlay in overlay_list:
if not getattr(overlay, 'enabled', True):
continue
image_path = getattr(overlay, 'image_path', '')
if not image_path:
continue
yield overlay, image_path
yield overlay
def _calculate_overlay_position(overlay, overlay_size):
"""Calculate top-left pixel position for an overlay image."""
overlay_width, overlay_height = overlay_size
canvas_width = width
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)
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))
return x, y
def _apply_image_overlays(pil_canvas):
"""Composite enabled image overlays onto the base canvas."""
overlays_to_apply = sorted(
_iter_enabled_overlays(),
key=lambda item: getattr(item[0], 'z_index', 0)
def _apply_composition_components(pil_canvas):
"""Composite enabled components (text or image) onto the base canvas."""
components = sorted(
_iter_enabled_components(),
key=lambda comp: getattr(comp, 'z_index', 0)
)
for overlay, image_path in overlays_to_apply:
resolved_path = image_path
scale = getattr(overlay, 'scale', 1.0)
for component in components:
if getattr(component, 'component_type', 'IMAGE') == 'TEXT':
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:
scale = float(scale if scale is not None else 1.0)
except (TypeError, ValueError):
@@ -121,7 +159,7 @@ def generate_texture_image(props, width, height):
resolved_path = raster_info.path
scale_to_apply = raster_info.applied_scale
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
try:
@@ -139,11 +177,11 @@ def generate_texture_image(props, width, height):
if new_size != overlay_img.size:
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:
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.paste(overlay_img, (x_pos, y_pos), overlay_img)
@@ -358,7 +396,7 @@ def generate_texture_image(props, width, height):
pass
# 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)
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
overlay_data = {
'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,
'x_position': overlay.x_position,
'y_position': overlay.y_position,
@@ -397,6 +402,11 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
pass
overlay = props.image_overlays.add()
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.x_position = overlay_data.get('x_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):
pass
"""Add new image overlay"""
"""Add new composition component"""
bl_idname = "text_texture.add_overlay"
bl_label = "Add Overlay"
bl_description = "Add new image overlay"
bl_label = "Add Component"
bl_description = "Add new composition component"
bl_options = {'REGISTER', 'UNDO'}
placement_mode: EnumProperty(
name="Placement Mode",
items=[
('ABSOLUTE', "Absolute", ""),
('PREPEND', "Prepend", ""),
('APPEND', "Append", "")
],
default='ABSOLUTE'
)
def execute(self, context):
pass
props = context.scene.text_texture_props
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
# Force UI redraw
@@ -360,7 +372,7 @@ class TEXT_TEXTURE_OT_add_overlay(Operator):
pass
area.tag_redraw()
self.report({'INFO'}, f"Added overlay: {overlay.name}")
self.report({'INFO'}, f"Added component: {overlay.name}")
return {'FINISHED'}
@@ -435,10 +447,10 @@ class TEXT_TEXTURE_OT_sync_margins(Operator):
class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
pass
"""Duplicate image overlay"""
"""Duplicate a composition component"""
bl_idname = "text_texture.duplicate_overlay"
bl_label = "Duplicate Overlay"
bl_description = "Duplicate this image overlay"
bl_label = "Duplicate Component"
bl_description = "Duplicate this composition component"
bl_options = {'REGISTER', 'UNDO'}
overlay_index: IntProperty()
@@ -457,7 +469,9 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
# Copy all properties with explicit verification
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.text_content = source_overlay.text_content
new_overlay.x_position = source_overlay.x_position
new_overlay.y_position = source_overlay.y_position
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
# For PREPEND/APPEND overlays, increment z_index to ensure duplicate appears above original
# 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
# 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
@@ -473,7 +487,7 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
pass
# For ABSOLUTE overlays, keep same z_index (different positions so no overlap issue)
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.image_spacing = source_overlay.image_spacing
new_overlay.enabled = source_overlay.enabled
@@ -517,7 +531,7 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
props.is_updating = False
self.report({'INFO'}, f"✅ Duplicated overlay: {source_overlay.name}")
self.report({'INFO'}, f"✅ Duplicated component: {source_overlay.name}")
else:
pass
self.report({'ERROR'}, "Invalid overlay index")
@@ -528,10 +542,10 @@ class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
class TEXT_TEXTURE_OT_delete_overlay(Operator):
pass
"""Delete image overlay"""
"""Delete a composition component"""
bl_idname = "text_texture.delete_overlay"
bl_label = "Delete Overlay"
bl_description = "Delete this image overlay"
bl_label = "Delete Component"
bl_description = "Delete this composition component"
bl_options = {'REGISTER', 'UNDO'}
overlay_index: IntProperty()
@@ -567,7 +581,7 @@ class TEXT_TEXTURE_OT_delete_overlay(Operator):
pass
area.tag_redraw()
self.report({'INFO'}, f"Deleted overlay: {overlay_name}")
self.report({'INFO'}, f"Deleted component: {overlay_name}")
else:
pass
self.report({'ERROR'}, "Invalid overlay index")
@@ -578,10 +592,10 @@ class TEXT_TEXTURE_OT_delete_overlay(Operator):
class TEXT_TEXTURE_OT_move_overlay(Operator):
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_label = "Move Overlay"
bl_description = "Move overlay up or down in the list"
bl_label = "Move Component"
bl_description = "Move component up or down within its placement group"
bl_options = {'REGISTER', 'UNDO'}
overlay_index: IntProperty()
@@ -601,12 +615,15 @@ class TEXT_TEXTURE_OT_move_overlay(Operator):
pass
current_index = self.overlay_index
new_index = current_index
target_mode = props.image_overlays[current_index].placement_mode
if self.direction == 'UP' and current_index > 0:
pass
new_index = current_index - 1
elif self.direction == 'DOWN' and current_index < len(props.image_overlays) - 1:
new_index = current_index + 1
step = -1 if self.direction == 'UP' else 1
probe = current_index + step
while 0 <= probe < len(props.image_overlays):
if props.image_overlays[probe].placement_mode == target_mode:
new_index = probe
break
probe += step
if new_index != current_index:
pass
@@ -622,10 +639,10 @@ class TEXT_TEXTURE_OT_move_overlay(Operator):
except Exception as e:
pass
self.report({'INFO'}, f"Moved overlay {self.direction.lower()}")
self.report({'INFO'}, f"Moved component {self.direction.lower()}")
else:
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:
pass
self.report({'ERROR'}, "Invalid overlay index")

View File

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

View File

@@ -50,19 +50,77 @@ def get_font_enum_items(self, context):
return items
class TEXT_TEXTURE_ImageOverlay(PropertyGroup):
class TEXT_TEXTURE_CompositionComponent(PropertyGroup):
pass
"""Single image overlay item"""
"""Single composition component that can be text or image."""
name: StringProperty(
name="Image Name",
description="Name identifier for this image overlay",
default="Overlay",
name="Component Name",
description="Name identifier for this component",
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
)
image_path: StringProperty(
name="Image File",
description="Path to image file",
description="Path to image file for image components",
subtype='FILE_PATH',
default="",
update=update_live_preview
@@ -88,7 +146,7 @@ class TEXT_TEXTURE_ImageOverlay(PropertyGroup):
scale: FloatProperty(
name="Scale",
description="Scale of the image",
description="Scale factor for this component",
default=1.0,
min=0.1,
max=5.0,
@@ -97,7 +155,7 @@ class TEXT_TEXTURE_ImageOverlay(PropertyGroup):
rotation: FloatProperty(
name="Rotation",
description="Rotation in degrees",
description="Rotation in degrees (image components only)",
default=0.0,
min=-360.0,
max=360.0,
@@ -113,44 +171,8 @@ class TEXT_TEXTURE_ImageOverlay(PropertyGroup):
update=update_live_preview
)
positioning_mode: EnumProperty(
name="Positioning Mode",
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
)
# Backwards compatibility alias (legacy name used throughout codebase)
TEXT_TEXTURE_ImageOverlay = TEXT_TEXTURE_CompositionComponent
class TEXT_TEXTURE_Preset(PropertyGroup):
pass
@@ -281,16 +303,6 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
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(
name="Width",
description="Texture width in pixels",
@@ -603,8 +615,8 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
update=update_live_preview
)
# Image overlays
image_overlays: CollectionProperty(type=TEXT_TEXTURE_ImageOverlay)
# Composition components (legacy name retained for compatibility)
image_overlays: CollectionProperty(type=TEXT_TEXTURE_CompositionComponent)
active_overlay_index: IntProperty(default=0)
# Presets

View File

@@ -229,163 +229,122 @@ def draw_generate_button(layout):
row.scale_y = 1.5
row.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA')
def draw_composition_image_section(layout, props):
pass
"""Draw image overlay controls for the composition panel."""
def _iter_components_by_placement(props, placement_mode):
"""Yield (index, component) pairs filtered by placement mode."""
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.use_property_split = False
section.use_property_decorate = False
section.label(text="Image Elements", icon='IMAGE_DATA')
# Check if image overlays are available
if not has_image_overlays():
pass
draw_feature_lock_indicator(section, 'image_overlays')
header = section.row(align=True)
header.label(text=title, icon=icon)
# 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
# Add overlay button - use safe operator call
row = section.row()
safe_operator_call(row, "text_texture.add_overlay",
lambda layout: draw_feature_lock_indicator(layout, 'image_overlays'),
text="Add Image Overlay", icon='ADD')
# 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
for index, component in components:
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")
_draw_component_header(header_row, component)
_draw_component_footer(header_row, props, index)
_draw_component_details(box, component)
def draw_composition_section(layout, props):
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.use_property_split = False
content.use_property_decorate = False
tabs_row = content.row(align=True)
tabs_row.prop(props, "composition_active_tab", expand=True)
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)
draw_component_section(content, props, 'ABSOLUTE', "Absolute Components", 'CURSOR')
draw_component_section(content, props, 'PREPEND', "Prepended Components", 'TRIA_LEFT_BAR')
draw_component_section(content, props, 'APPEND', "Appended Components", 'TRIA_RIGHT_BAR')
def draw_positioning_section(layout, props):
pass

View File

@@ -485,7 +485,8 @@ class TestGenerateTextureImage:
overlay = MagicMock()
overlay.enabled = True
overlay.image_path = str(overlay_path)
overlay.positioning_mode = 'ABSOLUTE'
overlay.component_type = 'IMAGE'
overlay.placement_mode = 'ABSOLUTE'
overlay.x_position = 0.5
overlay.y_position = 0.5
overlay.scale = 1.0
@@ -537,7 +538,8 @@ class TestGenerateTextureImage:
overlay = MagicMock()
overlay.enabled = True
overlay.image_path = str(svg_path)
overlay.positioning_mode = 'ABSOLUTE'
overlay.component_type = 'IMAGE'
overlay.placement_mode = 'ABSOLUTE'
overlay.x_position = 0.5
overlay.y_position = 0.5
overlay.scale = 1.0
@@ -580,7 +582,8 @@ class TestGenerateTextureImage:
overlay = MagicMock()
overlay.enabled = True
overlay.image_path = ""
overlay.positioning_mode = 'ABSOLUTE'
overlay.component_type = 'IMAGE'
overlay.placement_mode = 'ABSOLUTE'
overlay.x_position = 0.5
overlay.y_position = 0.5
overlay.scale = 1.0
@@ -619,7 +622,8 @@ class TestGenerateTextureImage:
overlay = MagicMock()
overlay.enabled = True
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.image_spacing = 0.0
overlay.scale = 1.0
@@ -676,7 +680,8 @@ class TestGenerateTextureImage:
overlay = MagicMock()
overlay.enabled = True
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.image_spacing = 0.0
overlay.scale = 1.0

View File

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