diff --git a/README.md b/README.md index ad0caad..b69a781 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 424a455..0b45f60 100644 --- a/blender_manifest_tree.toml +++ b/blender_manifest_tree.toml @@ -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" diff --git a/dist/index.html b/dist/index.html index 39878ea..0999c73 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 21.7KB - stylized_tree_generator-1.24.0 + stylized_tree_generator-1.25.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 21.8KB + 24.1KB -

Built 2026-07-31, 10:09

+

Built 2026-08-01, 20:46

diff --git a/dist/index.json b/dist/index.json index a5e52c1..3a3577b 100644 --- a/dist/index.json +++ b/dist/index.json @@ -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" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.24.0.zip b/dist/stylized_tree_generator-1.24.0.zip deleted file mode 100644 index 9ea0c08..0000000 Binary files a/dist/stylized_tree_generator-1.24.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.25.0.zip b/dist/stylized_tree_generator-1.25.0.zip new file mode 100644 index 0000000..c09c9ee Binary files /dev/null and b/dist/stylized_tree_generator-1.25.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 364915d..5460413 100644 --- a/stylized_tree_generator.py +++ b/stylized_tree_generator.py @@ -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 diff --git a/tests/test_proportionen.py b/tests/test_proportionen.py new file mode 100644 index 0000000..38b0c36 --- /dev/null +++ b/tests/test_proportionen.py @@ -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")