Panels aufgeraeumt (alle vier Addons) + Test-Suite instandgesetzt

Panel-Umbau
-----------
Jedes Addon hatte seine Einstellungen als Kette dauerhaft offener Boxen - beim
Rock Generator 15 Stueck, man musste am ganzen Panel vorbeiscrollen, um an den
Erzeugen-Knopf zu kommen. Jetzt steht oben nur, was man bei jedem Asset anfasst
(Vorlage, Name, grosser Erzeugen-Knopf, Zufall), der Rest liegt in nummerierten,
eingeklappten Unter-Panels in der Reihenfolge des Arbeitsablaufs:

  Rock Gen    9 Unter-Panels (Benennung ... Textur-Bake)
  Tree Gen    4 (Wachstum, Blattwerk, Zusatz-Ebenen, Export)
  Grass Gen   4 (Form, Haltung, Batch, Kanal-Belegung)
  Asset Utils 5 (war schon so, dient als Vorlage)

Schalter, die ein ganzes Unter-Panel steuern (Vertex-Farben backen,
Wachstums-Stufen), sitzen jetzt als Haekchen im Panel-Kopf - Zustand sichtbar
ohne aufzuklappen.

Gemessen statt geglaubt: test_panels.py registriert alle vier Addons, laesst
jedes Panel zeichnen, prueft Tab-Zugehoerigkeit und Auf-/Zugeklappt-Absicht,
dass jeder angebotene Operator existiert - und vor allem, dass JEDE definierte
Property irgendwo erreichbar bleibt (Zweige wie base_shape='BOX' werden dabei
durchgeschaltet). Ergebnis: 0 von 122 Properties beim Umbau verloren.

Test-Suite
----------
Beim Gegenpruefen kam heraus, dass fuenf Tests still veraltet waren und eine
echte Regression nicht gemeldet haetten:

  test_rock_gen      suchte Objekte mit Praefix "Rock_" - die heissen seit dem
                     Benennungssystem "Fels_A_M_01". Fand 0 von 3.
  test_base_shapes   loeschte am Schleifenanfang ALLE Objekte, auch die eigenen
                     Kopien aus vorigen Durchlaeufen -> StructRNA removed.
  test_ecogame_tree  erwartete eine feste Namensliste ohne die spaeter
                     dazugekommenen _Leaf-Objekte.
  test_tree_addon    dito, ausserdem Zip-Version 1.22.0 fest verdrahtet.
  test_leaves        Zip-Version 1.23.0 fest verdrahtet.

Drei Muster vermieden statt nur die Symptome geflickt: neue Objekte ueber die
Differenz vorher/nachher finden statt ueber ein Namens-Praefix, Zips per glob
statt mit fester Version, Zusatz-Ebenen beim Zaehlen herausfiltern.

Neu: run_tests.ps1 faehrt die komplette Suite mit einem Befehl (-Filter fuer
einzelne). Genau das fehlte - deshalb war das Veralten nicht aufgefallen.
13 von 13 Tests bestehen.

Nebenbei
--------
build.ps1 ruft server-generate jetzt mit --html auf (browsbare index.html fuer
statisches Hosting). Nachgemessen und im README festgehalten: zwei Zips
derselben Extension-ID sind KEIN Versions-Archiv - server-generate warnt
("archive found with duplicates ... conflicting blender versions"), mehrere
Versionen einer ID gehen nur bei verschiedenen Blender-Bereichen.

Versionen: Rock 2.13.0, Tree 1.24.0, Grass 1.1.0, Asset Utils 1.1.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 12:12:38 +02:00
co-authored by Claude Opus 4.8
parent ef57050739
commit 71b9982441
26 changed files with 902 additions and 284 deletions
+58 -2
View File
@@ -14,6 +14,35 @@
---
## Panel-Aufbau (alle vier Addons)
Jedes Addon hat oben im N-Panel nur das, was man **bei jedem Asset** anfasst -
Vorlage, Name, den grossen Erzeugen-Knopf, Zufall. Alles andere sitzt in
nummerierten, eingeklappten Unter-Panels in der Reihenfolge des Arbeitsablaufs:
| Addon | Unter-Panels |
|---|---|
| Rock Gen | 1 Benennung & Groesse · 2 Form · 3 Detail & Cleanup · 4 Shading · 5 Vertex-Farben · 6 UV/LOD/Material · 7 Batch & Transform · 8 FBX-Export · 9 Textur-Bake |
| Tree Gen | 1 Wachstum · 2 Blattwerk · 3 Zusatz-Ebenen · 4 Export |
| Grass Gen | 1 Form · 2 Haltung · 3 Batch & Budget · 4 Kanal-Belegung |
| Asset Utils | 1 Vorbereiten · 2 Vertex-Farben · 3 Benennung · 4 Pruefen · 5 Export |
Schalter, die ein ganzes Unter-Panel an- und ausknipsen (Vertex-Farben backen,
Wachstums-Stufen), sitzen als Haekchen **im Panel-Kopf** - so sieht man den
Zustand, ohne aufzuklappen.
In den Generatoren sind alle Unter-Panels zugeklappt, weil es Einstellungen sind,
die man selten aendert. In Asset Utils ist jedes Unter-Panel ein Arbeitsschritt;
1, 2 und 4 macht man bei jedem Asset und bleiben deshalb offen. `tests/test_panels.py`
haelt diese Absicht fest, damit sie nicht versehentlich verrutscht.
**Falle beim Bauen:** Unter-Panels brauchen dieselbe `bl_category` wie das
Eltern-Panel - sonst landen sie in einem anderen Tab und sind praktisch
unsichtbar. Und das Eltern-Panel muss **vor** seinen Kindern registriert werden,
sonst findet Blender die `bl_parent_id` nicht.
---
## EcoGame-Konventionen (beide Addons)
Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
@@ -203,6 +232,19 @@ zeigen lassen.
## Installation als Extension (via URL, mit Auto-Update)
> **Ein Repo, vier Extensions.** `blender --command extension server-generate`
> scannt `dist/` und listet jedes gefundene Zip - eine URL liefert in Blender
> alle vier Addons. `build.ps1` ruft das mit `--html` auf, dann entsteht
> zusaetzlich eine `index.html` zum Durchblaettern (auf einem statischen
> Webserver; Gitea liefert rohes HTML als Text aus).
>
> **Kein Versions-Archiv:** zwei Zips derselben Extension-ID nebeneinander sind
> nicht der Weg, eine alte Version zum Zurueckrollen bereitzuhalten -
> server-generate warnt dann (`archive found with duplicates ... conflicting
> blender versions`). Mehrere Versionen einer ID sind nur erlaubt, wenn sie
> verschiedene Blender-Bereiche abdecken. Alte Version wieder her: per
> `git checkout` des Zips.
Ab Blender 4.2 / 5.x als **Remote-Extension-Repository** einmal die URL
eintragen, danach gehen Updates per Klick.
@@ -291,14 +333,28 @@ Referenz:
## Tests
Alles auf einmal:
```bash
blender --background --python tests/test_rock_gen.py
pwsh -File run_tests.ps1
```
Weitere Suiten (jeweils `blender --background --python <datei>`):
Einzeln geht weiterhin `blender --background --python tests/<datei>.py`,
gefiltert `.\run_tests.ps1 -Filter panels`.
> **Warum der Runner existiert.** Die Tests lagen einzeln herum und wurden nur
> beim Bauen des jeweiligen Features gestartet. Beim Aufraeumen der Panels stellte
> sich heraus, dass **fuenf davon still veraltet waren**: sie prueften gegen
> Objekt-Namen aus der Zeit vor dem Benennungssystem (`Rock_` statt `Fels_A_M_01`),
> gegen fest verdrahtete Zip-Versionen, oder zaehlten die spaeter dazugekommenen
> `_Leaf`-Objekte mit. Sie haetten eine echte Regression nicht gemeldet. Die drei
> Muster, die das ausgeloest haben, sind jetzt vermieden: Objekte ueber die
> **Differenz** vorher/nachher finden statt ueber ein Namens-Praefix, Zips per
> **glob** statt mit fester Version, und Zusatz-Ebenen beim Zaehlen herausfiltern.
| Datei | prueft |
|---|---|
| `tests/test_panels.py` | Alle vier Addons: registrieren, jedes Panel zeichnet, Unter-Panels im richtigen Tab, **jede Property irgendwo erreichbar** |
| `tests/test_tree_addon.py` | Tree-Addon: Presets, Wachstums-Stufen, UVs, Modifier-Apply |
| `tests/test_ecogame_rock.py` | Fels-Benennung, Bake liefert nur `_albedo`/`_normal`, AO im Albedo |
| `tests/test_ecogame_tree.py` | Baum-Benennung `<Art>_<Variante>_S<Stufe>`, Ursprung, Lightmap-Default |
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_rock_generator"
version = "2.12.0"
version = "2.13.0"
name = "Stylized Rock Generator"
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
maintainer = "D4rkst3r"
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_grass_generator"
version = "1.0.0"
version = "1.1.0"
name = "Stylized Grass Generator"
tagline = "Gras, Blumen und Farne mit Wind-UV fuer SimpleGrassWind"
maintainer = "D4rkst3r"
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_tree_generator"
version = "1.23.0"
version = "1.24.0"
name = "Stylized Tree Generator"
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
maintainer = "D4rkst3r"
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_asset_utils"
version = "1.0.0"
version = "1.1.0"
name = "Stylized Asset Utils"
tagline = "Handgebaute Assets auf EcoGame-Konvention bringen"
maintainer = "D4rkst3r"
+9 -2
View File
@@ -31,7 +31,11 @@ $extensions = @(
@{ name = "utils"; manifest = "blender_manifest_utils.toml"; source = "stylized_asset_utils.py" }
)
# Alte Zips weg, damit geloeschte Versionen nicht im Listing haengen bleiben.
# Alte Zips weg. WICHTIG: zwei Zips derselben Extension-ID im selben Ordner sind
# KEIN Versions-Archiv zum Zurueckrollen - server-generate warnt dann
# ("archive found with duplicates ... conflicting blender versions"). Mehrere
# Versionen einer ID sind nur erlaubt, wenn sie verschiedene Blender-Bereiche
# abdecken. Alte Version braucht man wieder: per git checkout des Zips.
Get-ChildItem $dist -Filter *.zip -ErrorAction SilentlyContinue | Remove-Item -Force
foreach ($ext in $extensions) {
@@ -52,7 +56,10 @@ foreach ($ext in $extensions) {
}
Write-Host "== server-generate (index.json) ==" -ForegroundColor Cyan
& $Blender --command extension server-generate --repo-dir $dist
# --html erzeugt zusaetzlich eine index.html: auf einem statischen Webserver
# (z.B. nginx) laesst sich das Repo damit im Browser durchblaettern. Gitea liefert
# rohes HTML als text/plain aus, dort sieht man nur den Quelltext - schadet aber nicht.
& $Blender --command extension server-generate --repo-dir $dist --html
if ($LASTEXITCODE -ne 0) { throw "server-generate fehlgeschlagen" }
Write-Host "Fertig." -ForegroundColor Green
+66
View File
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Blender Extensions</title>
</head>
<body>
<p>Blender Extension Listing:</p>
<p>Add-on</p>
<hr>
<table>
<tr>
<th>ID</th>
<th>Name</th>
<th>Description</th>
<th>Website</th>
<th>Blender Versions</th>
<th>Python Versions</th>
<th>Platforms</th>
<th>Size</th>
</tr>
<tr>
<td><tt><a href="./stylized_asset_utils-1.1.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_asset_utils-1.1.0</a></tt></td>
<td>Stylized Asset Utils</td>
<td>Handgebaute Assets auf EcoGame-Konvention bringen</td>
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
<td>4.2.0 - ~</td>
<td>all</td>
<td>all</td>
<td>6.2KB</td>
</tr>
<tr>
<td><tt><a href="./stylized_grass_generator-1.1.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_grass_generator-1.1.0</a></tt></td>
<td>Stylized Grass Generator</td>
<td>Gras, Blumen und Farne mit Wind-UV fuer SimpleGrassWind</td>
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
<td>4.2.0 - ~</td>
<td>all</td>
<td>all</td>
<td>4.9KB</td>
</tr>
<tr>
<td><tt><a href="./stylized_rock_generator-2.13.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_rock_generator-2.13.0</a></tt></td>
<td>Stylized Rock Generator</td>
<td>Batch-Generator fuer stylized Rocks mit UE5-Export</td>
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
<td>4.2.0 - ~</td>
<td>all</td>
<td>all</td>
<td>21.7KB</td>
</tr>
<tr>
<td><tt><a href="./stylized_tree_generator-1.24.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.24.0</a></tt></td>
<td>Stylized Tree Generator</td>
<td>Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen</td>
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
<td>4.2.0 - ~</td>
<td>all</td>
<td>all</td>
<td>21.8KB</td>
</tr>
</table>
<center><p>Built 2026-07-31, 10:09</p></center>
</body>
</html>
+16 -16
View File
@@ -7,7 +7,7 @@
"id": "stylized_asset_utils",
"name": "Stylized Asset Utils",
"tagline": "Handgebaute Assets auf EcoGame-Konvention bringen",
"version": "1.0.0",
"version": "1.1.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -23,16 +23,16 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_asset_utils-1.0.0.zip",
"archive_size": 6340,
"archive_hash": "sha256:af8f9e95054432e4e7eea09b520b30a659ab354795191dcab0e5504c518306d3"
"archive_url": "./stylized_asset_utils-1.1.0.zip",
"archive_size": 6351,
"archive_hash": "sha256:71f3d1ee0db0b8ebb8a5e042a763e27de7accb83b655cb374bd2e8a3c6a3d94a"
},
{
"schema_version": "1.0.0",
"id": "stylized_grass_generator",
"name": "Stylized Grass Generator",
"tagline": "Gras, Blumen und Farne mit Wind-UV fuer SimpleGrassWind",
"version": "1.0.0",
"version": "1.1.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -48,16 +48,16 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_grass_generator-1.0.0.zip",
"archive_size": 4752,
"archive_hash": "sha256:ed8d7dda2f090f93c4ecad3b81af46c007578701e0851035366404399d13e14f"
"archive_url": "./stylized_grass_generator-1.1.0.zip",
"archive_size": 5027,
"archive_hash": "sha256:7a6335f0fabc8620d42a234a1c70856e1cf05b9fbd555306eec15e11d7f7ee7e"
},
{
"schema_version": "1.0.0",
"id": "stylized_rock_generator",
"name": "Stylized Rock Generator",
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
"version": "2.12.0",
"version": "2.13.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -73,16 +73,16 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_rock_generator-2.12.0.zip",
"archive_size": 21754,
"archive_hash": "sha256:6e18e32b841d627edfa6530587c99080c50d02ac4b0a532ed6079796bfa2aaf4"
"archive_url": "./stylized_rock_generator-2.13.0.zip",
"archive_size": 22263,
"archive_hash": "sha256:5ae39f5035435ae51803f63e1a0d06b67d8c68a5477e9fdb663a4a21c65d2492"
},
{
"schema_version": "1.0.0",
"id": "stylized_tree_generator",
"name": "Stylized Tree Generator",
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
"version": "1.23.0",
"version": "1.24.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -98,9 +98,9 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_tree_generator-1.23.0.zip",
"archive_size": 22135,
"archive_hash": "sha256:a9b986aaf5e95f7a9600d529eb3eb305af3d1edc6b327862035cecde50fed6e9"
"archive_url": "./stylized_tree_generator-1.24.0.zip",
"archive_size": 22352,
"archive_hash": "sha256:abdce976729a528b87f4eba56917f26b74a5f7e0c50877a0a427ff94bda40032"
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+49
View File
@@ -0,0 +1,49 @@
# Faehrt die komplette Test-Suite gegen die Addon-Dateien.
#
# Warum es das gibt: die Tests lagen einzeln herum und wurden nur beim Bauen des
# jeweiligen Features gestartet. Dadurch waren fuenf davon still veraltet - sie
# pruesten gegen Namen und Zip-Versionen, die es laengst nicht mehr gab, und
# haetten eine echte Regression nicht gemeldet. Ein Befehl fuer alles.
#
# Aufruf:
# .\run_tests.ps1
# .\run_tests.ps1 -Blender "E:\SteamLibrary\steamapps\common\Blender\blender.exe"
# .\run_tests.ps1 -Filter panels # nur tests\test_panels.py
#
# Hinweis: test_tree_addon.py prueft das GEBAUTE Zip aus dist\ - vorher
# .\build.ps1 laufen lassen, sonst meldet der Test das fehlende Zip.
param(
[string]$Blender = "blender",
[string]$Filter = "*"
)
$root = $PSScriptRoot
$tests = Get-ChildItem (Join-Path $root "tests") -Filter "test_$Filter.py" | Sort-Object Name
if (-not $tests) { throw "Keine Tests gefunden fuer Filter '$Filter'" }
$failed = @()
foreach ($t in $tests) {
$out = & $Blender --background --factory-startup --python $t.FullName 2>&1 | Out-String
# Die Tests melden ihr Ergebnis als letzte Zeile mit "ERGEBNIS:" bzw.
# "Alle Checks bestanden." - alles andere ist Blender-Rauschen.
$line = ($out -split "`n" |
Where-Object { $_ -match "ERGEBNIS|Alle Checks bestanden|FEHLGESCHLAGEN" } |
Select-Object -Last 1)
if (-not $line) { $line = "<keine Ergebniszeile>" }
$line = $line.Trim()
$ok = ($line -match "ALLE|OK$|bestanden") -and ($line -notmatch "FEHLER|FEHLGESCHLAGEN")
$color = if ($ok) { "Green" } else { "Red" }
if (-not $ok) { $failed += $t.BaseName }
Write-Host ("{0,-24} {1}" -f $t.BaseName, $line) -ForegroundColor $color
}
Write-Host ""
if ($failed) {
Write-Host ("FEHLGESCHLAGEN ({0}): {1}" -f $failed.Count, ($failed -join ", ")) -ForegroundColor Red
exit 1
}
Write-Host ("Alle {0} Tests bestanden." -f $tests.Count) -ForegroundColor Green
+1 -1
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Asset Utils",
"author": "D4rkst3r",
"version": (1, 0, 0),
"version": (1, 1, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Asset Utils",
"description": (
+79 -33
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Grass Generator",
"author": "D4rkst3r",
"version": (1, 0, 0),
"version": (1, 1, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Grass Gen",
"description": (
@@ -270,6 +270,7 @@ class GRASSGEN_OT_create(Operator):
# ---------------------------------------------------------------------------
class VIEW3D_PT_grass_generator(Panel):
"""Haupt-Panel: Vorlage, Name, Erzeugen. Der Rest sitzt in Unter-Panels."""
bl_label = "Grass Generator"
bl_idname = "VIEW3D_PT_grass_generator"
bl_space_type = 'VIEW_3D'
@@ -280,46 +281,86 @@ class VIEW3D_PT_grass_generator(Panel):
layout = self.layout
s = context.scene.grass_gen_settings
box = layout.box()
box.label(text="Vorlage", icon='PRESET')
row = box.row(align=True)
row = layout.row(align=True)
row.prop(s, "preset", text="")
row.operator("grassgen.apply_preset", text="", icon='IMPORT')
row = box.row(align=True)
row = layout.row(align=True)
row.prop(s, "art_name", text="")
row.prop(s, "variant")
box.label(text="Name: %s_%02d" % (_art(s), s.variant), icon='INFO')
box = layout.box()
box.label(text="Form")
box.prop(s, "blades")
box.prop(s, "height")
box.prop(s, "height_var")
box.prop(s, "width")
box.prop(s, "taper")
box.prop(s, "segments")
box = layout.box()
box.label(text="Haltung")
box.prop(s, "bend")
box.prop(s, "tilt")
box.prop(s, "spread")
box.prop(s, "curl")
box = layout.box()
box.label(text="Zufall", icon='FILE_REFRESH')
row = box.row(align=True)
row.prop(s, "seed")
row.operator("grassgen.randomize", text="", icon='FILE_REFRESH')
box.prop(s, "batch_count")
box.prop(s, "spacing")
box.prop(s, "tri_budget")
layout.separator()
layout.operator("object.grassgen_create", icon='OUTLINER_OB_CURVES')
col = layout.column(align=True)
col.label(text="UV0.V = Windmaske (Wurzel 0 - Spitze 1)", icon='INFO')
col.scale_y = 1.4
col.operator("object.grassgen_create", text="Gras erzeugen",
icon='OUTLINER_OB_CURVES')
layout.label(text="%s_%02d" % (_art(s), s.variant), icon='INFO')
row = layout.row(align=True)
row.prop(s, "seed")
row.operator("grassgen.randomize", text="", icon='FILE_REFRESH')
class _GrassSub(Panel):
"""Basis aller Unter-Panels."""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Grass Gen"
bl_parent_id = "VIEW3D_PT_grass_generator"
bl_options = {'DEFAULT_CLOSED'}
class VIEW3D_PT_grass_shape(_GrassSub):
bl_label = "1 - Form"
def draw(self, context):
layout = self.layout
s = context.scene.grass_gen_settings
layout.prop(s, "blades")
col = layout.column(align=True)
col.prop(s, "height")
col.prop(s, "height_var")
col.prop(s, "width")
col.prop(s, "taper")
layout.prop(s, "segments")
class VIEW3D_PT_grass_pose(_GrassSub):
bl_label = "2 - Haltung"
def draw(self, context):
layout = self.layout
s = context.scene.grass_gen_settings
col = layout.column(align=True)
col.prop(s, "bend")
col.prop(s, "tilt")
col.prop(s, "spread")
col.prop(s, "curl")
class VIEW3D_PT_grass_batch(_GrassSub):
bl_label = "3 - Batch & Budget"
def draw(self, context):
layout = self.layout
s = context.scene.grass_gen_settings
col = layout.column(align=True)
col.prop(s, "batch_count")
col.prop(s, "spacing")
layout.prop(s, "tri_budget")
class VIEW3D_PT_grass_info(_GrassSub):
bl_label = "4 - Kanal-Belegung"
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.label(text="UV0.V = Windmaske", icon='INFO')
col.label(text="Wurzel 0 - Spitze 1")
col.separator()
col.label(text="VCol: R=Wind G=Halm B=AO")
col.label(text="Biom-Tint laeuft ueber")
col.label(text="PerInstanceCustomData 0/1")
classes = (
@@ -327,7 +368,12 @@ classes = (
GRASSGEN_OT_apply_preset,
GRASSGEN_OT_randomize,
GRASSGEN_OT_create,
# Eltern-Panel zuerst, sonst findet Blender die bl_parent_id nicht.
VIEW3D_PT_grass_generator,
VIEW3D_PT_grass_shape,
VIEW3D_PT_grass_pose,
VIEW3D_PT_grass_batch,
VIEW3D_PT_grass_info,
)
+244 -163
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Rock Generator",
"author": "D4rkst3r",
"version": (2, 12, 0),
"version": (2, 13, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Rock Gen",
"description": (
@@ -1725,6 +1725,12 @@ class ROCKGEN_OT_preset_delete(Operator):
# ---------------------------------------------------------------------------
class VIEW3D_PT_rock_generator(Panel):
"""Haupt-Panel: nur was man bei JEDEM Stein anfasst.
Alles andere sitzt in eingeklappten Unter-Panels (bl_parent_id). Vorher war
das eine Kette aus 15 dauerhaft offenen Boxen - man musste scrollen, um an
den Erzeugen-Knopf zu kommen.
"""
bl_label = "Rock Generator"
bl_idname = "VIEW3D_PT_rock_generator"
bl_space_type = 'VIEW_3D'
@@ -1733,173 +1739,237 @@ class VIEW3D_PT_rock_generator(Panel):
def draw(self, context):
layout = self.layout
settings = context.scene.rock_gen_settings
s = context.scene.rock_gen_settings
box = layout.box()
box.label(text="Presets", icon='PRESET')
box.prop(settings, "active_preset", text="")
row = box.row(align=True)
row.operator("rockgen.preset_load", text="Laden", icon='IMPORT')
row.operator("rockgen.preset_delete", text="Loeschen", icon='TRASH')
row = box.row(align=True)
row.prop(settings, "new_preset_name", text="")
row.operator("rockgen.preset_save", text="Speichern", icon='ADD')
box = layout.box()
box.label(text="Benennung", icon='SORTALPHA')
row = box.row(align=True)
row.prop(settings, "name_prefix", text="")
row.prop(settings, "name_form", text="")
row.prop(settings, "name_class", text="")
box.prop(settings, "name_numbered")
row = box.row(align=True)
row.prop(settings, "use_class_size")
sub = row.row()
sub.enabled = not settings.use_class_size
sub.prop(settings, "base_size", text="")
if settings.use_class_size:
box.label(text="Basis-Groesse aus Klasse %s: x%.2f"
% (settings.name_class, CLASS_SIZES.get(settings.name_class, 1.0)),
icon='FIXED_SIZE')
box.label(text="Ergebnis: %s" % _asset_name(settings, 0, settings.batch_count),
icon='INFO')
box = layout.box()
box.label(text="Basis")
box.prop(settings, "base_shape")
if settings.base_shape == 'BOX':
box.prop(settings, "box_proportions")
box.prop(settings, "box_variation")
box.prop(settings, "subsurf_levels")
box = layout.box()
box.label(text="Noise / Displace")
box.prop(settings, "texture_type")
box.prop(settings, "noise_scale")
box.prop(settings, "displace_strength")
box.prop(settings, "seed")
box.prop(settings, "randomize_shape")
sub = box.column(align=True)
sub.enabled = settings.randomize_shape
sub.prop(settings, "noise_scale_max")
sub.prop(settings, "displace_strength_max")
box = layout.box()
box.label(text="Decimate / Cleanup")
box.prop(settings, "decimate_ratio_1")
box.prop(settings, "decimate_ratio_2")
box.prop(settings, "dissolve_angle")
box = layout.box()
box.label(text="Hard-Surface (optional)")
box.prop(settings, "add_bevel")
sub = box.column(align=True)
sub.enabled = settings.add_bevel
sub.prop(settings, "bevel_width")
sub.prop(settings, "bevel_segments")
box.prop(settings, "add_weighted_normal")
box = layout.box()
box.label(text="Shading / Form")
box.prop(settings, "shading_mode")
row = box.row()
row.enabled = settings.shading_mode == 'AUTO'
row.prop(settings, "autosmooth_angle")
box.prop(settings, "flatten_bottom")
row = box.row()
row.enabled = settings.flatten_bottom
row.prop(settings, "flatten_ratio")
box.prop(settings, "set_origin_bottom")
box = layout.box()
box.label(text="UV (Unreal Export)")
box.prop(settings, "generate_uv0")
box.prop(settings, "generate_lightmap_uv")
box = layout.box()
box.label(text="LOD")
box.prop(settings, "generate_lods")
sub = box.column(align=True)
sub.enabled = settings.generate_lods
sub.prop(settings, "lod_count")
sub.prop(settings, "lod_ratio_step")
box = layout.box()
box.label(text="Vertex-Cavity (ohne Textur)")
box.prop(settings, "bake_vertex_cavity")
sub = box.column(align=True)
sub.enabled = settings.bake_vertex_cavity
sub.prop(settings, "cavity_strength")
sub.prop(settings, "cavity_blur")
sub.separator()
sub.prop(settings, "moss_amount")
sub.prop(settings, "moss_noise")
sub.prop(settings, "moss_seed")
sub.prop(settings, "part_mask_from_shells")
sub.label(text="R=Cavity G=%s B=Hoehe"
% ("Teil-Maske" if settings.part_mask_from_shells else "Moos"),
icon='INFO')
box.separator()
box.operator("rockgen.bake_cavity_selection", icon='BRUSH_DATA')
box = layout.box()
box.label(text="Material")
box.prop(settings, "assign_material")
box = layout.box()
box.label(text="Transform")
box.prop(settings, "randomize_transform")
row = box.row(align=True)
row.prop(settings, "scale_min")
row.prop(settings, "scale_max")
box.prop(settings, "nonuniform_scale")
box.prop(settings, "keep_size")
box = layout.box()
box.label(text="Batch")
box.prop(settings, "batch_count")
box.prop(settings, "spacing")
box.prop(settings, "use_modal")
sub = box.row()
sub.enabled = settings.use_modal
sub.prop(settings, "modal_threshold")
box = layout.box()
box.label(text="Zufall", icon='FILE_REFRESH')
row = box.row(align=True)
row.prop(settings, "random_amount")
row.prop(settings, "random_shape", text="", icon='MOD_NOISE')
box.operator("rockgen.randomize", icon='FILE_REFRESH')
row = layout.row(align=True)
row.prop(s, "active_preset", text="")
row.operator("rockgen.preset_load", text="", icon='IMPORT')
row.operator("rockgen.preset_delete", text="", icon='TRASH')
row = layout.row(align=True)
row.prop(s, "new_preset_name", text="")
row.operator("rockgen.preset_save", text="", icon='ADD')
layout.separator()
layout.operator("object.generate_rocks_batch", icon='MESH_ICOSPHERE')
col = layout.column(align=True)
col.scale_y = 1.4
col.operator("object.generate_rocks_batch",
text="%d Stein(e) erzeugen" % s.batch_count,
icon='MESH_ICOSPHERE')
layout.label(text=_asset_name(s, 0, s.batch_count), icon='INFO')
box = layout.box()
box.label(text="FBX-Export (Unreal)")
box.prop(settings, "export_dir")
box.prop(settings, "export_selected_only")
box.operator("object.export_rocks_fbx", icon='EXPORT')
row = layout.row(align=True)
row.prop(s, "random_amount", text="Zufall")
row.prop(s, "random_shape", text="", icon='MOD_NOISE')
row.operator("rockgen.randomize", text="", icon='FILE_REFRESH')
box = layout.box()
box.label(text="Textur-Bake (tileable / Triplanar)")
box.prop(settings, "bake_dir")
box.prop(settings, "bake_prefix")
box.prop(settings, "bake_resolution")
box.prop(settings, "bake_base_color")
box.prop(settings, "bake_cell_scale")
box.prop(settings, "bake_seam_width")
box.prop(settings, "bake_plate_flat")
row = box.row(align=True)
row.prop(settings, "bake_color", toggle=True)
row.prop(settings, "bake_normal", toggle=True)
box.prop(settings, "bake_ao_into_albedo")
sub = box.row()
sub.enabled = settings.bake_ao_into_albedo
sub.prop(settings, "bake_ao_strength")
row = box.row(align=True)
row.prop(settings, "bake_roughness", toggle=True)
row.prop(settings, "bake_ao", toggle=True)
box.operator("object.bake_rock_textures", icon='TEXTURE')
class _RockSub(Panel):
"""Basis aller Unter-Panels - spart neunmal dieselben vier Zeilen."""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Rock Gen"
bl_parent_id = "VIEW3D_PT_rock_generator"
bl_options = {'DEFAULT_CLOSED'}
class VIEW3D_PT_rock_naming(_RockSub):
bl_label = "1 - Benennung & Groesse"
def draw(self, context):
layout = self.layout
s = context.scene.rock_gen_settings
row = layout.row(align=True)
row.prop(s, "name_prefix", text="")
row.prop(s, "name_form", text="")
row.prop(s, "name_class", text="")
layout.prop(s, "name_numbered")
layout.separator()
layout.prop(s, "use_class_size")
sub = layout.row()
sub.active = not s.use_class_size
sub.prop(s, "base_size")
if s.use_class_size:
layout.label(text="Klasse %s: x%.2f"
% (s.name_class, CLASS_SIZES.get(s.name_class, 1.0)),
icon='FIXED_SIZE')
class VIEW3D_PT_rock_shape(_RockSub):
bl_label = "2 - Form"
def draw(self, context):
layout = self.layout
s = context.scene.rock_gen_settings
layout.prop(s, "base_shape")
if s.base_shape == 'BOX':
layout.prop(s, "box_proportions")
layout.prop(s, "box_variation")
layout.prop(s, "subsurf_levels")
layout.separator()
layout.prop(s, "texture_type")
col = layout.column(align=True)
col.prop(s, "noise_scale")
col.prop(s, "displace_strength")
layout.prop(s, "seed")
layout.separator()
layout.prop(s, "randomize_shape")
sub = layout.column(align=True)
sub.active = s.randomize_shape
sub.prop(s, "noise_scale_max")
sub.prop(s, "displace_strength_max")
class VIEW3D_PT_rock_detail(_RockSub):
bl_label = "3 - Detail & Cleanup"
def draw(self, context):
layout = self.layout
s = context.scene.rock_gen_settings
col = layout.column(align=True)
col.prop(s, "decimate_ratio_1")
col.prop(s, "decimate_ratio_2")
layout.prop(s, "dissolve_angle")
layout.separator()
layout.prop(s, "add_bevel")
sub = layout.column(align=True)
sub.active = s.add_bevel
sub.prop(s, "bevel_width")
sub.prop(s, "bevel_segments")
layout.prop(s, "add_weighted_normal")
class VIEW3D_PT_rock_shading(_RockSub):
bl_label = "4 - Shading & Ausrichtung"
def draw(self, context):
layout = self.layout
s = context.scene.rock_gen_settings
layout.prop(s, "shading_mode")
row = layout.row()
row.active = s.shading_mode == 'AUTO'
row.prop(s, "autosmooth_angle")
layout.separator()
layout.prop(s, "flatten_bottom")
row = layout.row()
row.active = s.flatten_bottom
row.prop(s, "flatten_ratio")
layout.prop(s, "set_origin_bottom")
class VIEW3D_PT_rock_vcol(_RockSub):
bl_label = "5 - Vertex-Farben"
def draw_header(self, context):
# Schalter im Panel-Kopf: man sieht auch eingeklappt, ob gebacken wird.
self.layout.prop(context.scene.rock_gen_settings,
"bake_vertex_cavity", text="")
def draw(self, context):
layout = self.layout
s = context.scene.rock_gen_settings
col = layout.column()
col.active = s.bake_vertex_cavity
sub = col.column(align=True)
sub.prop(s, "cavity_strength")
sub.prop(s, "cavity_blur")
col.separator()
sub = col.column(align=True)
sub.prop(s, "moss_amount")
sub.prop(s, "moss_noise")
sub.prop(s, "moss_seed")
col.prop(s, "part_mask_from_shells")
col.label(text="R=Cavity G=%s B=Hoehe"
% ("Teil-Maske" if s.part_mask_from_shells else "Moos"),
icon='INFO')
# Nicht in der ausgegrauten Spalte: der Knopf arbeitet auf der Auswahl
# und ist auch dann sinnvoll, wenn beim Erzeugen nicht gebacken wird.
layout.separator()
layout.operator("rockgen.bake_cavity_selection", icon='BRUSH_DATA')
class VIEW3D_PT_rock_uv(_RockSub):
bl_label = "6 - UV, LOD & Material"
def draw(self, context):
layout = self.layout
s = context.scene.rock_gen_settings
layout.prop(s, "generate_uv0")
layout.prop(s, "generate_lightmap_uv")
layout.separator()
layout.prop(s, "generate_lods")
sub = layout.column(align=True)
sub.active = s.generate_lods
sub.prop(s, "lod_count")
sub.prop(s, "lod_ratio_step")
layout.separator()
layout.prop(s, "assign_material")
class VIEW3D_PT_rock_batch(_RockSub):
bl_label = "7 - Batch & Transform"
def draw(self, context):
layout = self.layout
s = context.scene.rock_gen_settings
col = layout.column(align=True)
col.prop(s, "batch_count")
col.prop(s, "spacing")
layout.separator()
layout.prop(s, "use_modal")
row = layout.row()
row.active = s.use_modal
row.prop(s, "modal_threshold")
layout.separator()
layout.prop(s, "randomize_transform")
sub = layout.column(align=True)
sub.active = s.randomize_transform
row = sub.row(align=True)
row.prop(s, "scale_min")
row.prop(s, "scale_max")
sub.prop(s, "nonuniform_scale")
sub.prop(s, "keep_size")
class VIEW3D_PT_rock_export(_RockSub):
bl_label = "8 - FBX-Export (Unreal)"
def draw(self, context):
layout = self.layout
s = context.scene.rock_gen_settings
layout.prop(s, "export_dir")
layout.prop(s, "export_selected_only")
layout.operator("object.export_rocks_fbx", icon='EXPORT')
class VIEW3D_PT_rock_bake(_RockSub):
bl_label = "9 - Textur-Bake (tileable)"
def draw(self, context):
layout = self.layout
s = context.scene.rock_gen_settings
layout.prop(s, "bake_dir")
row = layout.row(align=True)
row.prop(s, "bake_prefix", text="")
row.prop(s, "bake_resolution", text="")
layout.separator()
col = layout.column(align=True)
col.prop(s, "bake_base_color")
col.prop(s, "bake_cell_scale")
col.prop(s, "bake_seam_width")
col.prop(s, "bake_plate_flat")
layout.separator()
row = layout.row(align=True)
row.prop(s, "bake_color", toggle=True)
row.prop(s, "bake_normal", toggle=True)
row = layout.row(align=True)
row.prop(s, "bake_roughness", toggle=True)
row.prop(s, "bake_ao", toggle=True)
layout.prop(s, "bake_ao_into_albedo")
row = layout.row()
row.active = s.bake_ao_into_albedo
row.prop(s, "bake_ao_strength")
layout.separator()
layout.operator("object.bake_rock_textures", icon='TEXTURE')
# ---------------------------------------------------------------------------
@@ -1916,7 +1986,18 @@ classes = (
ROCKGEN_OT_preset_save,
ROCKGEN_OT_preset_load,
ROCKGEN_OT_preset_delete,
# Das Eltern-Panel MUSS vor seinen Unter-Panels stehen, sonst findet Blender
# die bl_parent_id nicht. Die Reihenfolge hier ist die Reihenfolge im N-Panel.
VIEW3D_PT_rock_generator,
VIEW3D_PT_rock_naming,
VIEW3D_PT_rock_shape,
VIEW3D_PT_rock_detail,
VIEW3D_PT_rock_shading,
VIEW3D_PT_rock_vcol,
VIEW3D_PT_rock_uv,
VIEW3D_PT_rock_batch,
VIEW3D_PT_rock_export,
VIEW3D_PT_rock_bake,
)
+93 -51
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Tree Generator",
"author": "D4rkst3r",
"version": (1, 23, 0),
"version": (1, 24, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -1518,6 +1518,7 @@ class TREEGEN_OT_create(Operator):
class VIEW3D_PT_tree_generator(Panel):
"""Haupt-Panel: Vorlage, Name, Erzeugen. Der Rest sitzt in Unter-Panels."""
bl_label = "Tree Generator"
bl_idname = "VIEW3D_PT_tree_generator"
bl_space_type = 'VIEW_3D'
@@ -1528,60 +1529,96 @@ class VIEW3D_PT_tree_generator(Panel):
layout = self.layout
s = context.scene.tree_gen_settings
box = layout.box()
box.label(text="Vorlage", icon='PRESET')
box.prop(s, "preset", text="")
box.prop(s, "seed")
row = box.row(align=True)
layout.prop(s, "preset", text="")
row = layout.row(align=True)
row.prop(s, "art_name", text="")
row.prop(s, "variant")
box.label(text="Name: %s_%02d%s" % (_art(s), s.variant,
"_S1..S%d" % s.stages if s.use_growth else ""),
icon='INFO')
box = layout.box()
box.label(text="Wachstum")
box.prop(s, "use_growth")
sub = box.column(align=True)
sub.enabled = s.use_growth
sub.prop(s, "stages")
sub.prop(s, "spacing")
box = layout.box()
box.label(text="Zufall", icon='FILE_REFRESH')
box.prop(s, "random_amount")
box.operator("object.treegen_randomize", icon='FILE_REFRESH')
box = layout.box()
box.label(text="Blattwerk", icon='OUTLINER_OB_POINTCLOUD')
box.prop(s, "leaf_card")
sub = box.column(align=True)
sub.enabled = s.leaf_card is not None
sub.prop(s, "leaf_density")
sub.prop(s, "leaf_size")
sub.prop(s, "leaf_size_var")
sub.prop(s, "leaf_max_radius")
sub.prop(s, "leaf_tilt")
sub.prop(s, "leaf_budget")
box.operator("object.treegen_leaves", icon='OUTLINER_OB_POINTCLOUD')
if s.leaf_card is None:
box.label(text="Ohne Card bleibt _Leaf leer", icon='INFO')
box = layout.box()
box.label(text="Zusatz-Ebenen")
box.prop(s, "make_leaf")
box.prop(s, "make_frucht")
box = layout.box()
box.label(text="Export")
box.prop(s, "apply_modifier")
row = box.row()
row.enabled = s.apply_modifier
row.prop(s, "generate_lightmap_uv")
layout.separator()
layout.operator("object.treegen_create", icon='OUTLINER_OB_MESH')
layout.label(text="Feinjustierung: am Modifier 'GN_Tree'", icon='INFO')
col = layout.column(align=True)
col.scale_y = 1.4
col.operator("object.treegen_create", text="Baum erzeugen",
icon='OUTLINER_OB_MESH')
layout.label(text="%s_%02d%s" % (_art(s), s.variant,
"_S1..S%d" % s.stages if s.use_growth else ""),
icon='INFO')
row = layout.row(align=True)
row.prop(s, "seed")
row.prop(s, "random_amount", text="")
row.operator("object.treegen_randomize", text="", icon='FILE_REFRESH')
layout.label(text="Feinjustierung: Modifier 'GN_Tree'", icon='MODIFIER')
class _TreeSub(Panel):
"""Basis aller Unter-Panels."""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Tree Gen"
bl_parent_id = "VIEW3D_PT_tree_generator"
bl_options = {'DEFAULT_CLOSED'}
class VIEW3D_PT_tree_growth(_TreeSub):
bl_label = "1 - Wachstum"
def draw_header(self, context):
self.layout.prop(context.scene.tree_gen_settings, "use_growth", text="")
def draw(self, context):
layout = self.layout
s = context.scene.tree_gen_settings
col = layout.column(align=True)
col.active = s.use_growth
col.prop(s, "stages")
col.prop(s, "spacing")
if s.use_growth:
layout.label(text="Setzling bis Baum in %d Stufen" % s.stages,
icon='INFO')
class VIEW3D_PT_tree_leaves(_TreeSub):
bl_label = "2 - Blattwerk"
def draw(self, context):
layout = self.layout
s = context.scene.tree_gen_settings
layout.prop(s, "leaf_card")
col = layout.column(align=True)
col.active = s.leaf_card is not None
col.prop(s, "leaf_density")
col.prop(s, "leaf_size")
col.prop(s, "leaf_size_var")
col.prop(s, "leaf_max_radius")
col.prop(s, "leaf_tilt")
col.prop(s, "leaf_budget")
layout.separator()
layout.operator("object.treegen_leaves", icon='OUTLINER_OB_POINTCLOUD')
if s.leaf_card is None:
layout.label(text="Ohne Card bleibt _Leaf leer", icon='INFO')
class VIEW3D_PT_tree_layers(_TreeSub):
bl_label = "3 - Zusatz-Ebenen"
def draw(self, context):
layout = self.layout
s = context.scene.tree_gen_settings
layout.prop(s, "make_leaf")
layout.prop(s, "make_frucht")
class VIEW3D_PT_tree_export(_TreeSub):
bl_label = "4 - Export"
def draw(self, context):
layout = self.layout
s = context.scene.tree_gen_settings
layout.prop(s, "apply_modifier")
row = layout.row()
row.active = s.apply_modifier
row.prop(s, "generate_lightmap_uv")
classes = (
@@ -1589,7 +1626,12 @@ classes = (
TREEGEN_OT_randomize,
TREEGEN_OT_leaves,
TREEGEN_OT_create,
# Eltern-Panel zuerst, sonst findet Blender die bl_parent_id nicht.
VIEW3D_PT_tree_generator,
VIEW3D_PT_tree_growth,
VIEW3D_PT_tree_leaves,
VIEW3D_PT_tree_layers,
VIEW3D_PT_tree_export,
)
+14 -4
View File
@@ -26,21 +26,31 @@ VARS=[("Wuerfel",'CUBE',None),
print("%-9s %-6s %s" % ("Variante","Form","Abmessungen X/Y/Z (m)"))
keep=[]; x=0.0
for name,shape,prop in VARS:
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
# FALLE: das hier loeschte frueher ALLE Objekte - auch die Kopien aus den
# vorigen Durchlaeufen. Die waren beim Rendern tot ("StructRNA ... removed").
kept = {k[0] for k in keep}
for o in list(bpy.data.objects):
if o not in kept: bpy.data.objects.remove(o,do_unlink=True)
s.base_shape=shape; s.box_variation=0.0
if prop: s.box_proportions=prop
s.seed=7
# Die behaltenen Kopien stehen weiter in der Szene - [0] wuerde eine davon
# greifen statt des frisch erzeugten Steins. Deshalb ueber die Differenz.
before=set(bpy.context.scene.objects)
bpy.ops.object.generate_rocks_batch()
ob=[o for o in bpy.context.scene.objects if o.type=='MESH'][0]
ob=[o for o in bpy.context.scene.objects
if o.type=='MESH' and o not in before][0]
bb=ob.bound_box
d=[max(v[i] for v in bb)-min(v[i] for v in bb) for i in range(3)]
print("%-9s %-6s %.2f / %.2f / %.2f" % (name,shape,d[0],d[1],d[2]))
if name=="Platte" and not (d[2] < d[0]*0.45): fails.append("platte_nicht_flach")
if name=="Riegel" and not (d[0] > d[1]*1.5): fails.append("riegel_nicht_laenglich")
cp=ob.copy(); cp.data=ob.data.copy(); keep.append((cp,x))
cp=ob.copy(); cp.data=ob.data.copy()
bpy.context.collection.objects.link(cp) # sofort verlinken, nicht spaeter
keep.append((cp,x))
x+=2.4
for cp,px in keep:
bpy.context.collection.objects.link(cp); cp.location=(px,0,0)
cp.location=(px,0,0)
cp.data.materials.clear(); cp.data.materials.append(mat)
for o in list(bpy.context.scene.objects):
if o.type=='MESH' and o not in [k[0] for k in keep]: bpy.data.objects.remove(o,do_unlink=True)
+7 -3
View File
@@ -16,17 +16,21 @@ if m.TreeGenSettings.bl_rna.properties['generate_lightmap_uv'].default: fails.ap
s.preset='birke'; s.art_name="Birke"; s.variant=1
s.use_growth=True; s.stages=4; s.apply_modifier=False
bpy.ops.object.treegen_create()
# Seit der Blattwerk-Ebene entsteht zu jeder Stufe ein zweites Objekt "_Leaf".
# Deshalb Stamm und Ebene getrennt pruefen statt eine feste Gesamtliste.
names=sorted(o.name for o in bpy.context.scene.objects if o.type=='MESH')
print("STUFEN:", names)
if names != ["Birke_01_S1","Birke_01_S2","Birke_01_S3","Birke_01_S4"]: fails.append("stufen_namen")
stems=[n for n in names if not n.endswith(("_Leaf","_Frucht"))]
if stems != ["Birke_01_S1","Birke_01_S2","Birke_01_S3","Birke_01_S4"]: fails.append("stufen_namen")
if [n for n in names if n.endswith("_Leaf")] != [s_+"_Leaf" for s_ in stems]: fails.append("stufen_leaf")
# Einzelbaum, Art aus Preset abgeleitet
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
s.use_growth=False; s.art_name=""; s.preset='eiche'; s.variant=2
bpy.ops.object.treegen_create()
single=[o.name for o in bpy.context.scene.objects if o.type=='MESH']
single=sorted(o.name for o in bpy.context.scene.objects if o.type=='MESH')
print("EINZELN:", single)
if single != ["Eiche_02"]: fails.append("einzel_name")
if [n for n in single if not n.endswith(("_Leaf","_Frucht"))] != ["Eiche_02"]: fails.append("einzel_name")
# Ursprung unten-mittig + echte Meter
ob=bpy.context.scene.objects[single[0]]
+9 -2
View File
@@ -1,5 +1,12 @@
import bpy, sys, zipfile, os, tempfile, importlib.util, math, bmesh
ZIP=r"A:\eco\stylized_rock_generator\dist\stylized_tree_generator-1.23.0.zip"
import bpy, glob, sys, zipfile, os, tempfile, importlib.util, math, bmesh
# Version NICHT fest verdrahten - sonst schlaegt der Test nach jedem
# Versions-Bump fehl, obwohl am Addon nichts kaputt ist.
_d = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "dist")
_z = sorted(glob.glob(os.path.join(_d, "stylized_tree_generator-*.zip")))
if not _z:
print("ERGEBNIS: FEHLER: kein gebautes Tree-Zip in dist/ (erst build.ps1)")
sys.exit(1)
ZIP = _z[-1]
tmp=tempfile.mkdtemp()
with zipfile.ZipFile(ZIP) as z: z.extractall(tmp)
init=[os.path.join(r,"__init__.py") for r,_,f in os.walk(tmp) if "__init__.py" in f][0]
+234
View File
@@ -0,0 +1,234 @@
"""Registriert alle vier Addons und prueft die Panel-Struktur.
Warum das eigene Test-Datei ist: py_compile sagt nur, dass die Datei syntaktisch
gueltig ist. Ob Blender die Panels ANNIMMT, ist eine andere Frage - bl_parent_id
muss auf ein bereits registriertes Panel zeigen, bl_category muss zwischen
Eltern- und Unter-Panel uebereinstimmen (sonst landet das Unter-Panel in einem
anderen Tab und ist praktisch unsichtbar), und jedes Panel muss zeichnen koennen,
ohne zu werfen.
Aufruf:
blender --background --factory-startup --python tests/test_panels.py
"""
import os
import sys
import importlib.util
import bpy
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Addon-Datei -> (Haupt-Panel-idname, Anzahl Unter-Panels, die offen sein DUERFEN)
#
# In den Generatoren sind Unter-Panels Einstellungen, die man selten anfasst -
# die gehoeren alle zugeklappt, damit der Erzeugen-Knopf oben sichtbar bleibt.
# In Asset Utils ist jedes Unter-Panel ein Arbeitsschritt: 1/2/4 macht man bei
# JEDEM Asset, die bleiben offen. Deshalb steht die Erwartung hier pro Addon
# statt als pauschale Regel.
ADDONS = {
"stylized_rock_generator.py": ("VIEW3D_PT_rock_generator", 9,
"rock_gen_settings", set()),
"stylized_tree_generator.py": ("VIEW3D_PT_tree_generator", 4,
"tree_gen_settings", set()),
"stylized_grass_generator.py": ("VIEW3D_PT_grass_generator", 4,
"grass_gen_settings", set()),
"stylized_asset_utils.py": ("VIEW3D_PT_asset_utils", 5,
"asset_utils_settings",
{"1 - Vorbereiten", "2 - Vertex-Farben",
"4 - Pruefen"}),
}
failures = []
def load(path, name):
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
class FakeSelf:
"""Ersatz fuer die Panel-Instanz: draw() braucht nur self.layout."""
def __init__(self, layout):
self.layout = layout
class FakeLayout:
"""Minimal-Layout, das jeden UI-Aufruf schluckt und mitzaehlt.
Damit laesst sich draw() ohne echten UI-Kontext ausfuehren - der ist im
--background-Modus nicht verfuegbar.
"""
def __init__(self, counter):
self._c = counter
def _child(self, *a, **k):
return FakeLayout(self._c)
row = column = box = split = _child
def prop(self, data, name, **k):
# Zugriff erzwingen: schlaegt fehl, wenn die Property nicht existiert.
getattr(data, name)
self._c["prop"] += 1
def operator(self, idname, **k):
self._c["op"] += 1
self._c["ops"].add(idname)
return self
def label(self, **k):
self._c["label"] += 1
def separator(self, **k):
pass
def menu(self, *a, **k):
pass
def template_ID(self, *a, **k):
pass
def __setattr__(self, k, v):
# scale_y / active / enabled einfach annehmen
object.__setattr__(self, k, v)
class CollectLayout(FakeLayout):
"""Wie FakeLayout, sammelt aber die Namen der gezeichneten Properties."""
def __init__(self, names):
FakeLayout.__init__(self, {"prop": 0, "op": 0, "label": 0, "ops": set()})
self._names = names
def _child(self, *a, **k):
return CollectLayout(self._names)
row = column = box = split = _child
def prop(self, data, name, **k):
self._names.add(name)
for filename, (parent_id, want_subs, settings_attr, open_ok) in ADDONS.items():
path = os.path.join(ROOT, filename)
mod = load(path, filename[:-3])
try:
mod.register()
except Exception as exc:
failures.append("%s: register() wirft %s" % (filename, exc))
continue
panels = [c for c in mod.classes if issubclass(c, bpy.types.Panel)]
parent = [p for p in panels if getattr(p, "bl_idname", "") == parent_id]
if not parent:
failures.append("%s: Haupt-Panel %s fehlt" % (filename, parent_id))
mod.unregister()
continue
parent = parent[0]
subs = [p for p in panels if getattr(p, "bl_parent_id", "") == parent_id]
if len(subs) != want_subs:
failures.append("%s: %d Unter-Panels, erwartet %d"
% (filename, len(subs), want_subs))
for sub in subs:
# Gleicher Tab wie das Eltern-Panel?
if sub.bl_category != parent.bl_category:
failures.append("%s/%s: bl_category '%s' != Eltern-Tab '%s'"
% (filename, sub.__name__, sub.bl_category,
parent.bl_category))
# Eingeklappt - ausser bei den ausdruecklich als offen gewollten.
is_closed = 'DEFAULT_CLOSED' in getattr(sub, "bl_options", set())
if not is_closed and sub.bl_label not in open_ok:
failures.append("%s/%s (%s): offen, sollte eingeklappt sein"
% (filename, sub.__name__, sub.bl_label))
if is_closed and sub.bl_label in open_ok:
failures.append("%s/%s (%s): eingeklappt, sollte offen sein"
% (filename, sub.__name__, sub.bl_label))
# Wirklich registriert? Blender haengt bl_rna an registrierte Klassen.
if not hasattr(sub, "bl_rna"):
failures.append("%s/%s: nicht registriert"
% (filename, sub.__name__))
# Jedes Panel einmal zeichnen lassen.
total = {"prop": 0, "op": 0, "label": 0, "ops": set()}
for p in [parent] + subs:
counter = {"prop": 0, "op": 0, "label": 0, "ops": set()}
# FALLE: registrierte Panel-Klassen lassen sich nicht instanziieren
# ("bpy_struct.__new__(struct): expected a single argument"). draw() ist
# aber eine normale Python-Funktion - self darf alles sein, was .layout hat.
inst = FakeSelf(FakeLayout(counter))
try:
p.draw(inst, bpy.context)
if "draw_header" in p.__dict__:
p.draw_header(inst, bpy.context)
except Exception as exc:
failures.append("%s/%s: draw() wirft %s: %s"
% (filename, p.__name__, type(exc).__name__, exc))
continue
if counter["prop"] + counter["op"] + counter["label"] == 0:
failures.append("%s/%s: zeichnet gar nichts"
% (filename, p.__name__))
for k in ("prop", "op", "label"):
total[k] += counter[k]
total["ops"] |= counter["ops"]
# Jede definierte Property muss irgendwo im Panel erreichbar sein.
#
# Das ist der eigentliche Regressionstest beim Umbauen der Panels: beim
# Verschieben von Reglern zwischen Panels faellt leicht einer heraus, und
# dann gibt es eine Einstellung, an die man in Blender nicht mehr rankommt.
# Ein paar Regler haengen an einem Zweig (BOX-Proportionen nur bei
# base_shape='BOX'), deshalb werden diese Zweige vorher durchgeschaltet.
st = getattr(bpy.context.scene, settings_attr, None)
if st is not None:
drawn = set()
branches = [{}]
for attr, values in (("base_shape", ('CUBE', 'BOX', 'ICOSPHERE')),
("g_mode", ('SHELLS', 'MOSS')),
("shading_mode", ('AUTO', 'FLAT', 'SMOOTH'))):
if hasattr(st, attr):
branches += [{attr: v} for v in values]
for combo in branches:
for attr, val in combo.items():
try:
setattr(st, attr, val)
except TypeError:
continue
for p in [parent] + subs:
try:
p.draw(FakeSelf(CollectLayout(drawn)), bpy.context)
if "draw_header" in p.__dict__:
p.draw_header(FakeSelf(CollectLayout(drawn)), bpy.context)
except Exception:
pass
defined = set(type(st).__annotations__)
unreachable = sorted(defined - drawn)
if unreachable:
failures.append("%s: Property(s) in keinem Panel erreichbar: %s"
% (filename, ", ".join(unreachable)))
# Jeder Operator, den ein Panel anbietet, muss es auch geben.
for idname in sorted(total["ops"]):
grp, _, op = idname.partition(".")
if not hasattr(getattr(bpy.ops, grp, None), op):
failures.append("%s: Panel ruft unbekannten Operator %s"
% (filename, idname))
print("%-32s %d Unter-Panels %3d Props %2d Buttons %2d Labels"
% (filename, len(subs), total["prop"], total["op"], total["label"]))
mod.unregister()
print("")
if failures:
print("ERGEBNIS: %d FEHLER" % len(failures))
for f in failures:
print(" - " + f)
sys.exit(1)
print("ERGEBNIS: ALLE PANELS OK")
+5 -1
View File
@@ -81,12 +81,16 @@ def main():
settings.assign_material = False
# 3. Batch erzeugen
# Vorher merken, was schon da ist: das Benennungsschema kommt aus den
# Einstellungen (Fels_A_M_01 ...), ein fest verdrahtetes Praefix wie "Rock_"
# geht schief, sobald jemand die Benennung aendert. Differenz statt Ratename.
before = set(bpy.context.scene.objects)
result = bpy.ops.object.generate_rocks_batch()
if 'FINISHED' not in result:
failures.append("Operator lieferte nicht {'FINISHED'}: %s" % (result,))
rocks = [o for o in bpy.context.scene.objects
if o.type == 'MESH' and o.name.startswith("Rock_")]
if o.type == 'MESH' and o not in before]
if len(rocks) != BATCH:
failures.append("Erwartet %d Rocks, gefunden %d" % (BATCH, len(rocks)))
+14 -2
View File
@@ -1,7 +1,15 @@
"""Prueft das gebaute Tree-Addon: registrieren, Operator ausfuehren, Ergebnis messen."""
import bpy, sys, zipfile, os, tempfile, importlib.util, traceback
ZIP = r"A:\eco\stylized_rock_generator\dist\stylized_tree_generator-1.22.0.zip"
# Version NICHT fest verdrahten - sonst schlaegt der Test bei jedem Bump fehl,
# obwohl am Addon nichts kaputt ist.
import glob
_d = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "dist")
_z = sorted(glob.glob(os.path.join(_d, "stylized_tree_generator-*.zip")))
if not _z:
print("ERGEBNIS: FEHLER: kein gebautes Tree-Zip in dist/ (erst build.ps1 laufen lassen)")
sys.exit(1)
ZIP = _z[-1]
fails = []
tmp = tempfile.mkdtemp()
@@ -46,7 +54,11 @@ for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
s.preset = "baum"; s.use_growth = True; s.stages = 4
r = bpy.ops.object.treegen_create()
obs = sorted([o for o in bpy.data.objects if o.type == 'MESH'], key=lambda o: o.name)
# Nur die Staemme: seit der Blattwerk-Ebene gibt es zu jeder Stufe noch ein
# "_Leaf"-Objekt. Mit denen zusammen ist die Vertex-Folge nicht mehr aufsteigend.
obs = sorted([o for o in bpy.data.objects
if o.type == 'MESH' and not o.name.endswith(("_Leaf", "_Frucht"))],
key=lambda o: o.name)
bpy.context.view_layer.update()
dg = bpy.context.evaluated_depsgraph_get()
counts = [len(o.evaluated_get(dg).data.vertices) for o in obs]