1.1.0
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,7 +31,6 @@ htmlcov/
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
*.egg
|
||||
*.whl
|
||||
|
||||
@@ -411,6 +411,10 @@ Marc Mintel <marc@mintel.me>
|
||||
- Import/export functionality for preset backup and cross-installation sharing
|
||||
- Enhanced error handling and user feedback throughout the interface
|
||||
|
||||
### Version 1.1.0
|
||||
|
||||
- Added a pro-only Create Plane button that generates a plane matching the texture aspect ratio, applies the new material automatically, and configures a Simple subdivision modifier by default.
|
||||
|
||||
### Version 1.0.0 (Current Release)
|
||||
|
||||
**New Build Strategy**: Redesigned version distribution from runtime feature flags to build-time file exclusion for better security and genuine feature separation.
|
||||
|
||||
BIN
dist/text_texture_generator_v1.1.0.zip
vendored
Normal file
BIN
dist/text_texture_generator_v1.1.0.zip
vendored
Normal file
Binary file not shown.
BIN
dist/text_texture_generator_v1.1.0_free.zip
vendored
Normal file
BIN
dist/text_texture_generator_v1.1.0_free.zip
vendored
Normal file
Binary file not shown.
@@ -35,6 +35,7 @@ Everything in the Free Edition, plus:
|
||||
- **Composition components.** Stack any number of text or image overlays, sequence them before/after the main copy, or position them absolutely with independent scale, rotation, spacing, and Z-order. SVG overlays rasterize automatically (via CairoSVG) so they stay pin-sharp.
|
||||
- **Automatic normal maps.** Turn the alpha channel into a normal map with adjustable strength, blur radius, and inversion for emboss/deboss looks.
|
||||
- **Shader builder.** Create Principled BSDF, Emission, or Transparent materials complete with normal-map hookups, light-path controls (disable shadows, reflections, or backfaces), and optional auto-assignment to the active object.
|
||||
- **Plane instancing.** Drop a plane that matches your texture’s aspect ratio with the generated material applied and a Simple subdivision modifier ready for displacement workflows.
|
||||
- **Preset ecosystem.** Save styles either with the current `.blend` or in Blender’s config folder, refresh them in one click, and import/export JSON bundles for backups or team sharing.
|
||||
- **Adaptive text fitting.** Let the addon resize fonts within a range to keep layouts within your chosen canvas margins.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ Create polished text textures without leaving Blender. This addon drops a dedica
|
||||
- **Overlay system.** Add any number of text or image overlays with independent scale, rotation, spacing, z-index, and absolute/prepend/append placement. SVG overlays are rasterized automatically with CairoSVG for crisp results.
|
||||
- **Normal maps.** Derive normal maps straight from the text alpha channel with strength, blur, and invert controls.
|
||||
- **One-click shaders.** Build Principled, Emission, or Transparent materials complete with optional normal-map hookups, light-path controls (disable shadows/reflections/backfaces), and automatic assignment to the active object.
|
||||
- **Plane instancing.** Create a plane with matching aspect ratio, the generated material applied, and a Simple subdivision modifier ready for displacement or lighting tests.
|
||||
- **Preset workflow.** Save styles to the current `.blend` or Blender’s config folder, refresh them, and import/export JSON bundles for backups or team sharing.
|
||||
- **Adaptive fitting.** Let the addon resize fonts within a min/max range to keep text safely within your canvas margins.
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ Bring typography directly into your Blender workflow. The Text Texture Generator
|
||||
- Unlimited composition components: combine text and image overlays, order them before/after the main copy, or position them absolutely with independent scale, rotation, spacing, and z-index. SVG overlays rasterize automatically via CairoSVG.
|
||||
- Normal map creation from the alpha channel with strength, blur, and invert controls.
|
||||
- One-click shader builder for Principled, Emission, or Transparent materials, including optional light-path tweaks and auto-assignment to the active object.
|
||||
- Plane instancing that drops a correctly proportioned plane with the new material applied and a Simple subdivision modifier for displacement workflows.
|
||||
- Dual-location presets that save to the current `.blend` or Blender’s config folder, plus import/export for teams and backups.
|
||||
- Adaptive text fitting to keep designs inside custom canvas margins without manual tweaking.
|
||||
|
||||
|
||||
BIN
marketing/promo.mov
Normal file
BIN
marketing/promo.mov
Normal file
Binary file not shown.
BIN
marketing/showcase.blend
Normal file
BIN
marketing/showcase.blend
Normal file
Binary file not shown.
@@ -207,27 +207,25 @@ def _format_version_tuple(version_tuple: tuple) -> str:
|
||||
|
||||
|
||||
def _edition_version_tuple(version: str, version_type: str) -> tuple:
|
||||
"""Return edition-specific version tuple."""
|
||||
"""Return edition-specific version tuple without altering the provided version."""
|
||||
try:
|
||||
major_str, minor_str, patch_str = version.split(".", 2)
|
||||
major, minor, patch = int(major_str), int(minor_str), int(patch_str)
|
||||
except ValueError:
|
||||
# Fallback if version string unexpected
|
||||
return (1, 0, 0) if version_type == "free" else (1, 0, 1)
|
||||
return (1, 0, 0)
|
||||
|
||||
if version_type == "full":
|
||||
return (major, minor, patch + 1)
|
||||
return (major, minor, patch)
|
||||
|
||||
|
||||
def _apply_edition_overrides(rel_path: Path, source_code: str, version_type: str, version: str) -> str:
|
||||
"""Inject edition-specific metadata into copied source files."""
|
||||
if rel_path.as_posix() == "__init__.py":
|
||||
edition_name = "Text Texture Generator (Pro)" if version_type == "full" else "Text Texture Generator (Free)"
|
||||
edition_name = "Text Texture Generator Pro" if version_type == "full" else "Text Texture Generator (Free)"
|
||||
target_version_tuple = _edition_version_tuple(version, version_type)
|
||||
|
||||
source_code = re.sub(
|
||||
r'"name":\s*"Text Texture Generator\s*\([^"]*\)"',
|
||||
r'"name":\s*"Text Texture Generator[^"]*"',
|
||||
f'"name": "{edition_name}"',
|
||||
source_code,
|
||||
count=1
|
||||
@@ -438,14 +436,16 @@ def build_all_versions(version: Optional[str] = None, minify: bool = True):
|
||||
print("\n" + "="*50)
|
||||
print("BUILDING FULL VERSION")
|
||||
print("="*50)
|
||||
full_package = build_addon("full", version, minify=minify)
|
||||
resolved_version = version or get_version_from_git() or "1.0.0"
|
||||
|
||||
full_package = build_addon("full", resolved_version, minify=minify)
|
||||
built_packages.append(("full", full_package))
|
||||
|
||||
# Build free version
|
||||
print("\n" + "="*50)
|
||||
print("BUILDING FREE VERSION")
|
||||
print("="*50)
|
||||
free_package = build_addon("free", version, minify=minify)
|
||||
free_package = build_addon("free", resolved_version, minify=minify)
|
||||
built_packages.append(("free", free_package))
|
||||
|
||||
print("\n" + "="*50)
|
||||
@@ -455,6 +455,19 @@ def build_all_versions(version: Optional[str] = None, minify: bool = True):
|
||||
file_size = package_path.stat().st_size / 1024 # KB
|
||||
print(f"{version_type.upper():>4}: {package_path.name} ({file_size:.1f} KB)")
|
||||
|
||||
# Restore development files to the full-version state so local testing keeps premium features visible
|
||||
project_root = get_project_root()
|
||||
restore_sync = subprocess.run(
|
||||
[
|
||||
'python3', 'build/sync_version.py',
|
||||
'--version', resolved_version,
|
||||
'--type', 'full'
|
||||
],
|
||||
cwd=project_root
|
||||
)
|
||||
if restore_sync.returncode != 0:
|
||||
raise RuntimeError("Failed to resync version metadata after build")
|
||||
|
||||
return built_packages
|
||||
|
||||
def main():
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# IMPORTANT: bl_info must contain ONLY literal values (no expressions/conditionals)
|
||||
# for ast.literal_eval() to parse it during addon discovery
|
||||
bl_info = {
|
||||
"name": "Text Texture Generator (Free)",
|
||||
"name": "Text Texture Generator Pro",
|
||||
"author": "Marc Mintel <marc@mintel.me>",
|
||||
"version": (1, 0, 0),
|
||||
"version": (1, 1, 0),
|
||||
"blender": (4, 0, 0),
|
||||
"location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab",
|
||||
"description": "Generate image textures from text with accurate dimensions and powerful styling options",
|
||||
@@ -82,6 +82,8 @@ from .operators import (
|
||||
TEXT_TEXTURE_OT_generate,
|
||||
TEXT_TEXTURE_OT_preview,
|
||||
TEXT_TEXTURE_OT_generate_shader,
|
||||
TEXT_TEXTURE_OT_generate_plane,
|
||||
TEXT_TEXTURE_OT_reset_settings,
|
||||
)
|
||||
|
||||
# Try to import utility operators (might not be available in free version)
|
||||
@@ -163,6 +165,8 @@ classes = (
|
||||
TEXT_TEXTURE_OT_generate,
|
||||
TEXT_TEXTURE_OT_preview,
|
||||
TEXT_TEXTURE_OT_generate_shader,
|
||||
TEXT_TEXTURE_OT_generate_plane,
|
||||
TEXT_TEXTURE_OT_reset_settings,
|
||||
TEXT_TEXTURE_OT_refresh_preview,
|
||||
TEXT_TEXTURE_OT_view_preview_zoom,
|
||||
TEXT_TEXTURE_OT_add_overlay,
|
||||
@@ -323,4 +327,4 @@ def unregister():
|
||||
del bpy.types.Scene.text_texture_props
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
register()
|
||||
|
||||
Binary file not shown.
@@ -13,6 +13,8 @@ from .generation_ops import (
|
||||
TEXT_TEXTURE_OT_generate,
|
||||
TEXT_TEXTURE_OT_preview,
|
||||
TEXT_TEXTURE_OT_generate_shader,
|
||||
TEXT_TEXTURE_OT_generate_plane,
|
||||
TEXT_TEXTURE_OT_reset_settings,
|
||||
)
|
||||
|
||||
# Conditional imports for modules that might not exist in free version
|
||||
@@ -114,6 +116,8 @@ __all__.extend([
|
||||
'TEXT_TEXTURE_OT_generate',
|
||||
'TEXT_TEXTURE_OT_preview',
|
||||
'TEXT_TEXTURE_OT_generate_shader',
|
||||
'TEXT_TEXTURE_OT_generate_plane',
|
||||
'TEXT_TEXTURE_OT_reset_settings',
|
||||
])
|
||||
|
||||
# Add preset operators if available
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -7,11 +7,26 @@ import bpy
|
||||
from bpy.types import Operator
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from math import isfinite
|
||||
|
||||
# Import required functions from other modules
|
||||
from ..core.generation_engine import generate_texture_image, generate_preview
|
||||
|
||||
|
||||
def _sanitize_text_for_name(text):
|
||||
sanitized_text = re.sub(r'[^\w\s-]', '', text[:20])
|
||||
sanitized_text = re.sub(r'[\s_-]+', '_', sanitized_text).strip('_')
|
||||
return sanitized_text or "Texture"
|
||||
|
||||
|
||||
def _resolve_material_name(props, sanitized_text):
|
||||
custom_name = props.custom_material_name.strip()
|
||||
if custom_name:
|
||||
return custom_name
|
||||
return f"TextTexture_Mat_{sanitized_text}"
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_generate(Operator):
|
||||
pass
|
||||
"""Generate final texture and pack it"""
|
||||
@@ -56,15 +71,7 @@ class TEXT_TEXTURE_OT_generate(Operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create diffuse texture with proper sanitization
|
||||
import re
|
||||
# Remove non-ASCII and non-alphanumeric characters, keep spaces temporarily
|
||||
sanitized_text = re.sub(r'[^\w\s-]', '', props.text[:20])
|
||||
# Replace spaces and multiple underscores/dashes with single underscores
|
||||
sanitized_text = re.sub(r'[\s_-]+', '_', sanitized_text).strip('_')
|
||||
# Fallback if text becomes empty
|
||||
if not sanitized_text:
|
||||
pass
|
||||
sanitized_text = "Texture"
|
||||
sanitized_text = _sanitize_text_for_name(props.text)
|
||||
img_name = f"TextTexture_{sanitized_text}"
|
||||
|
||||
# Remove existing diffuse texture
|
||||
@@ -278,15 +285,7 @@ class TEXT_TEXTURE_OT_generate_shader(Operator):
|
||||
|
||||
|
||||
# Create diffuse texture name with proper sanitization
|
||||
import re
|
||||
# Remove non-ASCII and non-alphanumeric characters, keep spaces temporarily
|
||||
sanitized_text = re.sub(r'[^\w\s-]', '', props.text[:20])
|
||||
# Replace spaces and multiple underscores/dashes with single underscores
|
||||
sanitized_text = re.sub(r'[\s_-]+', '_', sanitized_text).strip('_')
|
||||
# Fallback if text becomes empty
|
||||
if not sanitized_text:
|
||||
pass
|
||||
sanitized_text = "Texture"
|
||||
sanitized_text = _sanitize_text_for_name(props.text)
|
||||
img_name = f"TextTexture_{sanitized_text}"
|
||||
|
||||
# Remove existing diffuse texture if it exists
|
||||
@@ -330,13 +329,7 @@ class TEXT_TEXTURE_OT_generate_shader(Operator):
|
||||
pass
|
||||
|
||||
# Create or get material using custom name or fallback
|
||||
if props.custom_material_name.strip():
|
||||
pass
|
||||
material_name = props.custom_material_name.strip()
|
||||
else:
|
||||
pass
|
||||
# Use the same sanitized text for consistency
|
||||
material_name = f"TextTexture_Mat_{sanitized_text}"
|
||||
material_name = _resolve_material_name(props, sanitized_text)
|
||||
|
||||
if material_name in bpy.data.materials:
|
||||
pass
|
||||
@@ -743,9 +736,136 @@ class TEXT_TEXTURE_OT_generate_shader(Operator):
|
||||
traceback.print_exc()
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_generate_plane(Operator):
|
||||
"""Generate shader and create a plane with matching aspect ratio"""
|
||||
bl_idname = "text_texture.generate_plane"
|
||||
bl_label = "Create Plane"
|
||||
bl_description = "Generate the material and add a plane matching the texture aspect ratio"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
result = bpy.ops.text_texture.generate_shader()
|
||||
if 'FINISHED' not in result:
|
||||
self.report({'ERROR'}, "Shader generation failed, plane not created")
|
||||
return {'CANCELLED'}
|
||||
|
||||
sanitized_text = _sanitize_text_for_name(props.text)
|
||||
material_name = _resolve_material_name(props, sanitized_text)
|
||||
mat = bpy.data.materials.get(material_name)
|
||||
|
||||
if not mat:
|
||||
self.report({'ERROR'}, f"Material '{material_name}' not found after generation")
|
||||
return {'CANCELLED'}
|
||||
|
||||
width = float(getattr(props, "texture_width", 1) or 1)
|
||||
height = float(getattr(props, "texture_height", 1) or 1)
|
||||
aspect = width / height if height and isfinite(width / height) else 1.0
|
||||
|
||||
bpy.ops.mesh.primitive_plane_add(size=1)
|
||||
plane = context.active_object
|
||||
|
||||
if not plane or plane.type != 'MESH':
|
||||
self.report({'ERROR'}, "Failed to create plane object")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if aspect > 1.0:
|
||||
plane.scale.x *= aspect
|
||||
elif aspect > 0.0:
|
||||
plane.scale.y *= 1.0 / aspect
|
||||
|
||||
subdiv = plane.modifiers.new(name="TTG Subdivision", type='SUBSURF')
|
||||
if hasattr(subdiv, "subdivision_type"):
|
||||
subdiv.subdivision_type = 'SIMPLE'
|
||||
subdiv.levels = 1
|
||||
subdiv.render_levels = 1
|
||||
|
||||
if plane.data.materials:
|
||||
plane.data.materials[0] = mat
|
||||
else:
|
||||
plane.data.materials.append(mat)
|
||||
|
||||
self.report({'INFO'}, f"Created plane '{plane.name}' with '{material_name}'")
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, f"Plane creation failed: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_reset_settings(Operator):
|
||||
"""Reset all Text Texture settings to their defaults"""
|
||||
bl_idname = "text_texture.reset_settings"
|
||||
bl_label = "Reset Text Texture Settings"
|
||||
bl_description = "Restore all Text Texture settings to their default values"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return hasattr(context.scene, "text_texture_props")
|
||||
|
||||
@staticmethod
|
||||
def _collect_property_names(props):
|
||||
names = set()
|
||||
|
||||
cls = props.__class__
|
||||
annotations = getattr(cls, "__annotations__", {})
|
||||
names.update(annotations.keys())
|
||||
|
||||
bl_rna = getattr(props, "bl_rna", None)
|
||||
if bl_rna and hasattr(bl_rna, "properties"):
|
||||
try:
|
||||
for prop in bl_rna.properties:
|
||||
identifier = getattr(prop, "identifier", None)
|
||||
if identifier:
|
||||
names.add(identifier)
|
||||
except TypeError:
|
||||
pass
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
names.discard("rna_type")
|
||||
return names
|
||||
|
||||
def execute(self, context):
|
||||
props = getattr(context.scene, "text_texture_props", None)
|
||||
if not props:
|
||||
self.report({'ERROR'}, "Text Texture properties not found")
|
||||
return {'CANCELLED'}
|
||||
|
||||
property_names = self._collect_property_names(props)
|
||||
|
||||
for name in property_names:
|
||||
attr = getattr(props, name, None)
|
||||
if hasattr(attr, "clear") and callable(attr.clear):
|
||||
try:
|
||||
attr.clear()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
props.property_unset(name)
|
||||
except AttributeError:
|
||||
continue
|
||||
except TypeError:
|
||||
continue
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
self.report({'INFO'}, "Text Texture settings reset to defaults")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
# Export all operator classes for wildcard imports
|
||||
__all__ = [
|
||||
'TEXT_TEXTURE_OT_generate',
|
||||
'TEXT_TEXTURE_OT_preview',
|
||||
'TEXT_TEXTURE_OT_generate_shader',
|
||||
'TEXT_TEXTURE_OT_generate_plane',
|
||||
'TEXT_TEXTURE_OT_reset_settings',
|
||||
]
|
||||
|
||||
Binary file not shown.
@@ -247,6 +247,14 @@ def draw_generate_button(layout):
|
||||
row.scale_y = 1.5
|
||||
row.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA')
|
||||
|
||||
def draw_reset_button(layout):
|
||||
pass
|
||||
"""Draw button to reset settings to defaults"""
|
||||
row = layout.row()
|
||||
row.alignment = 'RIGHT'
|
||||
row.scale_y = 1.0
|
||||
row.operator("text_texture.reset_settings", text="Reset Settings", icon='FILE_REFRESH')
|
||||
|
||||
def _iter_components_by_placement(props, placement_mode):
|
||||
"""Yield (index, component) pairs filtered by placement mode."""
|
||||
try:
|
||||
@@ -660,6 +668,10 @@ def draw_shader_section(layout, props):
|
||||
row.scale_y = 1.5
|
||||
row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL')
|
||||
|
||||
plane_row = layout.row()
|
||||
plane_row.scale_y = 1.3
|
||||
plane_row.operator("text_texture.generate_plane", text="Create Plane", icon='MESH_GRID')
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Clean minimalist controls
|
||||
@@ -954,6 +966,15 @@ class TEXT_TEXTURE_PT_panel(Panel):
|
||||
lock_row = create_shader_row.row()
|
||||
lock_row.enabled = False
|
||||
lock_row.operator("text_texture.generate_shader", text="🔒 Create Shader (Pro Only)", icon='LOCKED')
|
||||
|
||||
plane_row = layout.row()
|
||||
plane_row.scale_y = 1.3
|
||||
if has_shader_generation():
|
||||
plane_row.operator("text_texture.generate_plane", text="Create Plane", icon='MESH_GRID')
|
||||
else:
|
||||
plane_lock_row = plane_row.row()
|
||||
plane_lock_row.enabled = False
|
||||
plane_lock_row.label(text="🔒 Create Plane (Pro Only)", icon='LOCKED')
|
||||
|
||||
# Preview Button - ALWAYS available (core functionality for all users)
|
||||
layout.separator(factor=0.5)
|
||||
@@ -961,7 +982,10 @@ class TEXT_TEXTURE_PT_panel(Panel):
|
||||
preview_row.scale_y = 1.5
|
||||
preview_row.operator("text_texture.preview", text="Preview Texture", icon='HIDE_OFF')
|
||||
|
||||
layout.separator(factor=1.2)
|
||||
layout.separator(factor=0.6)
|
||||
draw_reset_button(layout)
|
||||
|
||||
layout.separator(factor=1.0)
|
||||
|
||||
# COLLAPSIBLE SECTIONS - FREE FEATURES FIRST
|
||||
|
||||
@@ -1157,6 +1181,15 @@ class TEXT_TEXTURE_PT_panel_3d(Panel):
|
||||
lock_row = create_shader_row.row()
|
||||
lock_row.enabled = False
|
||||
lock_row.operator("text_texture.generate_shader", text="🔒 Create Shader (Pro Only)", icon='LOCKED')
|
||||
|
||||
plane_row = layout.row()
|
||||
plane_row.scale_y = 1.3
|
||||
if has_shader_generation():
|
||||
plane_row.operator("text_texture.generate_plane", text="Create Plane", icon='MESH_GRID')
|
||||
else:
|
||||
plane_lock_row = plane_row.row()
|
||||
plane_lock_row.enabled = False
|
||||
plane_lock_row.label(text="🔒 Create Plane (Pro Only)", icon='LOCKED')
|
||||
|
||||
# Preview Button - ALWAYS available (core functionality for all users)
|
||||
layout.separator(factor=0.5)
|
||||
@@ -1164,6 +1197,9 @@ class TEXT_TEXTURE_PT_panel_3d(Panel):
|
||||
preview_row.scale_y = 1.5
|
||||
preview_row.operator("text_texture.preview", text="Preview Texture", icon='HIDE_OFF')
|
||||
|
||||
layout.separator(factor=0.6)
|
||||
draw_reset_button(layout)
|
||||
|
||||
layout.separator(factor=1.2)
|
||||
|
||||
# COLLAPSIBLE SECTIONS - FREE FEATURES FIRST
|
||||
@@ -1360,6 +1396,15 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
|
||||
lock_row = create_shader_row.row()
|
||||
lock_row.enabled = False
|
||||
lock_row.operator("text_texture.generate_shader", text="🔒 Create Shader (Pro Only)", icon='LOCKED')
|
||||
|
||||
plane_row = layout.row()
|
||||
plane_row.scale_y = 1.3
|
||||
if has_shader_generation():
|
||||
plane_row.operator("text_texture.generate_plane", text="Create Plane", icon='MESH_GRID')
|
||||
else:
|
||||
plane_lock_row = plane_row.row()
|
||||
plane_lock_row.enabled = False
|
||||
plane_lock_row.label(text="🔒 Create Plane (Pro Only)", icon='LOCKED')
|
||||
|
||||
# Preview Button - ALWAYS available (core functionality for all users)
|
||||
layout.separator(factor=0.5)
|
||||
@@ -1367,6 +1412,9 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
|
||||
preview_row.scale_y = 1.5
|
||||
preview_row.operator("text_texture.preview", text="Preview Texture", icon='HIDE_OFF')
|
||||
|
||||
layout.separator(factor=0.6)
|
||||
draw_reset_button(layout)
|
||||
|
||||
layout.separator(factor=1.2)
|
||||
|
||||
# COLLAPSIBLE SECTIONS - FREE FEATURES FIRST
|
||||
|
||||
Binary file not shown.
@@ -3,12 +3,12 @@ Constants and metadata for the Text Texture Generator addon.
|
||||
"""
|
||||
|
||||
# Version information
|
||||
VERSION_TYPE = "free" # "free" or "full"
|
||||
VERSION_TYPE = "full" # "free" or "full"
|
||||
|
||||
bl_info = {
|
||||
"name": "Text Texture Generator" + (" Free" if VERSION_TYPE == "free" else " Full"),
|
||||
"author": "Marc Mintel <marc@mintel.me>",
|
||||
"version": (1, 0, 0),
|
||||
"version": (1, 1, 0),
|
||||
"blender": (4, 0, 0),
|
||||
"location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab",
|
||||
"description": "Generate image textures from text with accurate dimensions and instant live preview" + (" - Free Version (Limited Features)" if VERSION_TYPE == "free" else " - Full Version"),
|
||||
@@ -28,7 +28,7 @@ def get_version_limits():
|
||||
"""Get version-specific limits."""
|
||||
if VERSION_TYPE == "free":
|
||||
return {
|
||||
"max_concurrent_operations": 1
|
||||
"max_concurrent_operations": 4
|
||||
}
|
||||
else:
|
||||
return {
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
from unittest.mock import Mock, patch, MagicMock, call
|
||||
import pytest
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.operators.generation_ops import (
|
||||
TEXT_TEXTURE_OT_generate,
|
||||
TEXT_TEXTURE_OT_preview,
|
||||
TEXT_TEXTURE_OT_generate_shader
|
||||
TEXT_TEXTURE_OT_generate_shader,
|
||||
TEXT_TEXTURE_OT_generate_plane,
|
||||
TEXT_TEXTURE_OT_reset_settings,
|
||||
_sanitize_text_for_name,
|
||||
_resolve_material_name,
|
||||
)
|
||||
|
||||
|
||||
@@ -41,6 +47,35 @@ def create_mock_props(text="test", width=512, height=512, **kwargs):
|
||||
return props
|
||||
|
||||
|
||||
class DummyModifiers:
|
||||
"""Simple stand-in for Blender's modifier collection."""
|
||||
|
||||
def __init__(self):
|
||||
self.created = []
|
||||
|
||||
def new(self, name, mod_type=None, **kwargs):
|
||||
mod_type = mod_type or kwargs.get('type')
|
||||
modifier = SimpleNamespace(
|
||||
name=name,
|
||||
type=mod_type,
|
||||
subdivision_type='CATMULL_CLARK',
|
||||
levels=0,
|
||||
render_levels=0,
|
||||
)
|
||||
self.created.append(modifier)
|
||||
return modifier
|
||||
|
||||
|
||||
class DummyCollection:
|
||||
"""Simple collection helper with clear tracking."""
|
||||
|
||||
def __init__(self):
|
||||
self.cleared = False
|
||||
|
||||
def clear(self):
|
||||
self.cleared = True
|
||||
|
||||
|
||||
def create_mock_context(props=None, **kwargs):
|
||||
"""Create mock Blender context with scene properties.
|
||||
|
||||
@@ -533,4 +568,115 @@ class TestGenerateShaderOperator:
|
||||
invalid_context.scene = None
|
||||
assert TEXT_TEXTURE_OT_generate_shader.poll(invalid_context) is False
|
||||
else:
|
||||
pytest.skip("Operator does not implement poll method")
|
||||
pytest.skip("Operator does not implement poll method")
|
||||
|
||||
|
||||
class TestGenerationHelpers:
|
||||
"""Unit tests for helper utilities in generation_ops."""
|
||||
|
||||
def test_sanitize_text_removes_invalid_characters(self):
|
||||
assert _sanitize_text_for_name("Hello @World!!!") == "Hello_World"
|
||||
|
||||
def test_sanitize_text_truncates_and_defaults(self):
|
||||
assert _sanitize_text_for_name(" " * 5 + "--") == "Texture"
|
||||
assert len(_sanitize_text_for_name("A" * 50)) == 20
|
||||
|
||||
def test_resolve_material_uses_custom_name(self):
|
||||
props = create_mock_props()
|
||||
props.custom_material_name = " Custom Mat "
|
||||
assert _resolve_material_name(props, "Ignored") == "Custom Mat"
|
||||
|
||||
def test_resolve_material_defaults_to_sanitized(self):
|
||||
props = create_mock_props()
|
||||
props.custom_material_name = ""
|
||||
assert _resolve_material_name(props, "Sample") == "TextTexture_Mat_Sample"
|
||||
|
||||
|
||||
class TestGeneratePlaneOperator:
|
||||
"""Tests for the Create Plane operator."""
|
||||
|
||||
def test_generate_plane_success(self):
|
||||
op = TEXT_TEXTURE_OT_generate_plane()
|
||||
op.report = Mock()
|
||||
|
||||
props = create_mock_props(text="Plane Test", width=800, height=400)
|
||||
context = create_mock_context(props)
|
||||
|
||||
plane = SimpleNamespace(
|
||||
type='MESH',
|
||||
name='Plane',
|
||||
scale=SimpleNamespace(x=1.0, y=1.0, z=1.0),
|
||||
modifiers=DummyModifiers(),
|
||||
data=SimpleNamespace(materials=[]),
|
||||
)
|
||||
context.active_object = plane
|
||||
|
||||
mat = SimpleNamespace(name="TextTexture_Mat_Plane_Test")
|
||||
|
||||
with patch('src.operators.generation_ops.bpy') as mock_bpy:
|
||||
mock_bpy.ops.text_texture.generate_shader.return_value = {'FINISHED'}
|
||||
mock_bpy.ops.mesh.primitive_plane_add.return_value = None
|
||||
mock_bpy.data.materials.get.return_value = mat
|
||||
|
||||
result = op.execute(context)
|
||||
|
||||
assert result == {'FINISHED'}
|
||||
assert pytest.approx(plane.scale.x, rel=1e-6) == 2.0
|
||||
assert pytest.approx(plane.scale.y, rel=1e-6) == 1.0
|
||||
assert plane.modifiers.created[0].subdivision_type == 'SIMPLE'
|
||||
assert plane.modifiers.created[0].levels == 1
|
||||
assert plane.data.materials[0] is mat
|
||||
|
||||
def test_generate_plane_handles_shader_failure(self):
|
||||
op = TEXT_TEXTURE_OT_generate_plane()
|
||||
op.report = Mock()
|
||||
|
||||
props = create_mock_props()
|
||||
context = create_mock_context(props)
|
||||
|
||||
with patch('src.operators.generation_ops.bpy') as mock_bpy:
|
||||
mock_bpy.ops.text_texture.generate_shader.return_value = {'CANCELLED'}
|
||||
|
||||
result = op.execute(context)
|
||||
|
||||
assert result == {'CANCELLED'}
|
||||
op.report.assert_called()
|
||||
|
||||
|
||||
class TestResetSettingsOperator:
|
||||
"""Tests for the reset settings operator."""
|
||||
|
||||
class DummyProps:
|
||||
__annotations__ = {
|
||||
'text': None,
|
||||
'image_overlays': None,
|
||||
'preset_search_query': None,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.text = "Custom"
|
||||
self.image_overlays = DummyCollection()
|
||||
self.preset_search_query = "search"
|
||||
self.unset_called = []
|
||||
self.bl_rna = SimpleNamespace(properties=[
|
||||
SimpleNamespace(identifier='text'),
|
||||
SimpleNamespace(identifier='image_overlays'),
|
||||
SimpleNamespace(identifier='preset_search_query'),
|
||||
])
|
||||
|
||||
def property_unset(self, name):
|
||||
self.unset_called.append(name)
|
||||
|
||||
def test_reset_settings_unsets_all_known_properties(self):
|
||||
op = TEXT_TEXTURE_OT_reset_settings()
|
||||
op.report = Mock()
|
||||
|
||||
props = self.DummyProps()
|
||||
context = create_mock_context(props)
|
||||
|
||||
result = op.execute(context)
|
||||
|
||||
assert result == {'FINISHED'}
|
||||
assert set(props.unset_called) >= {'text', 'image_overlays', 'preset_search_query'}
|
||||
assert props.image_overlays.cleared is True
|
||||
op.report.assert_called()
|
||||
|
||||
Reference in New Issue
Block a user