"""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")