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:
@@ -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,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,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,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,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+66
@@ -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>
|
||||
Vendored
+16
-16
@@ -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.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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,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
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+86
-44
@@ -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,
|
||||
|
||||
layout.separator()
|
||||
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')
|
||||
|
||||
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")
|
||||
row = layout.row(align=True)
|
||||
row.prop(s, "seed")
|
||||
row.prop(s, "random_amount", text="")
|
||||
row.operator("object.treegen_randomize", text="", icon='FILE_REFRESH')
|
||||
|
||||
box = layout.box()
|
||||
box.label(text="Zufall", icon='FILE_REFRESH')
|
||||
box.prop(s, "random_amount")
|
||||
box.operator("object.treegen_randomize", icon='FILE_REFRESH')
|
||||
layout.label(text="Feinjustierung: Modifier 'GN_Tree'", icon='MODIFIER')
|
||||
|
||||
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")
|
||||
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'}
|
||||
|
||||
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")
|
||||
|
||||
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_create", icon='OUTLINER_OB_MESH')
|
||||
layout.label(text="Feinjustierung: am Modifier 'GN_Tree'", icon='INFO')
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
@@ -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)))
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user