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>
160 lines
6.5 KiB
Python
160 lines
6.5 KiB
Python
"""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.
|
|
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
|
|
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
|
|
# 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)%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
|
|
# 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")
|