Compare commits
5 Commits
ee5487e317
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| f8dc03caf9 | |||
| cf4f3ce564 | |||
| 2f5076b72b | |||
| fe2e81ecd0 | |||
| e3a8f0bcf0 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -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
70
USER_GUIDE.md
Normal 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 you’re 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 Blender’s 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.
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
Binary file not shown.
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
Binary file not shown.
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.
|
- **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 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.
|
- **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.
|
- **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.
|
- **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 Blender’s config folder, refresh them, and import/export JSON bundles for backups or team sharing.
|
- **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.
|
- **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`—we’re happy to help.
|
Questions or feature requests? Message us on Gumroad or email `marc@mintel.me`—we’re happy to help.
|
||||||
|
|||||||
@@ -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 Blender’s config folder, plus import/export for teams and backups.
|
- 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.
|
- 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
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.
@@ -1,5 +1,5 @@
|
|||||||
If you’ve ever jumped between Blender and a design app just to create a simple label, you already understand why I made Text Texture Generator. It’s 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 you’re doing the same label shuffle, try it on your next round and see how much quieter the workflow feels.
|
||||||
|
|||||||
@@ -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 I’m not digging through folders to see which version is right. Duplicate, tweak the wording, done.
|
||||||
|
|
||||||
If that sounds like the workflow you’ve 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 you’re tired of the export/import routine for labels, run the addon on your next revision cycle. It gave me my time back.
|
||||||
|
|||||||
@@ -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. I’d 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 doesn’t do anything magical—it just keeps the update process inside the scene. For me, that’s 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 you’re handling the same last-minute edits, keep the panel open during your next review. It helps.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Introducing people to Text Texture Generator is one of my favorite workshop moments. I start by showing that it’s 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 doesn’t disappear when the session ends.
|
It’s not glamorous, but it saves hours. I can stay focused on the design instead of managing image exports and shader nodes.
|
||||||
|
|
||||||
If you’re teaching or mentoring, add this addon to your toolkit. It’s 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. That’s exactly why I made it.
|
||||||
|
|||||||
@@ -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, it’s 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 you’re 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 you’re repeating the same labels, let the addon hold onto them. It keeps the process predictable.
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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',
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
@@ -955,13 +967,25 @@ class TEXT_TEXTURE_PT_panel(Panel):
|
|||||||
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)
|
||||||
preview_row = layout.row()
|
preview_row = layout.row()
|
||||||
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
|
||||||
|
|
||||||
@@ -1158,12 +1182,24 @@ class TEXT_TEXTURE_PT_panel_3d(Panel):
|
|||||||
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)
|
||||||
preview_row = layout.row()
|
preview_row = layout.row()
|
||||||
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
|
||||||
@@ -1361,12 +1397,24 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
|
|||||||
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)
|
||||||
preview_row = layout.row()
|
preview_row = layout.row()
|
||||||
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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 {
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||||
|
|
||||||
@@ -534,3 +569,114 @@ class TestGenerateShaderOperator:
|
|||||||
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user