diff --git a/dist/text_texture_generator_v1.0.0.zip b/dist/text_texture_generator_v1.0.0.zip index 719868f..ca8fb8c 100644 Binary files a/dist/text_texture_generator_v1.0.0.zip and b/dist/text_texture_generator_v1.0.0.zip differ diff --git a/dist/text_texture_generator_v1.0.0_free.zip b/dist/text_texture_generator_v1.0.0_free.zip index 1d5e0ea..74899c5 100644 Binary files a/dist/text_texture_generator_v1.0.0_free.zip and b/dist/text_texture_generator_v1.0.0_free.zip differ diff --git a/marketing/demo.blend b/marketing/demo.blend index 82551c8..62df8f8 100644 Binary files a/marketing/demo.blend and b/marketing/demo.blend differ diff --git a/marketing/demo.blend1 b/marketing/demo.blend1 index 242bf8f..82551c8 100644 Binary files a/marketing/demo.blend1 and b/marketing/demo.blend1 differ diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc index 38042fb..266d1b1 100644 Binary files a/src/__pycache__/__init__.cpython-311.pyc and b/src/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/core/__pycache__/generation_engine.cpython-311.pyc b/src/core/__pycache__/generation_engine.cpython-311.pyc index c0340fe..392f0fe 100644 Binary files a/src/core/__pycache__/generation_engine.cpython-311.pyc and b/src/core/__pycache__/generation_engine.cpython-311.pyc differ diff --git a/src/core/generation_engine.py b/src/core/generation_engine.py index 01bf031..321b7bd 100644 --- a/src/core/generation_engine.py +++ b/src/core/generation_engine.py @@ -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 diff --git a/src/operators/__pycache__/preset_ops.cpython-311.pyc b/src/operators/__pycache__/preset_ops.cpython-311.pyc index e5db1f3..20f884c 100644 Binary files a/src/operators/__pycache__/preset_ops.cpython-311.pyc and b/src/operators/__pycache__/preset_ops.cpython-311.pyc differ diff --git a/src/operators/__pycache__/utility_ops.cpython-311.pyc b/src/operators/__pycache__/utility_ops.cpython-311.pyc index f0c39a0..cc129ad 100644 Binary files a/src/operators/__pycache__/utility_ops.cpython-311.pyc and b/src/operators/__pycache__/utility_ops.cpython-311.pyc differ diff --git a/src/operators/preset_ops.py b/src/operators/preset_ops.py index 6e5d196..13dace5 100644 --- a/src/operators/preset_ops.py +++ b/src/operators/preset_ops.py @@ -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) diff --git a/src/operators/utility_ops.py b/src/operators/utility_ops.py index 53aa025..49e954c 100644 --- a/src/operators/utility_ops.py +++ b/src/operators/utility_ops.py @@ -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") @@ -647,4 +664,4 @@ __all__ = [ 'TEXT_TEXTURE_OT_duplicate_overlay', 'TEXT_TEXTURE_OT_delete_overlay', 'TEXT_TEXTURE_OT_move_overlay', -] \ No newline at end of file +] diff --git a/src/properties/__init__.py b/src/properties/__init__.py index 50de3a4..c5c3570 100644 --- a/src/properties/__init__.py +++ b/src/properties/__init__.py @@ -6,13 +6,15 @@ 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', -] \ No newline at end of file +] diff --git a/src/properties/__pycache__/__init__.cpython-311.pyc b/src/properties/__pycache__/__init__.cpython-311.pyc index 04a363d..e7b535e 100644 Binary files a/src/properties/__pycache__/__init__.cpython-311.pyc and b/src/properties/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/properties/__pycache__/property_groups.cpython-311.pyc b/src/properties/__pycache__/property_groups.cpython-311.pyc index 77630c1..3c09a2b 100644 Binary files a/src/properties/__pycache__/property_groups.cpython-311.pyc and b/src/properties/__pycache__/property_groups.cpython-311.pyc differ diff --git a/src/properties/property_groups.py b/src/properties/property_groups.py index 7f78e81..c936d50 100644 --- a/src/properties/property_groups.py +++ b/src/properties/property_groups.py @@ -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, @@ -112,45 +170,9 @@ class TEXT_TEXTURE_ImageOverlay(PropertyGroup): max=10, 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 diff --git a/src/ui/__pycache__/panels.cpython-311.pyc b/src/ui/__pycache__/panels.cpython-311.pyc index 2907a24..bd68e28 100644 Binary files a/src/ui/__pycache__/panels.cpython-311.pyc and b/src/ui/__pycache__/panels.cpython-311.pyc differ diff --git a/src/ui/panels.py b/src/ui/panels.py index 2e10f9e..5659df1 100644 --- a/src/ui/panels.py +++ b/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 diff --git a/src/utils/__pycache__/constants.cpython-311.pyc b/src/utils/__pycache__/constants.cpython-311.pyc index 165163f..3245e45 100644 Binary files a/src/utils/__pycache__/constants.cpython-311.pyc and b/src/utils/__pycache__/constants.cpython-311.pyc differ diff --git a/tests/unit/core/__pycache__/test_generation_engine.cpython-311-pytest-8.3.5.pyc b/tests/unit/core/__pycache__/test_generation_engine.cpython-311-pytest-8.3.5.pyc index f12241b..44a653f 100644 Binary files a/tests/unit/core/__pycache__/test_generation_engine.cpython-311-pytest-8.3.5.pyc and b/tests/unit/core/__pycache__/test_generation_engine.cpython-311-pytest-8.3.5.pyc differ diff --git a/tests/unit/core/test_generation_engine.py b/tests/unit/core/test_generation_engine.py index bf7e22a..5307507 100644 --- a/tests/unit/core/test_generation_engine.py +++ b/tests/unit/core/test_generation_engine.py @@ -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 diff --git a/tests/unit/ui/test_panel_layout_overhaul.py b/tests/unit/ui/test_panel_layout_overhaul.py index 51723e8..7ed603f 100644 --- a/tests/unit/ui/test_panel_layout_overhaul.py +++ b/tests/unit/ui/test_panel_layout_overhaul.py @@ -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