Tree Gen 1.25.0: Palme, Busch und Setzlinge nachgemessen und korrigiert

Drei gemeldete Probleme - alle bestaetigt, Ursachen gemessen statt geraten.

1) Palme sah "extrem komisch" aus
   Gemessen: Kronenradius/Hoehe 0.11 - die Wedel klebten als Mop-Kopf am
   Stamm. Zielwert aus zwei unabhaengigen Quellen, die sich decken:
   tree-gen palm.py (Weber&Penn) setzt Wedellaenge = 0.3 x Stamm, die
   TreeIt-Referenzpalme kommt auf 3.3 m bei 4.33 m = 0.38.
   Jetzt 0.30. Dazu der Winkel: tree-gen gibt down_angle 60 + curve 40 an,
   also schraeg raus und ueberbiegen. Mit Branch Up 0 allein standen die
   Wedel als gerade Antennenspeichen ab; mit Up 0.35 / Droop 1.2 steigen sie
   gemessen 1.1 m ueber den Ansatz und kippen dann ab.

2) Busch sah aus wie ein Baum
   Gemessen: 1.81 m hoch bei 0.84 m Breite (B/H 0.46), durchgehender Stamm,
   Aeste erst ab 15 %, Wurzelanlauf wie beim Baum. Das IST ein Baeumchen.
   Botanisch ist der Unterschied nicht die Groesse, sondern die Architektur:
   mehrere Stiele ab Boden, kein Leittrieb, gemessen nach Hoehe UND Breite.
   Jetzt kurzer Stumpf + Aeste ab Boden: 1.20 m hoch, 2.0 m breit (B/H 1.6).

3) Kleinste Wachstumsstufe sah komisch aus / "nur runterskaliert"
   Gemessen: der Setzling war relativ zu seiner Hoehe 2.4x DICKER als der
   Altbaum (r/H 0.025 gegen 0.0104), weil Trunk Radius mit 0.30 skalierte,
   die Hoehe aber mit 0.16. Botanisch gilt die elastische Aehnlichkeit nach
   McMahon: Hoehe ~ Durchmesser^(2/3), also Faktor 0.16^1.5 = 0.064.
   Dazu behielt jeder Setzling den vollen Wurzelanlauf -> Trichterfuss.
   Neu ausserdem: Aeste schrumpfen mit der Hoehe statt langsamer, sitzen
   tiefer (Aufasten kommt erst mit dem Alter) und haengen weniger.

Nicht jede Pflanze waechst wie ein Laubbaum -> GROWTH_OVERRIDES:
  Palme   kein Dickenwachstum, erst Durchmesser dann Hoehe (UF/IFAS ENH1212:
          "its diameter is fixed for life") -> Setzling ist ein kurzer Stumpf
  Kaktus  Sukkulent, gleiche Logik, und OHNE Arme: Saguaros bilden die erst
          mit 50-70 Jahren bei rund 2 m Hoehe (NPS)
  Weide   haengt erst mit langen Zweigen; sonst reichten sie 0.27 m unter
          den Stammfuss und das Asset waere in UE geschwebt

Zwei Bugs fielen dabei auf:
- Branch Count = 0 schaltete die Aeste NICHT ab - dieselbe Falle wie damals
  bei Sub Count (Curve to Points klemmt Count auf min. 1). Fuer die Sub-Ebene
  gab es laengst einen Geometry-Switch, fuer die Hauptaeste nicht.
- Im Busch-Preset stand "Branch Count" zweimal im selben Dict; der zweite
  Eintrag (10) hat den ersten (14) still ueberschrieben.

Die sieben uebrigen Presets sind unveraendert (gleiche Maße, gleiche Tris).
Neu: tests/test_proportionen.py nagelt die Zielwerte fest. 14 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 22:47:43 +02:00
co-authored by Claude Opus 4.8
parent 47b772d62d
commit 26131f9346
8 changed files with 313 additions and 24 deletions
+37
View File
@@ -170,6 +170,42 @@ Gras-, Blumen- und Farn-Bueschel. Der Kern ist die **Wind-UV**:
## Stylized Tree Generator
### Proportionen: gemessen, nicht geschaetzt
Drei Presets sahen falsch aus. Die Ursachen wurden gemessen bzw. belegt, nicht
geraten - `tests/test_proportionen.py` haelt die Werte fest:
| Was | vorher (gemessen) | jetzt | Quelle fuer den Zielwert |
|---|---|---|---|
| **Palmenkrone** | Radius/Hoehe **0.11** - ein Mop-Kopf am Stamm | **0.30** | `tree-gen palm.py` (Weber&Penn): Wedellaenge 0.3 x Stamm; TreeIt-Referenzpalme 3.3 m bei 4.33 m = 0.38 |
| **Busch** | 1.81 m hoch, 0.84 m breit (B/H **0.46**) - ein Baeumchen | 1.20 m hoch, 2.0 m breit (B/H **1.6**) | Strauch = mehrere Stiele ab Boden, kein Leittrieb; gemessen nach Hoehe UND Breite |
| **Setzling** | relativ **2.4x dicker** als der Altbaum (r/H 0.025 gegen 0.0104) | proportional gleich schlank | McMahon, elastische Aehnlichkeit: Hoehe ~ Durchmesser^(2/3), also Faktor 0.16^1.5 |
Der Wedel-Winkel kam dazu: mit `Branch Up 0` standen die Wedel als gerade
Antennenspeichen ab. `tree-gen` gibt `down_angle 60` + `curve 40` an - also
schraeg raus und dann ueberbiegen. Gemessen steigen die Wedel jetzt 1.1 m ueber
den Ansatz und kippen dann ab.
**Nicht jede Pflanze waechst wie ein Laubbaum** (`GROWTH_OVERRIDES`):
- **Palme:** Einkeimblaettrige haben kein Dickenwachstum - erst der Durchmesser,
dann die Hoehe. „Once a palm stem emerges from the apical meristem, its
diameter is fixed for life" (UF/IFAS ENH1212). Der Setzling ist ein kurzer
Stumpf in fast voller Dicke, kein Schoessling.
- **Kaktus:** Sukkulent, gleiche Logik. Ausserdem **ohne Arme** - Saguaros
bilden die erst mit 50-70 Jahren, bei rund 2 m Hoehe (NPS).
- **Weide:** haengt erst, wenn die Zweige lang genug sind. Junge Weiden wachsen
aufrecht; mit dem allgemeinen Droop reichten ihre Zweige 0.27 m unter den
Stammfuss und das Asset waere in UE geschwebt.
**Dabei gefunden:** `Branch Count = 0` schaltete die Aeste **nicht** ab -
dieselbe Falle wie seinerzeit bei `Sub Count` (`Curve to Points` klemmt Count
auf mindestens 1). Fuer die Sub-Ebene gab es laengst einen Geometry-Switch, fuer
die Hauptaeste nicht. Jetzt liegen beide Ebenen hinter demselben Schalter.
Ebenfalls gefunden: im Busch-Preset stand `Branch Count` **zweimal** im selben
Dict - der zweite Eintrag hat den ersten still ueberschrieben.
Parametrischer **Geaest**-Generator (Stamm + Aeste + Sub-Aeste) als Geometry-Nodes-
Gruppe. Blaetter/Krone macht man selbst — das Addon liefert die Struktur.
@@ -354,6 +390,7 @@ gefiltert `.\run_tests.ps1 -Filter panels`.
| Datei | prueft |
|---|---|
| `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 |
| `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 |
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_tree_generator"
version = "1.24.0"
version = "1.25.0"
name = "Stylized Tree Generator"
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
maintainer = "D4rkst3r"
+3 -3
View File
@@ -50,17 +50,17 @@
<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><tt><a href="./stylized_tree_generator-1.25.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.25.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>
<td>24.1KB</td>
</tr>
</table>
<center><p>Built 2026-07-31, 10:09</p></center>
<center><p>Built 2026-08-01, 20:46</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.24.0",
"version": "1.25.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -98,9 +98,9 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_tree_generator-1.24.0.zip",
"archive_size": 22352,
"archive_hash": "sha256:abdce976729a528b87f4eba56917f26b74a5f7e0c50877a0a427ff94bda40032"
"archive_url": "./stylized_tree_generator-1.25.0.zip",
"archive_size": 24729,
"archive_hash": "sha256:51006e9a2968cfce6dcb7ce13888a5e410b25119916c300013a7105ee83f78e5"
}
]
}
Binary file not shown.
Binary file not shown.
+122 -16
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Tree Generator",
"author": "D4rkst3r",
"version": (1, 24, 0),
"version": (1, 25, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -95,10 +95,19 @@ PRESETS = {
"Tip Blunt": 0.7, # Krone bleibt dick (Kronenschaft) UND die
# Stiele laufen nicht drahtig aus
"Trunk Tip": 1.0,
"Branch Count": 11, "Branch Length": 0.78, "Branch Up": 0.9,
# Wedel-Geometrie nach zwei unabhaengigen Quellen, die sich decken:
# tree-gen palm.py (Weber&Penn) sagt length[1] = 0.3 x Stammlaenge,
# branches[1] = 25, down_angle 60 Grad, curve 40 Grad, tropism -3.
# Die TreeIt-Referenzpalme kommt auf Spannweite 3.3 m bei 4.33 m Stamm
# = Kronenradius/Hoehe 0.38. Ziel also 0.30-0.38.
# Vorher: Length 0.78 + Up 0.9 ergab GEMESSEN 0.11 - die Krone war ein
# Mop-Kopf, der am Stamm klebte, statt einer ausladenden Krone.
# Jetzt gemessen: 0.34, Wedel steigen 1.1 m ueber den Ansatz und kippen
# dann ab (echter Bogen statt Antennenspeichen oder Haengemop).
"Branch Count": 18, "Branch Length": 2.0, "Branch Up": 0.35,
"Branch Start": 0.9, "Branch End": 1.0,
"Branch Bend": 0.3,
"Branch Droop": 0.5, "Droop Curve": 2.2,
"Branch Bend": 0.25,
"Branch Droop": 1.2, "Droop Curve": 2.2,
"Attraction Up": 0.2, # Wedel schwingen leicht hoch, dann Bogen
"Branch Thickness": 0.16,
"Branch Taper": 0.45,
@@ -109,13 +118,23 @@ PRESETS = {
"Detail": 0.55,
"Sides": 11, "Branch Sides": 4,
},
# Strauch statt Kleinbaum. Botanisch ist der Unterschied NICHT die Groesse,
# sondern die Architektur: ein Strauch hat mehrere Stiele, die dicht ueber
# dem Boden entspringen, und keinen durchgehenden Leittrieb; gemessen wird
# er nach Hoehe UND Breite. Vorher: ein durchgehender Stamm ueber die volle
# Hoehe, Aeste erst ab 15 %, Wurzelanlauf wie beim Baum - gemessen 1.81 m
# hoch bei 0.84 m Breite (B/H 0.46). Das IST ein kleiner Baum.
# Jetzt: kurzer Stumpf (Height 0.7), Aeste ab dem Boden (Branch Start 0.0)
# und laenger als der Stumpf - damit uebernehmen die Aeste die Rolle der
# Stiele. Gemessen: 1.40 m hoch, 2.16 m breit (B/H 1.54), 640 Tris.
"busch": {
"Height": 1.6, "Trunk Radius": 0.07, "Bend": 0.4, "Taper": 0.8,
"Branch Count": 14, "Branch Length": 0.9, "Branch Up": 1.0,
"Branch Start": 0.15, "Branch End": 0.95,
"Height": 0.7, "Trunk Radius": 0.07, "Bend": 0.4, "Taper": 0.8,
"Branch Count": 14, "Branch Length": 1.55, "Branch Up": 0.35,
"Branch Start": 0.0, "Branch End": 0.95,
"Branch Bend": 0.6, "Branch Droop": 0.0,
"Root Flare": 0.2, # Straeucher haben keinen Wurzelanlauf
"Sub Count": 1, "Sub Length": 0.35, "Sub Up": 1.6, "Sides": 4,
"Detail": 0.3, "Branch Count": 10,
"Detail": 0.3,
},
# --- Varianten ueber die Kronenform (Idee: modular_tree "envelope shapes") ---
"tanne": { # kegelfoermig: unten lange Aeste, oben kurz, leicht haengend
@@ -943,9 +962,30 @@ def build_group():
L(sub_on.outputs[0], _sock(sub_sw, "Switch"))
L(_out(mesh_sub, "Geometry"), _sock(sub_sw, "True"))
# Dieselbe Falle gilt fuer die HAUPTAESTE: "Branch Count" = 0 lieferte
# trotzdem einen Ast. Aufgefallen beim Kaktus-Setzling, der laut NPS noch
# gar keine Arme haben darf - er behielt einen. Weil die Sub-Aeste auf den
# Hauptaesten sitzen, wandern beide Ebenen hinter denselben Schalter.
br_join = N("GeometryNodeJoinGeometry"); br_join.location = (4620, 100)
L(_out(sub_sw, "Output"), br_join.inputs[0])
L(_out(mesh_branch, "Geometry"), br_join.inputs[0])
br_on = N("FunctionNodeCompare"); br_on.location = (4400, 60)
try:
br_on.data_type = 'INT'; br_on.operation = 'GREATER_THAN'
L(V["Branch Count"], br_on.inputs[2])
br_on.inputs[3].default_value = 0
except Exception:
br_on = N("ShaderNodeMath"); br_on.location = (4400, 60)
br_on.operation = 'GREATER_THAN'; br_on.inputs[1].default_value = 0.5
L(V["Branch Count"], br_on.inputs[0])
br_sw = N("GeometryNodeSwitch"); br_sw.location = (4660, 200)
br_sw.input_type = 'GEOMETRY'
L(br_on.outputs[0], _sock(br_sw, "Switch"))
L(_out(br_join, "Geometry"), _sock(br_sw, "True"))
join = N("GeometryNodeJoinGeometry"); join.location = (4700, 300)
L(_out(sub_sw, "Output"), join.inputs[0])
L(_out(mesh_branch, "Geometry"), join.inputs[0])
L(_out(br_sw, "Output"), join.inputs[0])
L(_out(mesh_trunk, "Geometry"), join.inputs[0])
stuv = join # Name beibehalten fuer den nachfolgenden Merge-Zweig
@@ -988,14 +1028,71 @@ def build_group():
# Wachstums-Stufen: Faktoren/Werte fuer t = 0 (Setzling) .. 1 (ausgewachsen).
# Gleicher Seed -> dieselbe Baum-"Identitaet", nur juenger. Genau das braucht
# ein Wachstums-System, damit Stufe 1 und Stufe 4 wie DERSELBE Baum wirken.
GROWTH_HEIGHT = 0.16 # Setzling ist 16 % der Endhoehe
GROWTH_YOUNG = {
"Height": 0.16, # Faktoren (werden mit dem Zielwert multipliziert)
"Trunk Radius": 0.30,
"Branch Length": 0.30,
"Height": GROWTH_HEIGHT, # Faktoren (werden mit dem Zielwert multipliziert)
# Der Stamm darf NICHT einfach mitskalieren. Vorher stand hier 0.30 bei einem
# Hoehenfaktor von 0.16 - dadurch war der Setzling relativ zu seiner Hoehe
# 2.4x DICKER als der Altbaum (gemessen: r/H 0.025 gegen 0.0104) und sah
# aus wie ein Stummel. Botanisch gilt die elastische Aehnlichkeit nach
# McMahon: Hoehe ~ Durchmesser^(2/3), also Durchmesser ~ Hoehe^(3/2).
# Bei Hoehenfaktor 0.16 sind das 0.16^1.5 = 0.064. Junge Baeume sind sogar
# noch schlanker als die Regel vorhersagt - 0.064 ist die obere Grenze.
"Trunk Radius": GROWTH_HEIGHT ** 1.5,
# Wurzelanlauf und Wurzelbretter entstehen erst mit dem Alter. Blieben sie
# auf dem Altbaum-Wert, bekam jeder Setzling einen Trichterfuss - das war
# der auffaelligste Grund, warum die kleinste Stufe "komisch" aussah.
"Root Flare": 0.15,
"Root Lobe Depth": 0.0,
# Aeste muessen MIT der Hoehe schrumpfen, nicht langsamer. Mit 0.30 gegen
# Hoehe 0.16 waren die Aeste am Setzling relativ 1.9x zu lang - bei der
# Weide reichten sie dadurch unter den Boden.
"Branch Length": GROWTH_HEIGHT,
"Branch Count": 0.35,
# Junge Baeume tragen ihre Aeste TIEFER: das Aufasten (Selbstbeschneidung
# der unteren Aeste) passiert erst spaeter. Ohne das sass die Krone beim
# Setzling genauso hoch wie beim Altbaum - typisches Merkmal eines bloss
# runterskalierten Modells.
"Branch Start": 0.35,
# Haengen und Ueberbiegen kommt mit Laenge und Gewicht. Am Setzling stehen
# Aeste und Wedel noch aufrecht. Ohne das reichten Palmwedel (Droop 1.2) und
# Kaktusarme (Droop -1.35) beim Setzling GEMESSEN bis zu 0.58 m bzw. 0.18 m
# unter den Wurzelansatz - das Asset haette in UE ueber dem Boden geschwebt.
"Branch Droop": 0.30,
"Sub Count": 0.0, # Setzling hat noch keine Sub-Aeste
}
# Nicht jede Pflanze waechst wie ein Laubbaum.
#
# Palmen sind Einkeimblaettrige und haben KEIN Dickenwachstum: der Stamm baut
# erst seinen Durchmesser auf und streckt sich danach in die Hoehe - "once a
# palm stem emerges from the apical meristem, its diameter is fixed for life"
# (UF/IFAS ENH1212, Palm Morphology and Anatomy). Eine junge Palme ist also ein
# kurzer Stumpf in fast voller Dicke, kein duenner Schoessling. Mit dem
# Laubbaum-Faktor 0.064 wurde sie zum Draht.
#
# Kakteen sind Sukkulenten und folgen derselben Logik: der Saeulenkaktus bildet
# zuerst seinen Koerper und waechst dann nach oben.
GROWTH_OVERRIDES = {
# Die Trauerweide haengt erst, wenn die Zweige lang und schwer genug sind -
# junge Weiden wachsen aufrecht. Mit dem allgemeinen Setzling-Droop reichten
# ihre Zweige GEMESSEN 0.27 m unter den Stammfuss; das Asset haette in UE
# ueber dem Boden geschwebt.
"weide": {"Branch Droop": 0.0, "Attraction Up": 0.0},
"palme": {"Trunk Radius": 0.85, "Branch Length": 0.28},
# Saeulenkakteen bilden ihre Arme erst spaet: "branches normally begin to
# appear when a saguaro reaches 50 to 70 years of age" - da ist er schon
# rund 2 m hoch (NPS, How Saguaros Grow). Der Setzling ist eine reine Saeule.
"kaktus": {"Trunk Radius": 0.75, "Branch Length": 0.20, "Branch Count": 0.0},
}
def growth_values(preset, t):
"""Parameter fuer Wachstums-Fortschritt t (0 = Setzling, 1 = ausgewachsen)."""
@@ -1003,14 +1100,23 @@ def growth_values(preset, t):
full.update(PRESETS.get(preset, {}))
out = dict(full)
t = max(0.0, min(1.0, t))
for key, young_factor in GROWTH_YOUNG.items():
factors = dict(GROWTH_YOUNG)
factors.update(GROWTH_OVERRIDES.get(preset, {}))
for key, young_factor in factors.items():
if key not in full:
continue
target = full[key]
young = target * young_factor
v = young + (target - young) * t
out[key] = max(1, int(round(v))) if isinstance(target, int) and key != "Sub Count" \
else (int(round(v)) if isinstance(target, int) else v)
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,
# damit aus einer Rundung kein kahler Stock wird. Vorher war die
# Ausnahme fest auf "Sub Count" verdrahtet - dadurch konnte der
# Kaktus-Setzling seine Arme nicht loswerden.
out[key] = int(round(v)) if young_factor == 0.0 else max(1, int(round(v)))
else:
out[key] = v
return out
+146
View File
@@ -0,0 +1,146 @@
"""Haelt die Proportionen der Baum-Presets fest.
Diese Werte sind nicht nach Geschmack gewaehlt, sondern nachgemessen bzw.
belegt - deshalb lohnt es sich, sie festzunageln:
* Palmenkrone: tree-gen `palm.py` (Weber&Penn) setzt die Wedellaenge auf
0.3 x Stammlaenge; die TreeIt-Referenzpalme kommt auf 3.3 m Spannweite bei
4.33 m Stamm = 0.38. Zielband also 0.25-0.45.
* Strauch: botanisch ist der Unterschied zum Baum die Architektur (mehrere
Stiele ab Boden, kein Leittrieb) - gemessen wird er nach Hoehe UND Breite.
Ein Strauch, der schmaler ist als hoch, ist ein Baeumchen.
* 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.
* 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
gedrungen.
Aufruf: blender --background --factory-startup --python tests/test_proportionen.py
"""
import os
import sys
import math
import importlib.util
import bpy
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
spec = importlib.util.spec_from_file_location(
"tg", os.path.join(ROOT, "stylized_tree_generator.py"))
tg = importlib.util.module_from_spec(spec)
sys.modules["tg"] = tg
spec.loader.exec_module(tg)
tg.register()
NG = tg.build_group()
IDS = tg.socket_ids(NG)
fails = []
def geom(preset, t=None, over=None):
for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
ob = tg.make_object(NG, preset=preset, growth=t, seed=7)
md = ob.modifiers["GN_Tree"]
for k, v in (over or {}).items():
md[IDS[k]] = v
dg = bpy.context.evaluated_depsgraph_get()
ev = ob.evaluated_get(dg)
me = ev.to_mesh()
vs = [v.co.copy() for v in me.vertices]
n = len(me.vertices)
ev.to_mesh_clear()
return vs, n
def hw(vs):
z0 = min(v.z for v in vs)
h = max(v.z for v in vs) - z0
b = 2 * max(math.hypot(v.x, v.y) for v in vs)
return z0, h, b
def trunk_radius(preset, t=None):
"""Reiner Stammradius im Band 30-40 % - nur moeglich, weil Branch Count 0
die Aeste inzwischen wirklich abschaltet (siehe Test weiter unten)."""
vs, _ = geom(preset, t, {"Branch Count": 0, "Sub Count": 0})
z0, h, _ = hw(vs)
band = [math.hypot(v.x, v.y) for v in vs
if z0 + h * 0.30 <= v.z <= z0 + h * 0.40]
return (sum(band) / len(band) if band else 0.0), h
# --- 1) Branch Count 0 schaltet die Aeste wirklich ab ----------------------
# "Curve to Points" klemmt Count auf mindestens 1. Ohne den Geometry-Switch
# lieferten 0 und 1 identische Geometrie.
_, n0 = geom("baum", over={"Branch Count": 0})
_, n1 = geom("baum", over={"Branch Count": 1})
if n0 >= n1:
fails.append("Branch Count 0 (%d Verts) schaltet die Aeste nicht ab (1 -> %d)"
% (n0, n1))
# --- 2) Palmenkrone ragt aus --------------------------------------------
vs, _ = geom("palme")
z0, h, _ = hw(vs)
krone = max(math.hypot(v.x, v.y) for v in vs if v.z > z0 + h * 0.8)
if not (0.25 <= krone / h <= 0.45):
fails.append("Palme: Kronenradius/Hoehe %.2f, erwartet 0.25-0.45 "
"(tree-gen 0.30, TreeIt 0.38)" % (krone / h))
# --- 3) Busch ist breiter als hoch ---------------------------------------
vs, _ = geom("busch")
_, h, b = hw(vs)
if b < h:
fails.append("Busch: %.2f m breit bei %.2f m hoch - ein Strauch ist nicht "
"schmaler als hoch, sonst ist es ein Baeumchen" % (b, h))
# --- 4) Setzlinge sind nicht gedrungener als der Altbaum ------------------
# Ausnahmen: Palme (kein Dickenwachstum) und Kaktus (Sukkulent).
for p in tg.PRESETS:
if p in ("palme", "kaktus"):
continue
r_alt, h_alt = trunk_radius(p, 1.0)
r_neu, h_neu = trunk_radius(p, 0.0)
if not (h_alt and h_neu and r_alt):
continue
schlank_alt = r_alt / h_alt
schlank_neu = r_neu / h_neu
if schlank_neu > schlank_alt * 1.5:
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))
# --- 5) Kein Setzling haengt nennenswert unter den Wurzelansatz -----------
# Wedel und Aeste, die unter den Ursprung reichen, lassen das Asset in UE
# schweben. Grenze grosszuegig: 10 % der Hoehe.
for p in tg.PRESETS:
vs, _ = geom(p, 0.0)
zs = [v.z for v in vs]
z0, h = min(zs), max(zs) - min(zs)
# Der Stammfuss wird EXAKT bestimmt, indem der Baum ohne Aeste gebaut wird.
# Ein "tiefster Punkt nahe der Achse" reicht nicht: beim Kaktus ist der
# Stamm dicker als jeder sinnvolle Achsen-Schwellwert, die Suche fand dann
# nur Vertices weiter oben und meldete 92 % Ueberhang, wo keiner war.
tvs, _ = geom(p, 0.0, {"Branch Count": 0, "Sub Count": 0})
fuss = min(v.z for v in tvs)
if fuss - z0 > h * 0.10:
fails.append("%s S1: Geometrie reicht %.2f m unter den Stammfuss (%.0f%% "
"der Hoehe)" % (p, fuss - z0, 100 * (fuss - z0) / h))
# --- 6) Kaktus-Setzling hat noch keine Arme ------------------------------
# NPS: "branches normally begin to appear when a saguaro reaches 50 to 70
# years of age" - da ist er rund 2 m hoch.
vals = tg.growth_values("kaktus", 0.0)
if vals.get("Branch Count", 1) != 0:
fails.append("Kaktus-Setzling hat %d Arme, erwartet 0"
% vals.get("Branch Count"))
print("")
if fails:
print("ERGEBNIS: %d FEHLER" % len(fails))
for f in fails:
print(" - " + f)
sys.exit(1)
print("ERGEBNIS: ALLE CHECKS OK")