Vom User im Wald abgenommen: "sehr schoen etwas weniger blaetter und wir
koennen das fuer alle baeume uebernehmen".
1) KRONEN-HUELLE statt Einzelkappen
Kugeln auf die Aeste, Mesh to Volume, Volume to Mesh, dann Streuung auf der
verschmolzenen Oberflaeche - der Standard-Ablauf aus den
Produktions-Breakdowns, den wir uebersprungen hatten. Die Aeste verschwinden
im Inneren der Krone; die Polster sind nicht mehr als getrennte Koerper
sichtbar.
Drei Befunde, alle gemessen:
- ast_id kam nur an 3 von 10 Aesten an. Nicht das Sampling, sondern die
Eingabe: nur VIER Cluster-Zentren auf drei Aesten. Sichtbar erst mit
Points to Vertices - to_mesh() auf eine Punktwolke liefert eine leere Mesh
und misst nichts.
- S1 stand kahl. Reihe ueber die Stufen 0/20/41/109 Zentren: allmaehlicher
Abfall, keine Klippe, also die Cluster-Dichte. "Dichte" ist Kugeln je m2
Zweigflaeche, die schrumpft quadratisch - sie laeuft jetzt mit 1/g^2
gegen die Groesse, damit die ANZAHL konstant bleibt. Skalenfrei, kein
Sonderweg fuer Setzlinge.
- 31 % der Cards lagen unter dem Kronenansatz: die Verschmelzung weitet die
Form nach unten, der Filter griff nur auf den Zentren. Jetzt auch auf der
Huelle.
2) WENIGER BLAETTER (-15 % auf die Stufen-Ziele)
Gemessen an der Eiche: 381 -> 319 Cards, Deckung 10.4 -> 8.6, 372 Tris
gespart. Der Gegenversuch - Cards um 15 % verkleinern - kostet MEHR Deckung
(7.9, die Flaeche geht quadratisch) und spart kein Dreieck.
3) FORMBAND um das Abgenommene gelegt, nicht umgekehrt
Alle Referenzformen unter der neuen Bauform neu gemessen:
Eiche u/m 1.04-1.16 o/m 0.79-0.91
abgenommen u/m 0.78-1.02 o/m 0.68-0.79
Trichter u/m 0.68-0.78 o/m 1.04-1.14
Pilz u/m 0.59-0.90 o/m 0.82-0.95
Haube u/m 0.94-1.03 o/m 0.71-0.78
u/m TRENNT NICHTS MEHR - der abgenommene Bereich ueberlappt den Pilz und
stoesst an den Trichter. Die alte 0.82-Schranke stammt aus der
Einzelkappen-Aera; dagegen zu kalibrieren haette die Krone verbogen, um eine
Zahl zu retten, die ihre Aussagekraft verloren hat. u/m wird nur noch
mitgedruckt. o/m trennt sauber und traegt die Formpruefung allein.
27 Tests bestehen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
461 lines
20 KiB
Python
461 lines
20 KiB
Python
"""Obstbaum: Kronenform, Deckung und ein Kronenradius-Boden.
|
|
|
|
ZUR HERKUNFT DES RADIUS-KRITERIUMS, damit es nicht als falsche Zusicherung
|
|
weiterlebt (Stand 2026-08-06, von der Spielseite klargestellt):
|
|
|
|
Urspruenglich war "Kronenradius >= 1.80 m" eine TECHNISCHE Voraussetzung. Die
|
|
Frucht-Streuung schob die Fruechte mit einem festen Radial-Schub (0.24 m) ueber
|
|
die Card-Schale hinaus; bei zu kleiner Krone haette dieser feste Betrag sie nach
|
|
draussen geschoben. Am generischen "baum" lag der Kronenradius bei 0.78 m gegen
|
|
1.28 m Card-Diagonale - die Cards waren groesser als die Krone.
|
|
|
|
Diese Kopplung ist WEG. Seit dem Umbau auf die Bueschel-Schalen arbeitet die
|
|
Streuung mit einer Huellen-Tabelle (16 Winkel-Sektoren x 6 Hoehenbaender) und
|
|
setzt die Fruechte auf 94 % der LOKALEN Huelle - vollstaendig relativ, ohne
|
|
absolute Distanz, damit unabhaengig von der Kronengroesse.
|
|
|
|
Das Kriterium bleibt trotzdem, aber als LOOK- UND GAMEPLAY-BODEN: unter einem
|
|
Obstbaum soll man ernten koennen. Und als stabile Groesse - max ueber die RINDE
|
|
ist von den Schalen unberuehrt, waehrend jedes Blatt-Mass mit der Bauform
|
|
wandert. Auf ein Blatt-Perzentil umzustellen wuerde die Zahl still umdeuten und
|
|
zwei Kalibrier-Runden neu aufmachen, ohne etwas zu gewinnen.
|
|
|
|
Was die Streuung tatsaechlich braucht, ist Laub in ausreichend vielen
|
|
Winkel-Sektoren - eine einseitige Krone wuerde die Fruechte verklumpen. Das ist
|
|
derzeit ueberall erfuellt (Kronen-Exzentrik 3-13 % des Radius) und deshalb hier
|
|
nicht geprueft.
|
|
|
|
Vorgaben der Spielseite, die mitgeprueft werden:
|
|
Kronenansatz ~30 %, Kronenradius S4 >= 1.8 m, Hoehe 4-5 m,
|
|
Breitenprofil + Kugel-Kennwert (Lollipop: Kronenhoehe ~ Kronenbreite),
|
|
Deckung gegen die Eiche.
|
|
B/H gilt fuer dieses Preset NICHT - siehe Kommentar unten.
|
|
|
|
Aufruf: blender --background --factory-startup --python tests/test_obstbaum.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()
|
|
|
|
fails = []
|
|
|
|
for o in list(bpy.data.objects):
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
|
|
me = bpy.data.meshes.new("Leaf_Card")
|
|
me.from_pydata([(-0.5, 0, 0), (0, 0, 0.7), (0.5, 0, 0), (0, 0, -0.1)],
|
|
[], [(0, 1, 2), (0, 2, 3)])
|
|
me.update()
|
|
card = bpy.data.objects.new("Leaf_Card", me)
|
|
bpy.context.scene.collection.objects.link(card)
|
|
|
|
s = bpy.context.scene.tree_gen_settings
|
|
s.apply_modifier = True
|
|
s.leaf_card = card
|
|
|
|
if "obstbaum" not in tg.PRESETS:
|
|
print("ERGEBNIS: FEHLER: Preset 'obstbaum' fehlt")
|
|
sys.exit(1)
|
|
|
|
|
|
def inseln(m):
|
|
nachbarn = {}
|
|
for e in m.edges:
|
|
a, b = e.vertices
|
|
nachbarn.setdefault(a, []).append(b)
|
|
nachbarn.setdefault(b, []).append(a)
|
|
gesehen, out = set(), []
|
|
for start in range(len(m.vertices)):
|
|
if start in gesehen:
|
|
continue
|
|
stapel, gruppe = [start], []
|
|
gesehen.add(start)
|
|
while stapel:
|
|
v = stapel.pop()
|
|
gruppe.append(v)
|
|
for w in nachbarn.get(v, ()):
|
|
if w not in gesehen:
|
|
gesehen.add(w)
|
|
stapel.append(w)
|
|
out.append(gruppe)
|
|
return out
|
|
|
|
|
|
def deckung_messen(preset, seed):
|
|
"""Blattflaeche je m3 Kronenzylinder - fuer beliebiges Preset.
|
|
|
|
Der Wert haengt an der verwendeten Card (Flaeche!), taugt also NICHT als
|
|
absolute Schwelle. Verglichen wird deshalb gegen die Eiche, die in
|
|
demselben Lauf mit DERSELBEN Card gemessen wird.
|
|
"""
|
|
for o in list(bpy.context.scene.objects):
|
|
if o is not card:
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
s.preset = preset
|
|
s.art_name = "Ref"
|
|
s.variant = 1
|
|
s.seed = seed
|
|
s.use_growth = False
|
|
bpy.ops.object.treegen_create()
|
|
baum = [o for o in bpy.context.scene.objects
|
|
if o.type == 'MESH' and o is not card
|
|
and not o.name.endswith(("_Leaf", "_Frucht"))][0]
|
|
for o in bpy.context.scene.objects:
|
|
o.select_set(o is baum)
|
|
bpy.context.view_layer.objects.active = baum
|
|
bpy.ops.object.treegen_leaves()
|
|
bl = bpy.data.objects.get(baum.name + "_Leaf")
|
|
dg = bpy.context.evaluated_depsgraph_get()
|
|
ev = bl.evaluated_get(dg)
|
|
m = ev.to_mesh()
|
|
flaeche = sum(p.area for p in m.polygons)
|
|
mitten = []
|
|
for grp in inseln(m):
|
|
pts = [m.vertices[i].co for i in grp]
|
|
mitten.append((sum(p.x for p in pts) / len(pts),
|
|
sum(p.y for p in pts) / len(pts),
|
|
sum(p.z for p in pts) / len(pts)))
|
|
# FALLE: erst kopieren, DANN freigeben. `to_mesh_clear` macht jede
|
|
# Referenz auf das Mesh ungueltig ("StructRNA of type Mesh has been
|
|
# removed") - die Kugel-Rechnung stand zuerst danach.
|
|
pts = [v.co.copy() for v in m.vertices]
|
|
ev.to_mesh_clear()
|
|
# 90. PERZENTIL statt Maximum, wie beim Breitenprofil. Von der Spielseite
|
|
# gemeldet und hier nachgemessen: seit den Bueschel-Schalen streut der
|
|
# Kronenradius viel breiter (Obstbaum p50 1.35 / p90 2.12 / max 3.17), und
|
|
# das Maximum haengt an einer einzelnen weit aussen sitzenden Card. Mit max
|
|
# faellt die Deckung 2.3-mal pessimistischer aus - und weil das Verhaeltnis
|
|
# max/p90 je Art verschieden ist (Tanne 1.57, Eiche 1.40), verzerrt es auch
|
|
# den VERGLEICH gegen die Eiche, den dieser Test fuehrt.
|
|
lr = sorted(math.hypot(x, y) for x, y, _z in mitten)
|
|
lr = lr[min(len(lr) - 1, int(0.9 * (len(lr) - 1)))]
|
|
lz = [z for _x, _y, z in mitten]
|
|
vol = math.pi * lr ** 2 * (max(lz) - min(lz))
|
|
# Kugel-Kennwert mit DERSELBEN Formel wie unten: Kronenhoehe geteilt durch
|
|
# den groessten Perzentil-Durchmesser.
|
|
pz = [q.z for q in pts]
|
|
d3 = (max(pz) - min(pz)) / 3.0
|
|
breiten = []
|
|
for kk in range(3):
|
|
teil = sorted(math.hypot(q.x, q.y) for q in pts
|
|
if min(pz) + kk * d3 <= q.z <= min(pz) + (kk + 1) * d3)
|
|
breiten.append(2 * (teil[int(0.9 * (len(teil) - 1))] if teil else 0.0))
|
|
kug = (max(pz) - min(pz)) / max(breiten) if max(breiten) else 0.0
|
|
return (flaeche / vol if vol else 0.0), kug
|
|
|
|
|
|
# Die EICHE ist der Massstab: sie schliesst laut Abnahme der Spielseite, der
|
|
# Obstbaum tat es nicht. Hier gemessen statt als Konstante hingeschrieben,
|
|
# weil der Wert an der Card-Flaeche haengt - eine feste Zahl waere nur fuer
|
|
# genau diese Testkarte richtig.
|
|
_ref = [deckung_messen("eiche", sd) for sd in (1, 2, 3)]
|
|
EICHE = sum(r[0] for r in _ref) / 3.0
|
|
EICHE_KUGEL = sum(r[1] for r in _ref) / 3.0
|
|
print("Massstab Eiche: %.2f m2 Blatt je m3 Krone, Kugel-Kennwert %.2f "
|
|
"(mit DIESER Testcard)" % (EICHE, EICHE_KUGEL))
|
|
print("")
|
|
|
|
# Die Seeds sind die der Spielseite: Apfel 1/15/22, Kirsche 2/16/23,
|
|
# Birne 3/17/24. Ein Preset, das nur bei einem Seed passt, ist keines.
|
|
SEEDS = (1, 15, 22, 2, 16, 23, 3, 17, 24)
|
|
|
|
print("%-6s %7s %6s %9s %9s %7s %8s %-15s %s"
|
|
% ("Seed", "Hoehe", "B/H", "KronR", "KronR/Cd", "Ansatz", "m2/m3",
|
|
"Profil u/m/o", "o/m"))
|
|
|
|
verh = []
|
|
deckungen = []
|
|
obenmitte = []
|
|
untenmitte = []
|
|
kugeln = []
|
|
for seed in SEEDS:
|
|
for o in list(bpy.context.scene.objects):
|
|
if o is not card:
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
s.preset = "obstbaum"
|
|
s.art_name = "Obst"
|
|
s.variant = 1
|
|
s.seed = seed
|
|
s.use_growth = False
|
|
bpy.ops.object.treegen_create()
|
|
baum = [o for o in bpy.context.scene.objects
|
|
if o.type == 'MESH' and o is not card
|
|
and not o.name.endswith(("_Leaf", "_Frucht"))][0]
|
|
for o in bpy.context.scene.objects:
|
|
o.select_set(o is baum)
|
|
bpy.context.view_layer.objects.active = baum
|
|
bpy.ops.object.treegen_leaves()
|
|
bl = bpy.data.objects.get(baum.name + "_Leaf")
|
|
|
|
dg = bpy.context.evaluated_depsgraph_get()
|
|
ev = baum.evaluated_get(dg)
|
|
m = ev.to_mesh()
|
|
vs = [v.co.copy() for v in m.vertices]
|
|
attr = m.attributes.get("ast_id")
|
|
aid = [attr.data[i].value for i in range(len(vs))] if attr else [0] * len(vs)
|
|
ev.to_mesh_clear()
|
|
z0 = min(v.z for v in vs)
|
|
hoehe = max(v.z for v in vs) - z0
|
|
kron_r = max(math.hypot(v.x, v.y) for v in vs)
|
|
astz = [v.z for v, i in zip(vs, aid) if i > 0]
|
|
ansatz = 100.0 * (min(astz) - z0) / hoehe if astz else 0.0
|
|
|
|
card_max = 0.0
|
|
blatt_flaeche = 0.0
|
|
kron_zyl = 0.0
|
|
if bl is not None:
|
|
ev = bl.evaluated_get(dg)
|
|
m2 = ev.to_mesh()
|
|
blatt_flaeche = sum(p.area for p in m2.polygons)
|
|
blatt_pts = [v.co.copy() for v in m2.vertices]
|
|
mitten = []
|
|
for grp in inseln(m2):
|
|
pts = [m2.vertices[i].co for i in grp]
|
|
card_max = max(card_max,
|
|
max((a - b).length for a in pts for b in pts))
|
|
mitten.append((sum(p.x for p in pts) / len(pts),
|
|
sum(p.y for p in pts) / len(pts),
|
|
sum(p.z for p in pts) / len(pts)))
|
|
ev.to_mesh_clear()
|
|
if mitten:
|
|
# p90 wie oben - dieselbe Formel wie beim Massstab Eiche.
|
|
lr = sorted(math.hypot(x, y) for x, y, _z in mitten)
|
|
lr = lr[min(len(lr) - 1, int(0.9 * (len(lr) - 1)))]
|
|
lz = [z for _x, _y, z in mitten]
|
|
kron_zyl = math.pi * lr ** 2 * (max(lz) - min(lz))
|
|
|
|
# BREITENPROFIL der Krone in Dritteln.
|
|
#
|
|
# Ohne diesen Check kann die Krone alle bisherigen Zahlen erfuellen und
|
|
# trotzdem falsch aussehen: der erste Obstbaum war ein TRICHTER - nach oben
|
|
# breiter, unten hohl (gemessen oben/mitte 1.07 gegen 0.90 der Eiche). Die
|
|
# Laubmasse sass an den top-aussen endenden Astspitzen. Kronenradius,
|
|
# Deckung und Tri-Zahl waren dabei alle in Ordnung.
|
|
profil = [0.0, 0.0, 0.0]
|
|
if blatt_pts:
|
|
pz = [p.z for p in blatt_pts]
|
|
pz0, pz1 = min(pz), max(pz)
|
|
drittel = (pz1 - pz0) / 3.0
|
|
for kk in range(3):
|
|
teil = [math.hypot(p.x, p.y) for p in blatt_pts
|
|
if pz0 + kk * drittel <= p.z <= pz0 + (kk + 1) * drittel]
|
|
# 90. PERZENTIL statt Maximum. Das Maximum haengt an EINER Card:
|
|
# bei ~400 Cards von 1.25 m Diagonale reicht eine einzige weit
|
|
# aussen haengende, um die Breite eines Drittels zu setzen.
|
|
# Gemessen sprang unten/mitte dadurch je Seed zwischen 0.89 und
|
|
# 1.00, waehrend der Mittelwert stabil bei 0.94 lag - das war
|
|
# Rauschen, keine Formabweichung. Das Perzentil misst, wo die
|
|
# Laubmasse endet, nicht wo die letzte Card haengt.
|
|
teil.sort()
|
|
profil[kk] = 2 * (teil[int(0.9 * (len(teil) - 1))] if teil else 0.0)
|
|
om = profil[2] / profil[1] if profil[1] else 0.0
|
|
um = profil[0] / profil[1] if profil[1] else 0.0
|
|
# KUGEL-Kennwert: Kronenhoehe geteilt durch die groesste Kronenbreite.
|
|
#
|
|
# RELATIV zur Eiche, nicht als feste Zahl. Die Bueschel-Schalen schieben
|
|
# jede Card um bis zu einen Kappenradius vom Ast weg, und zwar in ALLE
|
|
# Richtungen - die Krone gewinnt ringsum Breite, in der Hoehe aber nur oben.
|
|
# Das Verhaeltnis kippt dadurch bauartbedingt: der Obstbaum fiel von
|
|
# 1.35-1.47 auf 0.95-1.23, ohne dass an seiner Form etwas falsch war.
|
|
#
|
|
# Eine neue Konstante haette dasselbe Schicksal bei der naechsten Bauform.
|
|
# Die Vorgabe der Spielseite lautet deshalb: der Obstbaum bleibt die
|
|
# RUNDESTE Art, also deutlich unter dem Eichenwert aus demselben Lauf.
|
|
# Dasselbe Prinzip wie bei der Deckungs-Schranke.
|
|
kugel = 0.0
|
|
if blatt_pts and max(profil) > 0:
|
|
pz = [q.z for q in blatt_pts]
|
|
kugel = (max(pz) - min(pz)) / max(profil)
|
|
kugeln.append(kugel)
|
|
obenmitte.append(om)
|
|
untenmitte.append(um)
|
|
|
|
v = kron_r / card_max if card_max else 0.0
|
|
verh.append(v)
|
|
deckung = blatt_flaeche / kron_zyl if kron_zyl else 0.0
|
|
deckungen.append(deckung)
|
|
print("%-6d %7.2f %6.2f %9.2f %9.2f %6.0f%% %8.1f %.1f/%.1f/%.1f %.2f"
|
|
% (seed, hoehe, 2 * kron_r / hoehe, kron_r, v, ansatz, deckung,
|
|
profil[0], profil[1], profil[2], om))
|
|
|
|
# Kronen-DECKUNG: Blattflaeche je m3 Kronenvolumen.
|
|
#
|
|
# Die Card-ZAHL sagt das nicht: 400 Cards koennen eine geschlossene Haube
|
|
# sein oder fuenf belaubte Schlaeuche mit Himmel dazwischen ("behaarte
|
|
# Finger", von der Spielseite abgenommen). Was es sagt, ist die Flaeche pro
|
|
# Volumen - und die trennt die beiden Faelle sauber:
|
|
# Eiche (schliesst) 465 m2 / 18.9 m3 = 24.6
|
|
# Obstbaum vorher (offen) 414 m2 / 36.2 m3 = 11.4
|
|
# Die Obstbaum-Krone hat fast das doppelte Volumen; bei gleicher Card-Zahl
|
|
# blieb die halbe Deckung uebrig.
|
|
#
|
|
# Verglichen wird gegen die im selben Lauf gemessene Eiche, NICHT gegen
|
|
# eine feste Zahl: die Deckung haengt an der Card-Flaeche, und eine
|
|
# 6-Tri-Buescheldkarte bringt das Dreifache einer flachen Raute. Eine
|
|
# hart notierte 20 waere nur fuer genau eine Testcard richtig gewesen -
|
|
# der erste Anlauf dieses Checks ist genau daran gescheitert.
|
|
# 60 % der Eiche: der Obstbaum liegt bauartbedingt darunter (weitere
|
|
# Krone bei gleichem Budget), lag vorher aber bei 46 %.
|
|
if deckung < EICHE * 0.60:
|
|
fails.append("Seed %d: nur %.1f m2 Blatt je m3 Krone, Eiche %.1f "
|
|
"(%.0f %%) - die Krone liest sich als einzelne "
|
|
"Ast-Schlaeuche" % (seed, deckung, EICHE,
|
|
100 * deckung / EICHE))
|
|
|
|
# Die Krone muss sich nach OBEN schliessen. Die Eiche, die laut Abnahme als
|
|
# Baum liest, kommt auf 0.90; der Trichter lag bei 1.07. 0.85 als Grenze
|
|
# laesst Spielraum nach oben, schliesst den Trichter aber sicher aus.
|
|
# Die Grenzen sind unter der KRONEN-HUELLE neu erhoben und um die Form
|
|
# gelegt, die der User im Wald ABGENOMMEN hat - nicht umgekehrt. Spannen
|
|
# ueber neun Seeds, alle mit demselben Mass:
|
|
#
|
|
# Eiche u/m 1.04-1.16 o/m 0.79-0.91
|
|
# ABGENOMMEN u/m 0.78-1.02 o/m 0.68-0.79
|
|
# Trichter u/m 0.68-0.78 o/m 1.04-1.14
|
|
# Pilz u/m 0.59-0.90 o/m 0.82-0.95
|
|
# Haube u/m 0.94-1.03 o/m 0.71-0.78
|
|
#
|
|
# DARAUS FOLGT ETWAS UNBEQUEMES: u/m trennt unter der neuen Bauform NICHTS
|
|
# mehr. Der abgenommene Bereich (0.78-1.02) ueberlappt den Pilz (0.59-0.90)
|
|
# und stoesst an den Trichter (bis 0.78). Die fruehere 0.82-Schranke stammt
|
|
# aus der Einzelkappen-Aera; haette man dagegen kalibriert, waere die Krone
|
|
# verbogen worden, um eine Zahl zu retten, die ihre Aussagekraft verloren
|
|
# hat - derselbe Fall wie beim Kugel-Kennwert eine Runde zuvor.
|
|
#
|
|
# o/m trennt dagegen sauber: abgenommen bis 0.79, Pilz ab 0.82, Trichter ab
|
|
# 1.04. Diese eine Schranke traegt die Formpruefung, und der Kugel-Kennwert
|
|
# weiter unten trennt zusaetzlich von der Haube.
|
|
if om > 0.80:
|
|
fails.append("Seed %d: Krone oben %.1f m breit, in der Mitte %.1f "
|
|
"(%.2f) - abgenommen ist bis 0.79, darueber liegen Pilz "
|
|
"(ab 0.82) und Trichter (ab 1.04)"
|
|
% (seed, profil[2], profil[1], om))
|
|
# u/m wird nur noch MITGEDRUCKT. Ein Kriterium, das die Fehlformen nicht
|
|
# mehr ausschliesst, ist schlimmer als keines: es sieht aus wie ein Schutz
|
|
# und ist keiner.
|
|
if kugel > EICHE_KUGEL * 0.65:
|
|
fails.append("Seed %d: Kugel-Kennwert %.2f, Eiche %.2f (%.0f %%) - der "
|
|
"Obstbaum muss die RUNDESTE Art bleiben, Richtwert <= 65 %%"
|
|
% (seed, kugel, EICHE_KUGEL, 100 * kugel / EICHE_KUGEL))
|
|
|
|
# Kronenradius gegen Card-Groesse. Historisch die funktionale Bedingung der
|
|
# Frucht-Streuung, heute ein Plausibilitaets-Boden - siehe Dateikopf.
|
|
if v < 1.3:
|
|
fails.append("Seed %d: Kronenradius %.2f m bei %.2f m Card - die Krone "
|
|
"hat keine Aussenhaut fuer Fruechte" % (seed, kron_r, card_max))
|
|
if kron_r < 1.80:
|
|
fails.append("Seed %d: Kronenradius %.2f m, Vorgabe >= 1.80"
|
|
% (seed, kron_r))
|
|
if not (4.0 <= hoehe <= 5.2):
|
|
fails.append("Seed %d: Hoehe %.2f m, Vorgabe 4-5 m (5.2 als Toleranz)"
|
|
% (seed, hoehe))
|
|
if not (25.0 <= ansatz <= 35.0):
|
|
fails.append("Seed %d: Kronenansatz %.0f %%, Vorgabe ~30 %% "
|
|
"(25-35 als Toleranz)" % (seed, ansatz))
|
|
|
|
# B/H gilt fuer dieses Preset NICHT MEHR - Entscheidung der Spielseite,
|
|
# 2026-08-06. Es wird nur noch mitgedruckt.
|
|
#
|
|
# Vorgeschichte: das urspruengliche Band war 0.55-0.75, zusammen mit
|
|
# Kronenradius >= 1.80 m bei 4-5 m Hoehe. Beides zusammen laesst bei H = 5.0 nur
|
|
# B zwischen 3.60 und 3.75 zu - ein Fenster von 4 %, waehrend die Streuung von
|
|
# Seed zu Seed rund 12 % betraegt. Zweimal ist die Abwaegung deshalb zugunsten
|
|
# des Kronenradius ausgefallen (er traegt die Frucht-Streuung); beim zweiten Mal
|
|
# hat die Spielseite daraus eine Regel gemacht:
|
|
#
|
|
# Kronenradius >= 1.80 schlaegt B/H <= 0.75.
|
|
# Die gemessenen 0.77-0.82 sind der Preis der Frucht-Tragflaeche.
|
|
#
|
|
# Die Form sichert ohnehin das BREITENPROFIL schaerfer ab, als B/H es je
|
|
# konnte: B/H haette sowohl den Trichter (oben/mitte 1.07) als auch die
|
|
# Pilzform (unten/mitte 1.17) durchgelassen.
|
|
# --- Der HOCHSTAMM muss auf JEDER Wachstums-Stufe stehen -----------------
|
|
#
|
|
# Bis 1.46 war nur S4 kalibriert. Im Spiel ist der User auf einen S3 gestossen,
|
|
# der sich als duenne Astgabel mit Laubklumpen las statt als Obstbaeumchen -
|
|
# gemeldet aus dem Welt-Test. Gemessen lag der Kronenansatz bei 10 / 16 / 22 /
|
|
# 29 % ueber die Stufen: den freien Stamm gab es nur ganz am Ende.
|
|
#
|
|
# Ursache war der allgemeine Setzlings-Faktor auf "Branch Start", der die
|
|
# Selbstbeschneidung eines WILDEN Baums abbildet. Ein Hochstamm wird aber
|
|
# gezogen: die Baumschule veredelt hoch und nimmt die unteren Triebe weg, der
|
|
# freie Stamm ist von Anfang an da. Deshalb GROWTH_OVERRIDES.
|
|
print("")
|
|
print("%-6s %7s %7s %7s" % ("Stufe", "Hoehe", "Cards", "Ansatz"))
|
|
for o in list(bpy.context.scene.objects):
|
|
if o is not card:
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
s.preset = "obstbaum"
|
|
s.art_name = "Stufen"
|
|
s.variant = 1
|
|
s.seed = 1
|
|
s.use_growth = True
|
|
s.stages = 4
|
|
bpy.ops.object.treegen_create()
|
|
_st = sorted((o for o in bpy.context.scene.objects
|
|
if o.type == 'MESH' and o is not card
|
|
and not o.name.endswith(("_Leaf", "_Frucht"))), key=lambda o: o.name)
|
|
for o in bpy.context.scene.objects:
|
|
o.select_set(o in _st)
|
|
bpy.context.view_layer.objects.active = _st[0]
|
|
bpy.ops.object.treegen_leaves()
|
|
for _k, _x in enumerate(_st, 1):
|
|
_dg = bpy.context.evaluated_depsgraph_get()
|
|
_ev = _x.evaluated_get(_dg)
|
|
_m = _ev.to_mesh()
|
|
_vs = [v.co.copy() for v in _m.vertices]
|
|
_a = _m.attributes.get("ast_id")
|
|
_aid = [_a.data[i].value for i in range(len(_vs))] if _a else [0] * len(_vs)
|
|
_ev.to_mesh_clear()
|
|
_z0 = min(v.z for v in _vs)
|
|
_h = max(v.z for v in _vs) - _z0
|
|
_astz = [v.z for v, i in zip(_vs, _aid) if i > 0]
|
|
_an = 100.0 * (min(_astz) - _z0) / _h if _astz else 0.0
|
|
_bl = bpy.data.objects.get(_x.name + "_Leaf")
|
|
_n = 0
|
|
if _bl is not None:
|
|
_ev = _bl.evaluated_get(_dg)
|
|
_m2 = _ev.to_mesh()
|
|
_n = len(_m2.polygons) // 2
|
|
_ev.to_mesh_clear()
|
|
print("%-6s %7.2f %7d %6.0f%%" % ("S%d" % _k, _h, _n, _an))
|
|
if not (23.0 <= _an <= 35.0):
|
|
fails.append("S%d: Kronenansatz %.0f %% - der Hochstamm muss auf JEDER "
|
|
"Stufe stehen, nicht nur bei S4 (Vorgabe ~29 %%)"
|
|
% (_k, _an))
|
|
if _n == 0:
|
|
fails.append("S%d: keine Cards" % _k)
|
|
|
|
print("")
|
|
print("Kronenradius/Card ueber alle Seeds: %.2f - %.2f" % (min(verh), max(verh)))
|
|
print("Krone unten/mitte ueber alle Seeds: %.2f - %.2f (nur Info - trennt "
|
|
"unter der Huelle nichts mehr)" % (min(untenmitte), max(untenmitte)))
|
|
print("Kugel (KronH/KronB) ueber alle Seeds: %.2f - %.2f, Eiche %.2f "
|
|
"(%.0f-%.0f %% - Vorgabe <= 65 %%)"
|
|
% (min(kugeln), max(kugeln), EICHE_KUGEL,
|
|
100 * min(kugeln) / EICHE_KUGEL, 100 * max(kugeln) / EICHE_KUGEL))
|
|
print("Krone oben/mitte ueber alle Seeds: %.2f - %.2f (Pilz ab 0.82, "
|
|
"Trichter ab 1.04)" % (min(obenmitte), max(obenmitte)))
|
|
print("Kronen-Deckung ueber alle Seeds: %.1f - %.1f m2/m3, Eiche %.1f "
|
|
"(%.0f-%.0f %%)"
|
|
% (min(deckungen), max(deckungen), EICHE,
|
|
100 * min(deckungen) / EICHE, 100 * max(deckungen) / EICHE))
|
|
|
|
if fails:
|
|
print("ERGEBNIS: %d FEHLER" % len(fails))
|
|
for f in fails:
|
|
print(" - " + f)
|
|
sys.exit(1)
|
|
print("ERGEBNIS: ALLE CHECKS OK")
|