wip
This commit is contained in:
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
Binary file not shown.
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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',
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
247
src/ui/panels.py
247
src/ui/panels.py
@@ -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
|
||||
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")
|
||||
for index, component in components:
|
||||
box = section.box()
|
||||
header_row = box.row(align=True)
|
||||
_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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user