5 Commits

Author SHA1 Message Date
f8dc03caf9 1.1.0 2025-10-14 12:23:58 +02:00
cf4f3ce564 texts 2025-10-14 10:18:34 +02:00
2f5076b72b 1.0.0 publish 2025-10-13 22:42:55 +02:00
fe2e81ecd0 bundle svg 2025-10-13 19:08:28 +02:00
e3a8f0bcf0 docs 2025-10-13 18:11:12 +02:00
61 changed files with 649 additions and 96 deletions

5
.gitignore vendored
View File

@@ -31,10 +31,11 @@ htmlcov/
# Build artifacts # Build artifacts
build/ build/
dist/
*.egg-info/ *.egg-info/
*.egg *.egg
*.whl *.whl
!src/wheels/
!src/wheels/**/*.whl
# IDE and Editor files # IDE and Editor files
.vscode/ .vscode/
@@ -77,4 +78,4 @@ marketing/*.psd
*.temp *.temp
*.bak *.bak
*.swp *.swp
*~.nib *~.nib

View File

@@ -225,13 +225,10 @@ The addon features intelligent font detection and validation:
## Dependencies ## Dependencies
The addon automatically installs required dependencies: The addon bundles and automatically installs its dependencies when needed:
- **Pillow** (>=10.0.0): For image generation and text rendering - **Pillow** (>=10.0.0): Image generation and text rendering.
- **CairoSVG** (>=2.7.0): Rasterizes SVG overlays using the bundled wheel set for macOS and Linux (installs on first SVG use).
Optional dependency for vector overlays:
- **CairoSVG** (>=2.7.0): Required to rasterize SVG overlays. Install it inside Blender's Python environment with `pip install cairosvg`.
## Troubleshooting ## Troubleshooting
@@ -414,6 +411,10 @@ Marc Mintel <marc@mintel.me>
- Import/export functionality for preset backup and cross-installation sharing - Import/export functionality for preset backup and cross-installation sharing
- Enhanced error handling and user feedback throughout the interface - 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) ### 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. **New Build Strategy**: Redesigned version distribution from runtime feature flags to build-time file exclusion for better security and genuine feature separation.

70
USER_GUIDE.md Normal file
View File

@@ -0,0 +1,70 @@
# Text Texture Generator Quick User Guide
This guide keeps things short so you can start creating textures right away.
---
## What It Does
- Adds a **Text Texture** panel to Blender (3D Viewport, Shader Editor, Properties).
- Lets you type text, pick fonts, set colors, and position everything directly in Blender.
- Generates image textures (and optional normal maps) that are packed into your `.blend` file.
---
## Requirements
- Blender 4.0 or newer (Windows, macOS, Linux).
- Pillow and CairoSVG install automatically with the addon _(SVG overlays work out of the box)._
---
## Installation
1. Open Blender → `Edit > Preferences > Add-ons`.
2. Click **Install...**, choose the downloaded `.zip` (or extracted folder with `__init__.py`).
3. Enable **Text Texture Generator** in the add-on list.
4. Save preferences if you want it active next time.
---
## First Run Checklist
1. Select an object with a material (or create a new one).
2. Open the **Text Texture** panel (press `N` in the 3D Viewport or Shader Editor and look for the tab).
3. Type your text in the **Main Text** field.
4. Choose a font: use the system list or toggle **Use Custom Font** and browse to a `.ttf`/`.otf`.
5. Set the texture size (Width & Height) and background (transparent or solid color).
6. Click **Preview Texture** to see a quick render.
7. Click **Generate Texture** when youre happy. The image is created, packed, and ready in the Image Editor.
---
## Common Options (Plain English)
- **Prepend / Append Text**: Add lines before or after your main message (good for labels or subtitles).
- **Layout**: `Horizontal` keeps everything in one line; `Vertical` stacks each section.
- **Anchor Grid**: Choose where the text starts (center, top left, etc.). Offset sliders fine-tune the position.
- **Manual Position Mode**: Switch to percentage or pixel input if you need exact placement.
- **Preview Background**: Checker, solid color, or gradient so you can see how the text will look.
- **Save Presets**: Store your favorite settings either with the `.blend` file or in Blenders config folder so you can reuse them later.
- **Normal Map (Pro)**: Flip on **Generate Normal Map** to add depth—strength, blur, and invert control the look.
---
## Typical Workflow
1. Enter and style your text.
2. Use the preview to check spacing and colors.
3. Generate the final texture.
4. In the Shader Editor, click **Create Shader** to build a material with the new texture (and normal map if enabled).
5. Apply the material to your object. Done.
---
## Tips
- Need multiple styles fast? Duplicate the blend file or save presets before you start experimenting.
- Keep the preview size smaller while fine-tuning; switch to full resolution for final renders.
- SVG logos? Add them as overlay components—the addon bundles CairoSVG and rasterizes them automatically.
- For team projects, export presets (`Add-on Panel > Presets > Export`) so everyone can share the same look.

Binary file not shown.

Binary file not shown.

BIN
dist/text_texture_generator_v1.1.0.zip vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -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. - **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. - **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. - **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 textures 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 Blenders config folder, refresh them in one click, and import/export JSON bundles for backups or team sharing. - **Preset ecosystem.** Save styles either with the current `.blend` or in Blenders 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. - **Adaptive text fitting.** Let the addon resize fonts within a range to keep layouts within your chosen canvas margins.

View File

@@ -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. - **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. - **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. - **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 Blenders config folder, refresh them, and import/export JSON bundles for backups or team sharing. - **Preset workflow.** Save styles to the current `.blend` or Blenders 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. - **Adaptive fitting.** Let the addon resize fonts within a min/max range to keep text safely within your canvas margins.
@@ -28,6 +29,6 @@ Create polished text textures without leaving Blender. This addon drops a dedica
## Requirements ## Requirements
- Blender 4.0 or newer (Windows, macOS, or Linux). - Blender 4.0 or newer (Windows, macOS, or Linux).
- Pillow installs automatically; CairoSVG is only needed for SVG overlays. - Pillow and CairoSVG install automatically—SVG overlays are ready out of the box.
Questions or feature requests? Message us on Gumroad or email `marc@mintel.me`—were happy to help. Questions or feature requests? Message us on Gumroad or email `marc@mintel.me`—were happy to help.

View File

@@ -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. - 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. - 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. - 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 Blenders config folder, plus import/export for teams and backups. - Dual-location presets that save to the current `.blend` or Blenders config folder, plus import/export for teams and backups.
- Adaptive text fitting to keep designs inside custom canvas margins without manual tweaking. - Adaptive text fitting to keep designs inside custom canvas margins without manual tweaking.
@@ -29,7 +30,7 @@ Bring typography directly into your Blender workflow. The Text Texture Generator
## Requirements & Setup ## Requirements & Setup
- Blender 4.0 or newer on Windows, macOS, or Linux. - Blender 4.0 or newer on Windows, macOS, or Linux.
- Pillow installs automatically; install CairoSVG only if you plan to use SVG overlays. - Pillow and CairoSVG install automatically, so SVG overlays work immediately.
- Installation follows the standard “Install Add-on from File…” flow and includes demo `.blend` scenes to explore the toolset immediately. - Installation follows the standard “Install Add-on from File…” flow and includes demo `.blend` scenes to explore the toolset immediately.
Need a production license, classroom seats, or have a technical question? Send a DM on Superhive or drop an email to `marc@mintel.me`—we usually reply within one business day. Need a production license, classroom seats, or have a technical question? Send a DM on Superhive or drop an email to `marc@mintel.me`—we usually reply within one business day.

Binary file not shown.

Binary file not shown.

BIN
marketing/promo.mov Normal file

Binary file not shown.

BIN
marketing/showcase.blend Normal file

Binary file not shown.

View File

@@ -1,5 +1,5 @@
If youve ever jumped between Blender and a design app just to create a simple label, you already understand why I made Text Texture Generator. Its a panel inside Blender that lets me type the copy, pick a font, adjust placement, and see the result update instantly—no exporting, no importing, no detached workflow. I built Text Texture Generator because I needed faster product labels in Blender. Updating a name or certification meant exporting an image, importing it again, and rebuilding the shader every single time. Now I stay inside the file: type the new wording, pick a font, hit generate, and the texture is already on the model.
These days I open the addon, try a few layout ideas, and preview everything in the Image Editor while I stay focused on the scene. When I click generate, the texture appears right in `bpy.data.images`, packed and ready for shading. That small loop keeps me in the creative zone, because iteration feels effortless instead of procedural. The addon packs the image, wires the material, and lets me move on. Nothing fancy—just less time fighting assets.
If you want that same sense of flow, give the addon a spin, type your next headline inside Blender, and watch how fast it becomes part of your scene. If youre doing the same label shuffle, try it on your next round and see how much quieter the workflow feels.

View File

@@ -1,5 +1,5 @@
I built Text Texture Generator so long-form text work would finally feel natural inside Blender. Instead of juggling exported PNGs, I type my headlines, notes, and captions in one panel, switch layouts with a dropdown, and nudge the anchor controls until everything sits exactly where I want it. The live preview shows the result immediately, which makes experimenting with hierarchy and spacing feel like second nature. Text Texture Generator exists because I was drowning in product labels. Ingredient lists, recycling icons, certification text—it never ends. Instead of bouncing to a graphics program, I type the update directly in Blender, check the preview, and generate a fresh texture on the spot.
Because the textures are generated and packed inside the same blend file, I can reopen a project days later and keep iterating without wondering which folder held the latest assets. I clone a setup, change the copy, and stay focused on lighting and animation instead of file management. Everything stays in the .blend, so when I reopen a project Im not digging through folders to see which version is right. Duplicate, tweak the wording, done.
If that sounds like the workflow youve been looking for, open the panel on your next scene and let the addon handle the heavy lifting while you focus on the story. If youre tired of the export/import routine for labels, run the addon on your next revision cycle. It gave me my time back.

View File

@@ -1,5 +1,5 @@
Text Texture Generator is the tool I rely on when clients want to see revisions live. I share my Blender viewport, open the panel, and start adjusting text while everyone watches. We try new phrases, test alternate fonts, tweak spacing, and the preview keeps pace with every choice. It turns feedback into collaboration because the scene evolves in real time. Review calls used to fall apart as soon as someone caught a typo on the packaging. Id leave Blender, patch the text elsewhere, re-import the image, rebuild the shader, and only then could we continue. With Text Texture Generator I fix the wording right there in Blender. Two seconds later the new label is visible, and we keep talking about the design instead of waiting on files.
For animated shots, staying inside Blender matters even more. I can add a subtitle, position it with the anchor controls, and jump straight into keyframing without breaking rhythm. I never pause to export or import assets; the addon takes care of generating the texture and packing it into the file. The addon doesnt do anything magical—it just keeps the update process inside the scene. For me, thats the difference between a smooth meeting and an awkward pause.
If you want to keep your sessions fluid, bring the panel into your next review call and see how quickly it helps you iterate without leaving your creative flow. If youre handling the same last-minute edits, keep the panel open during your next review. It helps.

View File

@@ -1,5 +1,5 @@
Introducing people to Text Texture Generator is one of my favorite workshop moments. I start by showing that its simply a Blender panel: type a line of text, choose a font, reposition it with intuitive controls, and watch the preview update instantly. It demystifies typography inside 3D scenes because everything happens where you already feel comfortable. Every product mockup I build needs text—names, claims, instructions, compliance notes. Rebuilding textures for each update was a mess, so I wrote Text Texture Generator to keep it all inside Blender. I type the copy, adjust the size, press generate, and the label lands on the model with a material already hooked up.
Once everyone sees the basics, we layer in playful touches—maybe a supporting graphic, maybe a translucent accent—and the addon keeps it all non-destructive. Students revisit their blend files later and find the exact settings ready to tweak, which means their learning doesnt disappear when the session ends. Its not glamorous, but it saves hours. I can stay focused on the design instead of managing image exports and shader nodes.
If youre teaching or mentoring, add this addon to your toolkit. Its an easy way to show that text can be as expressive and accessible as any other part of Blender. If your scenes are full of labels too, give it a shot on your next mockup. Thats exactly why I made it.

View File

@@ -1,5 +1,5 @@
Text Texture Generator has become my sketchbook for typography inside Blender. Whenever I dial in a look that feels right, I save it as a preset so I can return to it later. Because the addon stores those presets with the blend file, I can hand a project to a teammate and they immediately see the same options I used. Once a label design is approved, I save it as a preset in Text Texture Generator. The next time we need the same badge or panel, its already in the scene. I change the text, regenerate, and move on—no rebuilding textures or hunting through folders.
That shared context makes collaboration feel easy. We swap variations, leave notes directly in the panel, and keep everything embedded in the scene. The addon handles texture generation and packing, so we never lose track of assets or guess which version is current. Sharing those presets with collaborators keeps us on the same page. They open the blend file, see the same options, and reuse them without extra instructions.
If youre building a collection of reusable styles, let the addon capture them for you. It keeps your favorite ideas close at hand and ready for the next project. If youre repeating the same labels, let the addon hold onto them. It keeps the process predictable.

View File

@@ -201,7 +201,46 @@ def should_exclude_file(file_path: Path, source_dir: Path, exclude_patterns: Lis
return False return False
def copy_source_files(source_dir: Path, dest_dir: Path, exclude_patterns: List[str], exclude_files: List[str] = None, minify: bool = True): def _format_version_tuple(version_tuple: tuple) -> str:
"""Format version tuple for insertion into bl_info."""
return "(" + ", ".join(str(part) for part in version_tuple) + ")"
def _edition_version_tuple(version: str, version_type: str) -> 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)
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)"
target_version_tuple = _edition_version_tuple(version, version_type)
source_code = re.sub(
r'"name":\s*"Text Texture Generator[^"]*"',
f'"name": "{edition_name}"',
source_code,
count=1
)
source_code = re.sub(
r'"version":\s*\([^\)]+\)',
f'"version": {_format_version_tuple(target_version_tuple)}',
source_code,
count=1
)
return source_code
def copy_source_files(source_dir: Path, dest_dir: Path, exclude_patterns: List[str], exclude_files: List[str] = None, minify: bool = True, version_type: str = "free", version: str = "1.0.0"):
"""Copy source files to destination, excluding specified patterns and files.""" """Copy source files to destination, excluding specified patterns and files."""
if exclude_files is None: if exclude_files is None:
exclude_files = [] exclude_files = []
@@ -232,17 +271,25 @@ def copy_source_files(source_dir: Path, dest_dir: Path, exclude_patterns: List[s
# Read, minify, and write Python files # Read, minify, and write Python files
try: try:
source_code = item.read_text(encoding='utf-8') source_code = item.read_text(encoding='utf-8')
source_code = _apply_edition_overrides(rel_path, source_code, version_type, version)
minified_code = minify_python_code(source_code) minified_code = minify_python_code(source_code)
dest_file.write_text(minified_code, encoding='utf-8') dest_file.write_text(minified_code, encoding='utf-8')
print(f" Copied (minified): {rel_path}") print(f" Copied (minified): {rel_path}")
except Exception as e: except Exception as e:
# If minification fails, fall back to regular copy # If minification fails, fall back to regular copy
print(f" Warning: Could not minify {rel_path}, copying as-is: {e}") print(f" Warning: Could not minify {rel_path}, copying as-is: {e}")
shutil.copy2(item, dest_file) source_code = item.read_text(encoding='utf-8')
source_code = _apply_edition_overrides(rel_path, source_code, version_type, version)
dest_file.write_text(source_code, encoding='utf-8')
print(f" Copied: {rel_path}") print(f" Copied: {rel_path}")
else: else:
# Copy non-Python files or when minification is disabled # Copy non-Python files or when minification is disabled
shutil.copy2(item, dest_file) if item.suffix == '.py':
source_code = item.read_text(encoding='utf-8')
source_code = _apply_edition_overrides(rel_path, source_code, version_type, version)
dest_file.write_text(source_code, encoding='utf-8')
else:
shutil.copy2(item, dest_file)
print(f" Copied: {rel_path}") print(f" Copied: {rel_path}")
copied_count += 1 copied_count += 1
@@ -360,7 +407,7 @@ def build_addon(version_type: str, version: Optional[str] = None, output_dir: Op
# Copy source files to temp directory with version-specific exclusions # Copy source files to temp directory with version-specific exclusions
build_temp_dir = temp_dir / f"{config['project']['id']}_{version_type}" build_temp_dir = temp_dir / f"{config['project']['id']}_{version_type}"
copy_source_files(source_dir, build_temp_dir, exclude_patterns, exclude_files, minify) copy_source_files(source_dir, build_temp_dir, exclude_patterns, exclude_files, minify, version_type=version_type, version=version)
# Create output filename # Create output filename
version_suffix = f"_{version_type}" if version_type == "free" else "" version_suffix = f"_{version_type}" if version_type == "free" else ""
@@ -389,14 +436,16 @@ def build_all_versions(version: Optional[str] = None, minify: bool = True):
print("\n" + "="*50) print("\n" + "="*50)
print("BUILDING FULL VERSION") print("BUILDING FULL VERSION")
print("="*50) 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)) built_packages.append(("full", full_package))
# Build free version # Build free version
print("\n" + "="*50) print("\n" + "="*50)
print("BUILDING FREE VERSION") print("BUILDING FREE VERSION")
print("="*50) 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)) built_packages.append(("free", free_package))
print("\n" + "="*50) print("\n" + "="*50)
@@ -406,6 +455,19 @@ def build_all_versions(version: Optional[str] = None, minify: bool = True):
file_size = package_path.stat().st_size / 1024 # KB file_size = package_path.stat().st_size / 1024 # KB
print(f"{version_type.upper():>4}: {package_path.name} ({file_size:.1f} 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 return built_packages
def main(): def main():

View File

@@ -2,9 +2,9 @@
# IMPORTANT: bl_info must contain ONLY literal values (no expressions/conditionals) # IMPORTANT: bl_info must contain ONLY literal values (no expressions/conditionals)
# for ast.literal_eval() to parse it during addon discovery # for ast.literal_eval() to parse it during addon discovery
bl_info = { bl_info = {
"name": "Text Texture Generator (Free)", "name": "Text Texture Generator Pro",
"author": "Marc Mintel <marc@mintel.me>", "author": "Marc Mintel <marc@mintel.me>",
"version": (1, 0, 0), "version": (1, 1, 0),
"blender": (4, 0, 0), "blender": (4, 0, 0),
"location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab", "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", "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_generate,
TEXT_TEXTURE_OT_preview, TEXT_TEXTURE_OT_preview,
TEXT_TEXTURE_OT_generate_shader, 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) # Try to import utility operators (might not be available in free version)
@@ -163,6 +165,8 @@ classes = (
TEXT_TEXTURE_OT_generate, TEXT_TEXTURE_OT_generate,
TEXT_TEXTURE_OT_preview, TEXT_TEXTURE_OT_preview,
TEXT_TEXTURE_OT_generate_shader, TEXT_TEXTURE_OT_generate_shader,
TEXT_TEXTURE_OT_generate_plane,
TEXT_TEXTURE_OT_reset_settings,
TEXT_TEXTURE_OT_refresh_preview, TEXT_TEXTURE_OT_refresh_preview,
TEXT_TEXTURE_OT_view_preview_zoom, TEXT_TEXTURE_OT_view_preview_zoom,
TEXT_TEXTURE_OT_add_overlay, TEXT_TEXTURE_OT_add_overlay,
@@ -323,4 +327,4 @@ def unregister():
del bpy.types.Scene.text_texture_props del bpy.types.Scene.text_texture_props
if __name__ == "__main__": if __name__ == "__main__":
register() register()

View File

@@ -6,6 +6,8 @@ Core functionality for generating texture images with text overlays.
import math import math
import bpy import bpy
from array import array
from ..utils.constants import has_text_fitting from ..utils.constants import has_text_fitting
from ..utils.overlay_assets import ( from ..utils.overlay_assets import (
SVGConversionError, SVGConversionError,
@@ -662,13 +664,13 @@ def generate_texture_image(props, width, height):
# Convert PIL image to Blender format (flip vertically to fix mirroring) # Convert PIL image to Blender format (flip vertically to fix mirroring)
pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM) # Fix mirroring issue pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM) # Fix mirroring issue
pil_pixels = list(pil_img.getdata()) raw_bytes = pil_img.tobytes()
blender_pixels = [] normalized = array('f', (channel / 255.0 for channel in raw_bytes))
for r, g, b, a in pil_pixels: pixels_attr = getattr(blender_img, "pixels", None)
pass if hasattr(pixels_attr, "foreach_set"):
blender_pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0]) pixels_attr.foreach_set(normalized)
else:
blender_img.pixels[:] = blender_pixels blender_img.pixels[:] = normalized.tolist()
return blender_img, None return blender_img, None
except ImportError as e: except ImportError as e:
pass pass

View File

@@ -13,6 +13,8 @@ from .generation_ops import (
TEXT_TEXTURE_OT_generate, TEXT_TEXTURE_OT_generate,
TEXT_TEXTURE_OT_preview, TEXT_TEXTURE_OT_preview,
TEXT_TEXTURE_OT_generate_shader, 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 # Conditional imports for modules that might not exist in free version
@@ -46,22 +48,34 @@ except ImportError:
# preset_ops module not available (free version) # preset_ops module not available (free version)
PRESET_OPERATORS = [] PRESET_OPERATORS = []
if is_full_version(): try:
try: pass
pass from .text_editor_ops import *
from .text_editor_ops import * from .text_editor_ops import (
from .text_editor_ops import ( TEXT_TEXTURE_OT_edit_text,
TEXT_TEXTURE_OT_edit_text, )
) TEXT_EDITOR_OPERATORS = [
TEXT_EDITOR_OPERATORS = [ 'TEXT_TEXTURE_OT_edit_text',
'TEXT_TEXTURE_OT_edit_text', ]
] except ImportError as e:
except ImportError as e: import sys
import sys print(f"WARNING: text_editor_ops import failed in operators/__init__.py: {e}", file=sys.stderr)
print(f"WARNING: text_editor_ops import failed in operators/__init__.py: {e}", file=sys.stderr)
TEXT_EDITOR_OPERATORS = [] class TEXT_TEXTURE_OT_edit_text:
else: """Fallback stub when text editor operator is unavailable."""
TEXT_EDITOR_OPERATORS = [] bl_idname = "text_texture.edit_text"
bl_label = "Edit Text (Unavailable)"
bl_description = "Text editor feature unavailable in this build"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return False
def execute(self, context):
raise RuntimeError("Text editor feature is unavailable in this build.")
TEXT_EDITOR_OPERATORS = ['TEXT_TEXTURE_OT_edit_text']
try: try:
pass pass
@@ -102,6 +116,8 @@ __all__.extend([
'TEXT_TEXTURE_OT_generate', 'TEXT_TEXTURE_OT_generate',
'TEXT_TEXTURE_OT_preview', 'TEXT_TEXTURE_OT_preview',
'TEXT_TEXTURE_OT_generate_shader', 'TEXT_TEXTURE_OT_generate_shader',
'TEXT_TEXTURE_OT_generate_plane',
'TEXT_TEXTURE_OT_reset_settings',
]) ])
# Add preset operators if available # Add preset operators if available

View File

@@ -7,11 +7,26 @@ import bpy
from bpy.types import Operator from bpy.types import Operator
import os import os
import json import json
import re
from math import isfinite
# Import required functions from other modules # Import required functions from other modules
from ..core.generation_engine import generate_texture_image, generate_preview 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): class TEXT_TEXTURE_OT_generate(Operator):
pass pass
"""Generate final texture and pack it""" """Generate final texture and pack it"""
@@ -56,15 +71,7 @@ class TEXT_TEXTURE_OT_generate(Operator):
return {'CANCELLED'} return {'CANCELLED'}
# Create diffuse texture with proper sanitization # Create diffuse texture with proper sanitization
import re sanitized_text = _sanitize_text_for_name(props.text)
# 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"
img_name = f"TextTexture_{sanitized_text}" img_name = f"TextTexture_{sanitized_text}"
# Remove existing diffuse texture # Remove existing diffuse texture
@@ -162,8 +169,8 @@ class TEXT_TEXTURE_OT_preview(Operator):
# Get properties and generate preview with correct parameters # Get properties and generate preview with correct parameters
props = context.scene.text_texture_props props = context.scene.text_texture_props
preview_size = props.preview_size if hasattr(props, 'preview_size') else 512 preview_size = props.preview_size if hasattr(props, 'preview_size') else 512
preview_width = getattr(props, 'texture_width', preview_size) or preview_size preview_width = preview_size
preview_height = getattr(props, 'texture_height', preview_size) or preview_size preview_height = preview_size
result = generate_preview(props, preview_width, preview_height) result = generate_preview(props, preview_width, preview_height)
if result: if result:
@@ -278,15 +285,7 @@ class TEXT_TEXTURE_OT_generate_shader(Operator):
# Create diffuse texture name with proper sanitization # Create diffuse texture name with proper sanitization
import re sanitized_text = _sanitize_text_for_name(props.text)
# 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"
img_name = f"TextTexture_{sanitized_text}" img_name = f"TextTexture_{sanitized_text}"
# Remove existing diffuse texture if it exists # Remove existing diffuse texture if it exists
@@ -330,13 +329,7 @@ class TEXT_TEXTURE_OT_generate_shader(Operator):
pass pass
# Create or get material using custom name or fallback # Create or get material using custom name or fallback
if props.custom_material_name.strip(): material_name = _resolve_material_name(props, sanitized_text)
pass
material_name = props.custom_material_name.strip()
else:
pass
# Use the same sanitized text for consistency
material_name = f"TextTexture_Mat_{sanitized_text}"
if material_name in bpy.data.materials: if material_name in bpy.data.materials:
pass pass
@@ -743,9 +736,136 @@ class TEXT_TEXTURE_OT_generate_shader(Operator):
traceback.print_exc() traceback.print_exc()
return {'CANCELLED'} 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 # Export all operator classes for wildcard imports
__all__ = [ __all__ = [
'TEXT_TEXTURE_OT_generate', 'TEXT_TEXTURE_OT_generate',
'TEXT_TEXTURE_OT_preview', 'TEXT_TEXTURE_OT_preview',
'TEXT_TEXTURE_OT_generate_shader', 'TEXT_TEXTURE_OT_generate_shader',
'TEXT_TEXTURE_OT_generate_plane',
'TEXT_TEXTURE_OT_reset_settings',
] ]

View File

@@ -247,6 +247,14 @@ def draw_generate_button(layout):
row.scale_y = 1.5 row.scale_y = 1.5
row.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA') row.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA')
def draw_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): def _iter_components_by_placement(props, placement_mode):
"""Yield (index, component) pairs filtered by placement mode.""" """Yield (index, component) pairs filtered by placement mode."""
try: try:
@@ -660,6 +668,10 @@ def draw_shader_section(layout, props):
row.scale_y = 1.5 row.scale_y = 1.5
row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL') 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() layout.separator()
# Clean minimalist controls # Clean minimalist controls
@@ -954,6 +966,15 @@ class TEXT_TEXTURE_PT_panel(Panel):
lock_row = create_shader_row.row() lock_row = create_shader_row.row()
lock_row.enabled = False lock_row.enabled = False
lock_row.operator("text_texture.generate_shader", text="🔒 Create Shader (Pro Only)", icon='LOCKED') 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) # Preview Button - ALWAYS available (core functionality for all users)
layout.separator(factor=0.5) layout.separator(factor=0.5)
@@ -961,7 +982,10 @@ class TEXT_TEXTURE_PT_panel(Panel):
preview_row.scale_y = 1.5 preview_row.scale_y = 1.5
preview_row.operator("text_texture.preview", text="Preview Texture", icon='HIDE_OFF') 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 # COLLAPSIBLE SECTIONS - FREE FEATURES FIRST
@@ -1157,6 +1181,15 @@ class TEXT_TEXTURE_PT_panel_3d(Panel):
lock_row = create_shader_row.row() lock_row = create_shader_row.row()
lock_row.enabled = False lock_row.enabled = False
lock_row.operator("text_texture.generate_shader", text="🔒 Create Shader (Pro Only)", icon='LOCKED') 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) # Preview Button - ALWAYS available (core functionality for all users)
layout.separator(factor=0.5) layout.separator(factor=0.5)
@@ -1164,6 +1197,9 @@ class TEXT_TEXTURE_PT_panel_3d(Panel):
preview_row.scale_y = 1.5 preview_row.scale_y = 1.5
preview_row.operator("text_texture.preview", text="Preview Texture", icon='HIDE_OFF') 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) layout.separator(factor=1.2)
# COLLAPSIBLE SECTIONS - FREE FEATURES FIRST # COLLAPSIBLE SECTIONS - FREE FEATURES FIRST
@@ -1360,6 +1396,15 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
lock_row = create_shader_row.row() lock_row = create_shader_row.row()
lock_row.enabled = False lock_row.enabled = False
lock_row.operator("text_texture.generate_shader", text="🔒 Create Shader (Pro Only)", icon='LOCKED') 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) # Preview Button - ALWAYS available (core functionality for all users)
layout.separator(factor=0.5) layout.separator(factor=0.5)
@@ -1367,6 +1412,9 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
preview_row.scale_y = 1.5 preview_row.scale_y = 1.5
preview_row.operator("text_texture.preview", text="Preview Texture", icon='HIDE_OFF') 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) layout.separator(factor=1.2)
# COLLAPSIBLE SECTIONS - FREE FEATURES FIRST # COLLAPSIBLE SECTIONS - FREE FEATURES FIRST

View File

@@ -18,11 +18,18 @@ from .constants import (
is_free_version, is_free_version,
is_full_version is_full_version
) )
from .system import install_pillow, get_system_fonts, get_font_enum_items, find_default_truetype_font from .system import (
install_pillow,
install_cairosvg_from_bundle,
get_system_fonts,
get_font_enum_items,
find_default_truetype_font
)
__all__ = [ __all__ = [
'bl_info', 'bl_info',
'install_pillow', 'install_pillow',
'install_cairosvg_from_bundle',
'get_system_fonts', 'get_system_fonts',
'get_font_enum_items', 'get_font_enum_items',
'find_default_truetype_font', 'find_default_truetype_font',

View File

@@ -3,12 +3,12 @@ Constants and metadata for the Text Texture Generator addon.
""" """
# Version information # Version information
VERSION_TYPE = "free" # "free" or "full" VERSION_TYPE = "full" # "free" or "full"
bl_info = { bl_info = {
"name": "Text Texture Generator" + (" Free" if VERSION_TYPE == "free" else " Full"), "name": "Text Texture Generator" + (" Free" if VERSION_TYPE == "free" else " Full"),
"author": "Marc Mintel <marc@mintel.me>", "author": "Marc Mintel <marc@mintel.me>",
"version": (1, 0, 0), "version": (1, 1, 0),
"blender": (4, 0, 0), "blender": (4, 0, 0),
"location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab", "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"), "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.""" """Get version-specific limits."""
if VERSION_TYPE == "free": if VERSION_TYPE == "free":
return { return {
"max_concurrent_operations": 1 "max_concurrent_operations": 4
} }
else: else:
return { return {

View File

@@ -87,11 +87,16 @@ def ensure_svg_raster(image_path: str, scale: float) -> RasterizationResult:
try: try:
import cairosvg # type: ignore import cairosvg # type: ignore
except ImportError as import_error: except ImportError:
raise SVGConversionError( try:
"SVG overlay support requires the 'cairosvg' package. " from .system import install_cairosvg_from_bundle
"Install it via pip (pip install cairosvg) inside Blender's Python environment." install_cairosvg_from_bundle()
) from import_error import cairosvg # type: ignore # noqa: F401
except Exception as install_error:
raise SVGConversionError(
"SVG overlay support requires CairoSVG, but the bundled installation failed. "
"Please reinstall the addon or install CairoSVG manually inside Blender's Python environment."
) from install_error
# Guard against invalid scales (NaN, infinity, zero, negative) # Guard against invalid scales (NaN, infinity, zero, negative)
if not isinstance(scale, (int, float)) or not math.isfinite(scale): if not isinstance(scale, (int, float)) or not math.isfinite(scale):

View File

@@ -6,9 +6,77 @@ import subprocess
import sys import sys
import platform import platform
import os import os
from pathlib import Path
from typing import Iterable
_SUPPORTED_PYTHON_TAG = "cp311"
_BUNDLED_WHEEL_DIRS = {
"darwin": "cp311-macosx_10_9_universal2",
"linux": "cp311-manylinux_2_28_x86_64",
}
_BUNDLED_CAIROSVG_PACKAGES = (
"cairosvg==2.7.1",
"cairocffi==1.7.1",
"cssselect2==0.7.0",
"tinycss2==1.2.1",
"defusedxml==0.7.1",
"cffi==1.17.1",
"pycparser==2.21",
"webencodings==0.5.1",
)
_DEFAULT_FONT_CACHE = None _DEFAULT_FONT_CACHE = None
def _pip_install_from_bundle(packages: Iterable[str], bundle_dir: Path) -> None:
"""Install packages from a bundled wheel directory using pip."""
pass
command = [
sys.executable,
"-m",
"pip",
"install",
"--no-index",
"--no-cache-dir",
"--find-links",
str(bundle_dir),
*packages,
]
subprocess.check_call(command)
def _get_wheel_bundle_dir() -> Path:
"""Return the platform-specific wheel bundle directory for the current environment."""
pass
python_tag = f"cp{sys.version_info.major}{sys.version_info.minor}"
if python_tag != _SUPPORTED_PYTHON_TAG:
pass
raise RuntimeError(
f"Bundled dependencies target Python {_SUPPORTED_PYTHON_TAG}, "
f"but current interpreter reports {python_tag}."
)
platform_key = platform.system().lower()
bundle_name = _BUNDLED_WHEEL_DIRS.get(platform_key)
if not bundle_name:
pass
raise RuntimeError(f"No bundled wheel directory configured for platform '{platform_key}'.")
wheels_root = Path(__file__).resolve().parent.parent / "wheels"
bundle_dir = wheels_root / bundle_name
if not bundle_dir.exists():
pass
raise FileNotFoundError(f"Bundled wheel directory not found: {bundle_dir}")
return bundle_dir
def install_cairosvg_from_bundle() -> None:
"""Install CairoSVG and its dependencies using the bundled wheels."""
pass
bundle_dir = _get_wheel_bundle_dir()
_pip_install_from_bundle(_BUNDLED_CAIROSVG_PACKAGES, bundle_dir)
def install_pillow(): def install_pillow():
pass pass
"""Install Pillow if not available""" """Install Pillow if not available"""

View File

@@ -3,10 +3,16 @@
from unittest.mock import Mock, patch, MagicMock, call from unittest.mock import Mock, patch, MagicMock, call
import pytest import pytest
from types import SimpleNamespace
from src.operators.generation_ops import ( from src.operators.generation_ops import (
TEXT_TEXTURE_OT_generate, TEXT_TEXTURE_OT_generate,
TEXT_TEXTURE_OT_preview, 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 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): def create_mock_context(props=None, **kwargs):
"""Create mock Blender context with scene properties. """Create mock Blender context with scene properties.
@@ -533,4 +568,115 @@ class TestGenerateShaderOperator:
invalid_context.scene = None invalid_context.scene = None
assert TEXT_TEXTURE_OT_generate_shader.poll(invalid_context) is False assert TEXT_TEXTURE_OT_generate_shader.poll(invalid_context) is False
else: 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()