Tree Gen 1.35.0: Node-Gruppen-Waechter + Mindestradius fuer Saemlinge

1) build_group riss Szenen-Baeume ab
   Die Gruppe GN_Tree wurde bei JEDEM Erzeugen geloescht und neu angelegt -
   damit zeigten alle bereits erzeugten Baeume auf ein totes Datablock und
   evaluierten leer. Gemessen: nach vier Arten-Erzeugungen lebte nur noch die
   letzte. Dass direkt nach dem Erzeugen exportiert wurde, hat das verdeckt.

   Jetzt ein Waechter mit ZWEI Kriterien: abgelegte Schema-Version
   (TREE_SCHEMA) UND die tatsaechlich vorhandenen Eingaenge. Die Version allein
   reicht nicht - wer waehrend der Entwicklung Sockets aendert ohne die Zahl
   anzufassen, bekaeme eine veraltete Gruppe untergeschoben. Muss doch neu
   gebaut werden, werden bestehende Modifier auf die neue Gruppe umgehaengt.

   Beide Pfade gemessen: vier Arten nacheinander leben alle (624/796/924/1036
   Verts), und nach build_group(rebuild=True) ebenfalls unveraendert.

2) Saemlinge zu duenn
   Die McMahon-Skalierung macht duennstaemmige Arten am Setzling zu Draehten:
   Birke 0.6 cm Fussradius gegen 1.9 der Eiche - dreimal duenner, waehrend der
   Unterschied beim Altbaum nur 1.2x betraegt. Die Regel gilt fuer die Statik
   ausgewachsener Baeume; ein Saemling ist eine Pflanze, kein Balken.
   SEEDLING_MIN_RADIUS + SEEDLING_MIN_FLARE heben die duennen Arten an, ohne
   die dicken anzufassen. Danach liegen alle bei 2.8 cm.

   test_proportionen laesst die McMahon-Abweichung genau dort zu, wo der Boden
   greift, und prueft dass sie nicht weiter geht - die Regel bleibt sonst scharf.

23 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 11:08:24 +02:00
co-authored by Claude Opus 4.8
parent 1b833bf44a
commit fa7067d42e
8 changed files with 127 additions and 16 deletions
+30
View File
@@ -563,6 +563,36 @@ Der Kaktus erzeugt regulaer **0 Cards** — kein Ast ist duenner als `Max Radius
und das ist botanisch richtig. Vorher lief das still durch und man stand vor
einer leeren `_Leaf`-Ebene; jetzt sagt der Operator es und nennt den Regler.
### Die Node-Gruppe wird WIEDERVERWENDET, nicht neu gebaut
`build_group()` loeschte die Gruppe `GN_Tree` bei jedem Erzeugen und legte sie
neu an — damit riss sie sie **allen bereits erzeugten Baeumen** unter dem
Modifier weg. Gemessen: nach vier Arten-Erzeugungen lebte nur noch die letzte,
alle frueheren evaluierten leer. Dass in der Praxis direkt nach dem Erzeugen
exportiert wurde, hat das lange verdeckt.
Jetzt prueft ein Waechter zwei Dinge, bevor er ueberhaupt neu baut: die
abgelegte **Schema-Version** (`TREE_SCHEMA`) *und* die tatsaechlich vorhandenen
Eingaenge. Die Version allein reichte nicht — wer waehrend der Entwicklung
Sockets aendert, ohne die Zahl anzufassen, bekaeme sonst eine veraltete Gruppe
untergeschoben. Muss doch neu gebaut werden, werden **bestehende Modifier auf
die neue Gruppe umgehaengt**. Beide Pfade sind gemessen: vier Arten
nacheinander leben alle, und nach einem erzwungenen `build_group(rebuild=True)`
ebenfalls.
### Saemlinge: Mindestradius sticht McMahon
Die Skalierung nach McMahon (Durchmesser ~ Hoehe^1.5) macht duennstaemmige
Arten am Setzling zu Draehten: gemessen kam die Birke auf **0.6 cm** Fussradius
gegen **1.9 cm** der Eiche — dreimal duenner, waehrend der Unterschied beim
Altbaum nur 1.2x betraegt. Die Regel gilt fuer die Statik ausgewachsener Baeume;
ein Saemling ist eine Pflanze, kein Balken.
`SEEDLING_MIN_RADIUS` und `SEEDLING_MIN_FLARE` heben die duennen Arten an, ohne
die dicken (Tanne, Palme, Kaktus) anzufassen. Danach liegen alle bei **2.8 cm**.
`tests/test_proportionen.py` laesst die McMahon-Abweichung genau dort zu, wo der
Boden greift — und prueft, dass sie nicht weiter geht.
### Faell-Teile (Punkt 4)
Kein Laufzeit-Zerlegen: beim Faellen tauscht das Spiel die ISM-Instanz gegen
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_tree_generator"
version = "1.34.0"
version = "1.35.0"
name = "Stylized Tree Generator"
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
maintainer = "D4rkst3r"
+3 -3
View File
@@ -50,17 +50,17 @@
<td>34.1KB</td>
</tr>
<tr>
<td><tt><a href="./stylized_tree_generator-1.34.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.34.0</a></tt></td>
<td><tt><a href="./stylized_tree_generator-1.35.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.35.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>40.0KB</td>
<td>41.2KB</td>
</tr>
</table>
<center><p>Built 2026-08-06, 08:32</p></center>
<center><p>Built 2026-08-06, 09:08</p></center>
</body>
</html>
+4 -4
View File
@@ -82,7 +82,7 @@
"id": "stylized_tree_generator",
"name": "Stylized Tree Generator",
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
"version": "1.34.0",
"version": "1.35.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -98,9 +98,9 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_tree_generator-1.34.0.zip",
"archive_size": 40964,
"archive_hash": "sha256:1cb548b5fef9bb79342ef92f7bc40f7ecb66e21f6356c8e00623ada5c842b0d6"
"archive_url": "./stylized_tree_generator-1.35.0.zip",
"archive_size": 42185,
"archive_hash": "sha256:db4de9904c876c1336879fe4de7a183f1a1f2c7dc52c55e8278e0bf7e43bd323"
}
]
}
Binary file not shown.
Binary file not shown.
+73 -5
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Tree Generator",
"author": "D4rkst3r",
"version": (1, 34, 0),
"version": (1, 35, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -222,11 +222,53 @@ def _out(node, *names):
return node.outputs[0]
def build_group():
old = bpy.data.node_groups.get(GROUP_NAME)
if old:
bpy.data.node_groups.remove(old)
# Schema-Version der Baum-Gruppe. HOCHZAEHLEN, sobald sich Sockets aendern -
# daran erkennt build_group, ob eine vorhandene Gruppe noch passt.
TREE_SCHEMA = 3
def _gruppe_passt(ng):
"""Ist eine vorhandene GN_Tree-Gruppe noch die aktuelle?
Zwei Kriterien: die abgelegte Schema-Version UND die tatsaechlich
vorhandenen Eingaenge. Die Version allein reichte nicht - wer waehrend der
Entwicklung Sockets aendert, ohne die Zahl anzufassen, bekaeme eine
veraltete Gruppe untergeschoben.
"""
if ng.get("schema") != TREE_SCHEMA:
return False
vorhanden = {so.name for so in ng.interface.items_tree
if getattr(so, "in_out", "") == 'INPUT'}
return set(DEFAULTS).issubset(vorhanden)
def build_group(rebuild=False):
"""Baut die Baum-Node-Gruppe - aber nur, wenn noetig.
FALLE, die das hier noetig macht: die Gruppe bedingungslos zu loeschen und
neu anzulegen reisst sie ALLEN bereits erzeugten Baeumen unter dem Modifier
weg. Gemessen: nach vier Arten-Erzeugungen lebte nur noch die letzte, alle
frueheren evaluierten leer. Dass der Export direkt nach dem Erzeugen lief,
hat das lange verdeckt.
Wenn doch neu gebaut werden muss (Schema-Aenderung), werden bestehende
Modifier auf die neue Gruppe umgehaengt - sonst zeigen sie auf ein totes
Datablock.
"""
alt = bpy.data.node_groups.get(GROUP_NAME)
if alt is not None and not rebuild and _gruppe_passt(alt):
return alt
nutzer = []
if alt is not None:
for ob in bpy.data.objects:
for md in ob.modifiers:
if getattr(md, "node_group", None) is alt:
nutzer.append(md)
bpy.data.node_groups.remove(alt)
ng = bpy.data.node_groups.new(GROUP_NAME, "GeometryNodeTree")
ng["schema"] = TREE_SCHEMA
N, L = ng.nodes.new, ng.links.new
iface = ng.interface
@@ -1042,6 +1084,15 @@ def build_group():
shade = N("GeometryNodeSetShadeSmooth"); shade.location = (2200, 200)
L(_out(pick, "Output"), _sock(shade, "Geometry"))
L(_out(shade, "Geometry"), gout.inputs[0])
# Bestehende Modifier auf die NEUE Gruppe umhaengen. Ohne das zeigen sie
# nach einem Rebuild auf ein geloeschtes Datablock und der Baum evaluiert
# leer - der Fehler, der ueberhaupt zu diesem Waechter gefuehrt hat.
for md in nutzer:
try:
md.node_group = ng
except (ReferenceError, AttributeError):
pass
return ng
@@ -1050,6 +1101,17 @@ def build_group():
# ein Wachstums-System, damit Stufe 1 und Stufe 4 wie DERSELBE Baum wirken.
GROWTH_HEIGHT = 0.16 # Setzling ist 16 % der Endhoehe
# Mindest-Stammradius und Mindest-Wurzelanlauf fuer den Saemling.
#
# Die McMahon-Skalierung (Durchmesser ~ Hoehe^1.5) macht duennstaemmige Arten am
# Setzling zu Draehten: gemessen kam die Birke auf 0.6 cm Fussradius gegen 1.9
# der Eiche - dreimal duenner, waehrend der Unterschied beim Altbaum nur 1.2x
# betraegt. Die Regel gilt fuer die STATIK ausgewachsener Baeume; ein Saemling
# ist eine Pflanze, kein Balken. Der Boden hebt die duennen Arten an, ohne die
# dicken (Tanne, Palme, Kaktus) anzufassen.
SEEDLING_MIN_RADIUS = 0.018 # m, Stammradius am Fuss vor dem Wurzelanlauf
SEEDLING_MIN_FLARE = 0.55 # Wurzelanlauf-Faktor, sonst steht der Stiel stumpf
GROWTH_YOUNG = {
"Height": GROWTH_HEIGHT, # Faktoren (werden mit dem Zielwert multipliziert)
@@ -1128,6 +1190,12 @@ def growth_values(preset, t):
target = full[key]
young = target * young_factor
v = young + (target - young) * t
# Boden fuer die Saemlings-Stufen anwenden, bevor gerundet wird.
if key == "Trunk Radius":
v = max(v, min(target, SEEDLING_MIN_RADIUS))
elif key == "Root Flare":
v = max(v, min(target, SEEDLING_MIN_FLARE))
if isinstance(target, int):
# Ein Faktor von genau 0.0 heisst "gibt es beim Setzling noch gar
# nicht" und darf auf 0 fallen. Alles andere bekommt mindestens 1,
+16 -3
View File
@@ -12,6 +12,9 @@ belegt - deshalb lohnt es sich, sie festzunageln:
* Setzling-Schlankheit: elastische Aehnlichkeit nach McMahon, Hoehe ~
Durchmesser^(2/3). Der Setzling darf relativ zu seiner Hoehe nicht dicker
sein als der Altbaum. Vorher war er es um den Faktor 2.4.
AUSNAHME: wo der Mindest-Stammradius greift, ist die Abweichung gewollt -
McMahon gilt fuer die Statik ausgewachsener Baeume, ein Saemling ist eine
Pflanze und kein Balken.
* Palme und Kaktus sind davon ausgenommen: Palmen haben kein Dickenwachstum
("its diameter is fixed for life", UF/IFAS ENH1212), Kakteen sind Sukkulenten.
Beide bauen zuerst den Durchmesser auf - deren Setzling ist absichtlich
@@ -107,10 +110,20 @@ for p in tg.PRESETS:
continue
schlank_alt = r_alt / h_alt
schlank_neu = r_neu / h_neu
if schlank_neu > schlank_alt * 1.5:
# AUSNAHME: der Mindest-Stammradius fuer Saemlinge sticht McMahon. Die Regel
# gilt fuer die Statik ausgewachsener Baeume; duennstaemmige Arten wurden am
# Setzling sonst zu Draehten (Birke 0.6 cm Fussradius gegen 1.9 der Eiche,
# dreimal duenner, waehrend der Unterschied beim Altbaum nur 1.2x betraegt).
# Wo der Boden greift, ist die Abweichung gewollt - geprueft wird dann nur,
# dass sie NICHT weiter geht als der Boden hergibt.
am_boden = r_neu <= tg.SEEDLING_MIN_RADIUS * 1.05
grenze = 2.5 if am_boden else 1.5
if schlank_neu > schlank_alt * grenze:
fails.append("%s: Setzling ist relativ %.1fx dicker als der Altbaum "
"(r/H %.4f gegen %.4f) - McMahon verlangt schlanker"
% (p, schlank_neu / schlank_alt, schlank_neu, schlank_alt))
"(r/H %.4f gegen %.4f)%s"
% (p, schlank_neu / schlank_alt, schlank_neu, schlank_alt,
" - mehr als der Mindestradius hergibt" if am_boden
else " - McMahon verlangt schlanker"))
# --- 5) Kein Setzling haengt nennenswert unter den Wurzelansatz -----------
# Wedel und Aeste, die unter den Ursprung reichen, lassen das Asset in UE