Files
adminandClaude Opus 5 182b5b66b4 Aussenplan an die Farbgebung der Geschoss-Renderings angleichen
Aus OG.png gemessen, nicht geschaetzt: die hellste Stufe ist genau neutral
und liegt bei 200 - heller wird dort nichts -, und je dunkler eine Flaeche
wird, desto blauer wird sie. Beides steckt jetzt in aussenSchatten(), das
bisher farbtreu gegen Schwarz gerechnet hat.

Damit allein fehlte dem Plan aber immer noch das obere Drittel der
Helligkeitsskala, das die Renderings haben. Grund war, dass jede Aussenwand
denselben Faktor bekam, egal wohin sie zeigte - der Plan hatte gar kein
gerichtetes Licht. aussenBeleuchtung() liefert es jetzt aus einer Sonne im
Westsuedwesten und ersetzt nebenbei die grobe Zweistufung am Dach.

Waagerechte Flaechen bleiben knapp unter der besonnten Wand. Mit vollem
Licht waren Boden und Terrassen das Hellste im Bild; im Vorbild sind es die
Waende.

Die Deckflaeche lief bisher an aussenSchatten() vorbei, sonst haette der
Deckel die flachen Flaechen nie erreicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:33:20 +02:00

883 lines
38 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Der Lageplan des Außengeländes und die Projektion, die ihn zeichnet.
*
* OG, EG und UG haben je ein Rendering in assets/img. Für das Außengelände
* gibt es keins - die Planungssoftware kennt nur das einzelne Gebäude, nicht
* das Grundstück -, und eine schlichte Draufsicht taugt hier ohnehin nicht:
* Veranda und Terrasse gibt es zweimal, einmal für jede Wohnpartei, und in
* der Draufsicht liegt das obere Paar genau auf dem unteren. Man könnte die
* beiden nicht auseinanderhalten und schon gar nicht getrennt anklicken.
*
* Deshalb eine Schrägsicht von Südwesten. Von dort sieht man den Hof, auf den
* die Veranden zeigen; die Ostseite des Hauses trägt nichts, was hier
* vorkommen müsste.
*
* Gezeichnet wird in Militärprojektion (Planoblique), nicht in echter
* Perspektive:
*
* X = x·cos φ y·sin φ
* Y = (x·sin φ + y·cos φ)·STAUCHUNG z·UEBERHOEHUNG
*
* Der Grundriss bleibt dabei maßhaltig - Geraden bleiben gerade, Parallelen
* parallel, Rechtecke Rechtecke. Genau deshalb diese Projektion und keine
* perspektivische: die Flächen lassen sich am Luftbild von oben abklicken
* (tools/lageplan.php), und dieselben Zahlen ergeben ohne Umweg die
* Schrägsicht. Eine echte Perspektive müsste man neu vermessen.
*
* Das Vorzeichen des Winkels bestimmt die Himmelsrichtung: bei +35° zeigt der
* Y-Gradient nach Osten und Süden, die nächste Ecke wäre die südöstliche.
* Für Südwesten muss er negativ sein.
*
* Was die Paare trennt, ist nicht die Drehung, sondern Stauchung und
* Überhöhung: auch ohne jede Drehung stünde Terrasse OG über Terrasse EG.
* Die Drehung macht nur die Verwandtschaft zu den anderen Geschossen.
*/
require_once(__DIR__ . "/rooms.php");
/**
* Vorgabefarben je Flaeche. Sie stehen hier und nicht in der Geometrie,
* damit die Messseite und die Anzeige dasselbe Gruen fuer den Garten
* nehmen - und damit man eine Farbe an einer Stelle aendert.
*/
function aussenFarbe($name)
{
$farben = [
"Haus" => "#c8cdd6",
"Hof" => "#57585a",
"Garten" => "#2e5c33",
"Garage" => "#8a9099",
"Carport EG" => "#3f4a63", // nur Fundament, mit PV darauf
"Carport OG" => "#6f7680",
"Veranda EG" => "#9a7b52",
"Veranda OG" => "#c9a978",
"Terrasse EG" => "#9a7b52",
"Terrasse OG" => "#c9a978",
"Pergola" => "#7a6a8f",
];
return $farben[$name] ?? "#8a9099";
}
/** Blickrichtung: negativ heißt von Westen her. -35° ist Südwesten. */
const PLAN_WINKEL = -35.0;
/** Wie stark die Tiefe zusammengedrückt wird. 1 wäre die reine Draufsicht. */
const PLAN_STAUCHUNG = 0.55;
/**
* Wie hoch Höhen erscheinen. 1,4 heißt: ein Meter Höhe sieht aus wie 1,4 m
* Grundfläche. Überhöht, damit die drei Meter zwischen Terrasse EG und
* Terrasse OG deutlich werden - bei maßstäblicher Höhe klebten die beiden
* aufeinander, und genau das soll die Schrägsicht ja auflösen.
*/
const PLAN_UEBERHOEHUNG = 1.4;
/**
* Hellster Wert, den eine Flaeche annehmen darf.
*
* Aus den Renderings der Innengeschosse gemessen: dort geht kein einziger
* Bildpunkt ueber 200. Reines Weiss kommt nicht vor, und genau das laesst die
* Bilder ruhig wirken. Ohne diesen Deckel steht der Aussenplan grell neben
* ihnen.
*/
const PLAN_LICHT = 200;
/**
* Wohin abgedunkelt wird - nicht nach Schwarz, sondern ins Blaugraue.
*
* Ebenfalls gemessen: im Vorbild ist die hellste Stufe genau neutral, und mit
* abnehmender Helligkeit waechst der Blauanteil auf rund 30 Stufen an. Das ist
* kein Stilmittel, sondern Physik - eine Flaeche, die keine Sonne abbekommt,
* bekommt ihr Licht vom Himmel, und der ist blau.
*/
const PLAN_SCHATTEN = [40, 62, 98];
/**
* Woher das Licht kommt, als Richtung in der Grundflaeche - x nach Osten,
* y nach Sueden. Westsuedwest, also Nachmittagssonne.
*
* Nicht genau aus Suedwesten, obwohl von dort geschaut wird: Dann laegen die
* beiden sichtbaren Seiten gleich hell, und die Kante zwischen ihnen waere
* verschwunden. Etwas nach Westen versetzt trennt sie sich.
*/
const PLAN_LICHTRICHTUNG = [-0.85, 0.53];
/** Wieviel Licht eine Flaeche bekommt, die von der Sonne abgewandt ist. */
const PLAN_UMGEBUNG = 0.58;
/**
* Licht auf einer waagerechten Flaeche - Boden, Terrasse, Wandoberkante.
*
* Etwas weniger als eine gut zur Sonne stehende Wand. Bei rund 40 Grad
* Sonnenhoehe bekommen beide fast dasselbe ab, und die Wand einen Hauch mehr.
* Mit vollem Licht waren Boden und Terrassen das Hellste im Bild und zogen den
* Blick auf sich; im Vorbild sind es die Waende.
*/
const PLAN_WAAGERECHT = 0.88;
/** So groß ist die Zeichenfläche, in die alles hineinpassen muss. */
const PLAN_BREITE = 400;
const PLAN_HOEHE = 300;
/** Luft zwischen Zeichnung und Rand, in Einheiten der Zeichenfläche. */
const PLAN_RAND = 12;
/**
* Die Flächen des Außengeländes, in Metern.
*
* Herkunft: tools/lageplan.php - dort werden sie am amtlichen Luftbild
* abgeklickt, die Seite gibt diese Zeilen fertig aus. Von Hand zu ändern
* lohnt nur für Kleinigkeiten; wer etwas verschiebt, misst besser neu.
*
* raum Raumname aus rooms.php, oder null für Flächen, die kein Raum sind
* (Haus, Hof, Garten - sie tragen keine Kachel)
* ecken Grundriss als [x, y] in Metern, x nach Osten, y nach Süden
* z Höhe der Fläche über Gelände (0 = ebenerdig, ~3 = OG-Ebene)
* hoehe Bauhöhe darüber (0 = flache Fläche, sonst ein Körper)
* dach Satteldach über der Bauhöhe, 0 = flach
* farbe Deckfläche; die Seiten werden daraus abgedunkelt
*
* Solange die Liste leer ist, hat AG keinen Grundriss - die Etage taucht dann
* im Menü nicht auf (siehe floorsWithPlan() in rooms.php), Zuordnung und
* Automatiken funktionieren trotzdem.
*/
function aussenFlaechen()
{
return [
// Der Umriss laeuft als Schluesselloch um Haus und Veranda herum. Der
// Ausbuchtung zwischen [22.7, 24.2] und [19.9, 25.6] fehlte dabei ein
// Stueck: zwischen der Ostkante der Garage, der Westseite der Veranda
// und der Nordkante des Hofs blieb ein Keil ohne jede Flaeche, und der
// zeigte den schwarzen Hintergrund. Die vier Punkte dazwischen fuehren
// den Rand jetzt um diesen Keil herum.
["raum" => "Garten", "z" => 0, "hoehe" => 0, "farbe" => "#4a6b4e",
"ecken" => [[8.6, 40.6], [7.9, 45.0], [49.0, 38.4], [50.7, 12.9], [23.6, 16.7],
[24.3, 19.5], [36.8, 17.2], [39.2, 31.2], [24.2, 33.8], [22.7, 24.2],
[22.0, 19.9], [18.5, 18.0], [19.6, 24.3],
[19.9, 25.6], [20.2, 28.7], [19.0, 29.8], [19.3, 39.1]]],
// Der Hof laeuft vor dem EG-Carport gerade bis an den Garten weiter;
// der Carport steht also auf ihm. Die suedliche Kante ist die
// Gartengrenze - dieselben beiden Punkte stehen dort im Umriss.
["raum" => null, "z" => 0, "hoehe" => 0, "farbe" => "#6e7278", // Hof
"ecken" => [[9.3, 26.1], [8.9, 36.4], [8.6, 40.6], [19.3, 39.1], [18.9, 34.7],
[19.0, 29.8], [20.4, 28.7], [19.8, 25.6], [19.6, 24.3]]],
// Das Haus. First ueber die laengere Achse, also Ost-West - so liegt
// er auch auf dem Luftbild.
// Die Anlage liegt auf der Suedschraege. Die Wiederkehr wird danach
// gezeichnet und deckt die Mitte ab - genau wie auf dem Luftbild, wo
// die Module links und rechts von ihr liegen.
["raum" => null, "z" => 0, "hoehe" => 6, "farbe" => "#c8c8c8",
"dachfarbe" => "#9aa3ad", "dach" => ["art" => "sattel", "hoehe" => 3],
"pv" => "S",
"ecken" => [[36.8, 17.2], [39.2, 31.2], [24.2, 33.8], [21.8, 19.8]]],
// Wiederkehr auf der Suedseite: ein Kreuzgiebel quer zum Hauptfirst.
// Als eigener Koerper auf derselben Grundflaeche gezeichnet - er liegt
// suedlich und damit weiter vorn, deckt das Hauptdach also von selbst
// richtig ab. Die Firsthoehe folgt der Dachneigung des Hauses: 3 m auf
// 7,1 m halbe Spannweite sind knapp 23 Grad, bei 3 m halber
// Spannweite also rund 1,3 m.
//
// Breite und Tiefe sind vom Luftbild geschaetzt - wenn es genauer sein
// soll, klick die Flaeche in tools/lageplan.php ab.
["raum" => null, "z" => 6, "hoehe" => 0, "farbe" => "#c8c8c8",
"dachfarbe" => "#9aa3ad",
"dach" => ["art" => "sattel", "hoehe" => 1.3, "achse" => "NS"],
"ecken" => [[34.7, 32.0], [28.7, 33.0], [27.5, 25.6], [33.4, 24.6]]],
// Beide Carports sind Holzkonstruktionen mit Pultdach, die Module
// liegen plan darauf. Der OG-Carport faellt zur Strasse ab, also nach
// Westen, und ist nach Sueden offen.
// Die hohe Seite liegt im Osten und schliesst dort an die Traufe des
// Garagendachs an - deshalb 2,23 + 0,27 und nicht glatt 2,5: die
// Oberkante der hohen Seite soll die 2,5 treffen, nicht die Wand.
["raum" => "Carport OG", "z" => 0, "hoehe" => 2.23, "farbe" => "#b8b3aa",
"dachfarbe" => "#a8a29a", "dachdicke" => 0.3,
"dach" => ["art" => "pult", "hoehe" => 0.27, "tief" => "W"],
"offen" => ["S"],
"ecken" => [[12.2, 19.2], [13.3, 25.4], [9.3, 26.1], [8.2, 19.9]]],
// Der EG-Carport faellt nach Sueden ab und ist nach Westen offen.
["raum" => "Carport EG", "z" => 0, "hoehe" => 2.2, "farbe" => "#b8b3aa",
"dachfarbe" => "#a8a29a", "pv" => true, "dachdicke" => 0.3,
"dach" => ["art" => "pult", "hoehe" => 0.23, "tief" => "S"],
"offen" => ["W"],
"ecken" => [[19.0, 34.6], [19.3, 39.1], [10.7, 40.3], [10.4, 35.8]]],
// Die Garage hat ein begruentes Dach und ihr Tor nach Sueden. Der
// First laeuft Nord-Sued, die Traufen liegen also im Osten und Westen;
// an der westlichen setzt der OG-Carport an. Ausgeschrieben, weil bei
// einem fast quadratischen Grundriss sonst die zweite Nachkommastelle
// entscheidet, welche Seite die laengere ist.
["raum" => "Garage", "z" => 0, "hoehe" => 2.5, "farbe" => "#c8c8c8",
"dachfarbe" => "#7d9470", "tor" => "S",
"dach" => ["art" => "sattel", "hoehe" => 1.5, "achse" => "NS"],
"ecken" => [[12.2, 19.2], [13.3, 25.4], [19.6, 24.3], [18.5, 18.0]]],
["raum" => "Veranda EG", "z" => 0, "hoehe" => 0, "farbe" => "#aeb8c4",
"ecken" => [[24.3, 19.5], [26.2, 30.8], [23.8, 31.1], [22.0, 19.9]]],
["raum" => "Veranda OG", "z" => 3, "hoehe" => 0, "farbe" => "#c3cbd5",
"ecken" => [[24.3, 19.5], [26.2, 30.8], [23.8, 31.1], [22.0, 19.9]]],
["raum" => "Terrasse EG", "z" => 0, "hoehe" => 0, "farbe" => "#aeb8c4",
"ecken" => [[23.8, 31.1], [28.3, 30.4], [29.0, 33.0], [24.2, 33.8]]],
["raum" => "Terrasse OG", "z" => 3, "hoehe" => 0, "farbe" => "#c3cbd5",
"ecken" => [[23.8, 31.1], [28.3, 30.4], [29.0, 33.0], [24.2, 33.8]]],
// Ein Sonnensegel, kein Bauwerk: eine Bahn Tuch auf zwei Metern. Dass
// sie ohne Sichtbares darunter schwebt, ist richtig so - die Masten
// sind duenner als ein Strich bei diesem Massstab.
["raum" => "Pergola", "z" => 2, "hoehe" => 0, "farbe" => "#c6cbd2",
"ecken" => [[48.0, 36.5], [43.8, 36.4], [46.0, 31.5], [49.4, 32.4]]],
];
}
/**
* Die Projektion selbst. Liefert eine Funktion, die (x, y, z) in Metern auf
* (X, Y) in projizierten Metern abbildet - noch ohne Bezug zur Zeichenfläche.
*/
function aussenProjektor()
{
$w = deg2rad(PLAN_WINKEL);
$c = cos($w);
$s = sin($w);
return function ($x, $y, $z) use ($c, $s) {
return [$x * $c - $y * $s,
($x * $s + $y * $c) * PLAN_STAUCHUNG - $z * PLAN_UEBERHOEHUNG];
};
}
/**
* Alles Gezeichnete in die Zeichenfläche einpassen.
*
* Erst wird projiziert, dann ein Maßstab gesucht, in den die ganze Zeichnung
* passt - nicht umgekehrt. Die Zeichenfläche ist 400 × 300 und hat mit Metern
* nichts zu tun; wer den Maßstab vorher festlegt, schneidet entweder etwas ab
* oder lässt die Hälfte leer.
*
* Wichtig: Messseite und Anzeige müssen dieselbe Einpassung benutzen, sonst
* sitzen die Kacheln woanders als die Flächen, auf denen sie liegen sollen.
* Deshalb steht sie hier und nicht zweimal.
*/
function aussenEinpassung($flaechen)
{
$P = aussenProjektor();
$xs = [];
$ys = [];
foreach ($flaechen as $f) {
// "dach" ist eine Beschreibung, keine Zahl - die Hoehe steckt darin.
$oben = ($f["z"] ?? 0) + ($f["hoehe"] ?? 0) + ($f["dach"]["hoehe"] ?? 0);
foreach ($f["ecken"] as $e) {
foreach ([$f["z"] ?? 0, $oben] as $z) {
list($X, $Y) = $P($e[0], $e[1], $z);
$xs[] = $X;
$ys[] = $Y;
}
}
}
if (!$xs) {
return null;
}
$breite = max($xs) - min($xs);
$hoehe = max($ys) - min($ys);
// Der kleinere der beiden Maßstäbe passt in beide Richtungen.
$massstab = min(($breite > 0 ? (PLAN_BREITE - 2 * PLAN_RAND) / $breite : INF),
($hoehe > 0 ? (PLAN_HOEHE - 2 * PLAN_RAND) / $hoehe : INF));
if (!is_finite($massstab)) {
$massstab = 1.0;
}
return [
"massstab" => $massstab,
// Zusätzlich zentrieren: was schmaler ist als die Fläche, soll nicht
// in der Ecke kleben.
"dx" => (PLAN_BREITE - $breite * $massstab) / 2 - min($xs) * $massstab,
"dy" => (PLAN_HOEHE - $hoehe * $massstab) / 2 - min($ys) * $massstab,
];
}
/** Ein Punkt in Metern zu einem Punkt auf der Zeichenfläche. */
function aussenPunkt($einpassung, $x, $y, $z)
{
$P = aussenProjektor();
list($X, $Y) = $P($x, $y, $z);
return [$X * $einpassung["massstab"] + $einpassung["dx"],
$Y * $einpassung["massstab"] + $einpassung["dy"]];
}
/**
* Wie nah eine Flaeche am Betrachter liegt. Groesser heisst naeher.
*
* Bei dieser Projektion faellt genau eine Raumrichtung auf einen Bildpunkt
* zusammen; ihre Komponenten sind sin(Winkel), cos(Winkel) und
* Stauchung/Ueberhoehung. Ein Punkt darauf projiziert ist das Tiefenmass.
*
* Die projizierte Y-Koordinate taugt dafuer nicht: in ihr steckt auch die
* Hoehe, ein hoher Punkt saehe damit weiter hinten aus.
*/
function aussenTiefe($welt)
{
$w = deg2rad(PLAN_WINKEL);
$summe = 0.0;
foreach ($welt as $p) {
$summe += $p[0] * sin($w) + $p[1] * cos($w)
+ $p[2] * (PLAN_STAUCHUNG / PLAN_UEBERHOEHUNG);
}
return $summe / max(1, count($welt));
}
/** Mittelwert einer Punktliste - gut genug als Ankerpunkt einer Fläche. */
function aussenMitte($punkte)
{
$sx = 0.0;
$sy = 0.0;
foreach ($punkte as $p) {
$sx += $p[0];
$sy += $p[1];
}
$n = max(1, count($punkte));
return [$sx / $n, $sy / $n];
}
/**
* Eine Farbe ins Licht setzen - Faktor 1 volle Beleuchtung, kleiner ist
* Schatten.
*
* Zwei Dinge unterscheiden das vom blossen Abdunkeln, und beide sind aus den
* Renderings der Innengeschosse abgelesen:
*
* - Der Deckel. Keine Flaeche wird heller als PLAN_LICHT, auch wenn die
* Grundfarbe es waere. Der Farbton bleibt dabei erhalten, weil alle drei
* Kanaele mit demselben Wert heruntergezogen werden.
* - Die Richtung. Abgedunkelt wird nicht nach Schwarz, sondern nach
* PLAN_SCHATTEN. Deshalb wird eine Flaeche blauer, je weniger Licht sie
* bekommt - der farbtreue Weg dorthin sah daneben stumpf aus.
*/
function aussenSchatten($hex, $faktor)
{
$n = hexdec(ltrim($hex, "#"));
$kanal = [($n >> 16) & 255, ($n >> 8) & 255, $n & 255];
$spitze = max($kanal);
if ($spitze > PLAN_LICHT) {
foreach ($kanal as $i => $wert) {
$kanal[$i] = $wert * PLAN_LICHT / $spitze;
}
}
$f = max(0.0, min(1.0, $faktor));
return sprintf("#%02x%02x%02x",
(int) round($kanal[0] * $f + PLAN_SCHATTEN[0] * (1 - $f)),
(int) round($kanal[1] * $f + PLAN_SCHATTEN[1] * (1 - $f)),
(int) round($kanal[2] * $f + PLAN_SCHATTEN[2] * (1 - $f)));
}
/**
* Wie hell eine senkrechte Flaeche mit dieser Aussennormalen liegt.
*
* Bisher bekam jede Aussenwand denselben Wert, egal wohin sie zeigte. Der Plan
* sah dadurch flach aus, und nachgemessen fehlte ihm genau das obere Drittel
* der Helligkeitsskala, das die Renderings haben. Eine Lichtrichtung fuellt es.
*/
function aussenBeleuchtung($nx, $ny)
{
$laenge = hypot($nx, $ny);
if ($laenge <= 0) {
return 1.0; // waagerecht, volle Beleuchtung von oben
}
$anteil = ($nx * PLAN_LICHTRICHTUNG[0] + $ny * PLAN_LICHTRICHTUNG[1]) / $laenge;
return PLAN_UMGEBUNG + (1 - PLAN_UMGEBUNG) * max(0.0, $anteil);
}
/**
* Himmelsrichtung, in die eine Aussennormale zeigt.
*
* Das Grundstueck steht rund zehn Grad schief im Raster; die groessere der
* beiden Komponenten ist deshalb immer eindeutig.
*/
function aussenRichtung($nx, $ny)
{
if (abs($nx) > abs($ny)) {
return $nx > 0 ? "O" : "W";
}
return $ny > 0 ? "S" : "N";
}
/**
* Ein Vieleck mit positivem Umlaufsinn.
*
* Daran haengt zweierlei: die Aussennormale einer Kante ist nur bei bekanntem
* Umlauf berechenbar, und das Wegwerfen der Rueckseiten prueft das Vorzeichen
* der projizierten Flaeche. Beides ginge schief, wenn eine Flaeche andersherum
* abgeklickt wurde als die naechste.
*/
function aussenUmlauf($ecken)
{
$summe = 0.0;
$n = count($ecken);
for ($i = 0; $i < $n; $i++) {
$j = ($i + 1) % $n;
$summe += $ecken[$i][0] * $ecken[$j][1] - $ecken[$j][0] * $ecken[$i][1];
}
return $summe < 0 ? array_reverse($ecken) : $ecken;
}
/** Je Kante ihre Himmelsrichtung, Aussennormale und Laenge. */
function aussenKanten($ecken)
{
$kanten = [];
$n = count($ecken);
for ($i = 0; $i < $n; $i++) {
$a = $ecken[$i];
$b = $ecken[($i + 1) % $n];
$dx = $b[0] - $a[0];
$dy = $b[1] - $a[1];
$laenge = hypot($dx, $dy);
// Aussennormale einer positiv umlaufenden Flaeche in Bildkoordinaten.
$nx = $laenge > 0 ? $dy / $laenge : 0;
$ny = $laenge > 0 ? -$dx / $laenge : 0;
$kanten[] = ["i" => $i, "a" => $a, "b" => $b, "laenge" => $laenge,
"nx" => $nx, "ny" => $ny,
"richtung" => aussenRichtung($nx, $ny),
"mitte" => [($a[0] + $b[0]) / 2, ($a[1] + $b[1]) / 2]];
}
return $kanten;
}
/** Die Kante, deren Aussennormale in diese Richtung zeigt. */
function aussenKante($kanten, $richtung)
{
foreach ($kanten as $k) {
if ($k["richtung"] === $richtung) {
return $k;
}
}
return null;
}
/**
* Zeigt eine projizierte Flaeche zum Betrachter?
*
* Bei positivem Umlauf der Grundflaeche haben die zugewandten Seiten eine
* positive projizierte Flaeche, die abgewandten eine negative. Damit fallen
* die Rueckseiten weg, ohne sie einzeln benennen zu muessen - und mit ihnen
* die Kanten, die man durch einen Koerper hindurch gesehen haette.
*/
function aussenSichtbar($punkte)
{
$summe = 0.0;
$n = count($punkte);
for ($i = 0; $i < $n; $i++) {
$j = ($i + 1) % $n;
$summe += $punkte[$i][0] * $punkte[$j][1] - $punkte[$j][0] * $punkte[$i][1];
}
return $summe > 0;
}
/** Ein Vieleck zu seinem Schwerpunkt hin einziehen, um `abstand` Meter. */
function aussenEinzug($ecken, $abstand)
{
$m = aussenMitte($ecken);
$neu = [];
foreach ($ecken as $p) {
$dx = $p[0] - $m[0];
$dy = $p[1] - $m[1];
$d = hypot($dx, $dy);
$f = $d > $abstand ? ($d - $abstand) / $d : 0.0;
$neu[] = [$m[0] + $dx * $f, $m[1] + $dy * $f];
}
return $neu;
}
/**
* Die Zeichnung als Liste von Polygonen, von hinten nach vorn sortiert.
*
* Sortiert wird nach dem hintersten Punkt der Grundflaeche (Malerreihenfolge).
* Das reicht hier: die Koerper stehen nebeneinander auf dem Boden. Die
* Wiederkehr durchdringt zwar das Hausdach, liegt aber suedlich davon und
* damit ohnehin weiter vorn - genau so faelscht man einen Kreuzgiebel, ohne
* Flaechen verschneiden zu muessen.
*
* Returns:
* ["polygone" => [["punkte" => [[x,y],...], "farbe" => "#rrggbb"], ...],
* "anker" => [Raumname => [x, y], ...]] Mitte der Deckflaeche
*/
function aussenZeichnung()
{
$flaechen = aussenFlaechen();
$einpassung = aussenEinpassung($flaechen);
if (!$einpassung) {
return ["polygone" => [], "anker" => []];
}
$stuecke = [];
foreach ($flaechen as $f) {
$ecken = aussenUmlauf($f["ecken"]);
$kanten = aussenKanten($ecken);
$z = $f["z"] ?? 0;
$wand = $f["hoehe"] ?? 0;
$dach = $f["dach"] ?? null;
$farbe = $f["farbe"] ?? "#8a9099";
$offen = $f["offen"] ?? [];
$halb = $f["halb"] ?? [];
// Wandstaerke gibt es nur, wo sie zu sehen ist - an einer Oeffnung.
// Bei einem geschlossenen Koerper waere sie Rechenarbeit fuer nichts.
$dicke = ($offen || $halb) ? ($f["dicke"] ?? 0.2) : 0.0;
// --- Hoehe der Oberkante je Ecke ---------------------------------
//
// Beim Pultdach faellt sie zu einer Seite ab. Die Fallrichtung kommt
// aus der Aussennormalen der genannten Kante und nicht aus einem
// festen Nord-Sued-Vektor: das Grundstueck steht schief, und ein
// First soll parallel zur Traufe laufen und nicht fast parallel.
$zOben = array_fill(0, count($ecken), $z + $wand);
if ($dach && $dach["art"] === "pult") {
$tief = aussenKante($kanten, $dach["tief"]);
if ($tief) {
$werte = [];
foreach ($ecken as $p) {
$werte[] = $p[0] * $tief["nx"] + $p[1] * $tief["ny"];
}
$lo = min($werte);
$hi = max($werte);
foreach ($ecken as $i => $p) {
$anteil = $hi > $lo ? ($hi - $werte[$i]) / ($hi - $lo) : 0;
$zOben[$i] = $z + $wand + $dach["hoehe"] * $anteil;
}
}
}
$unten = [];
$oben = [];
foreach ($ecken as $i => $p) {
$unten[] = aussenPunkt($einpassung, $p[0], $p[1], $z);
$oben[] = aussenPunkt($einpassung, $p[0], $p[1], $zOben[$i]);
}
$polygone = [];
$tiefe = INF;
foreach ($ecken as $p) {
$tiefe = min($tiefe, aussenPunkt($einpassung, $p[0], $p[1], 0)[1]);
}
// --- Boden -------------------------------------------------------
//
// Nur wo eine Seite offen ist. Bei einem geschlossenen Koerper sieht
// man ihn nie, bei einem offenen dagegen sofort - ohne ihn schaut man
// durch den Carport auf den schwarzen Hintergrund, und das liest sich
// als Loch und nicht als Unterstand.
if ($wand > 0 && ($offen || $halb)) {
$boden = [];
foreach ($ecken as $pkt) {
$boden[] = aussenPunkt($einpassung, $pkt[0], $pkt[1], $z);
}
$polygone[] = ["punkte" => $boden, "farbe" => aussenSchatten($farbe, 0.55),
"was" => "Boden"];
}
// --- Waende ------------------------------------------------------
$waende = [];
if ($wand > 0) {
foreach ($kanten as $k) {
if (in_array($k["richtung"], $offen, true)) {
continue;
}
$i = $k["i"];
$j = ($i + 1) % count($ecken);
$von = $ecken[$i];
$bis = $ecken[$j];
$zVon = $zOben[$i];
$zBis = $zOben[$j];
if (in_array($k["richtung"], $halb, true)) {
// Halb offene Seite: stehen bleibt die Haelfte, die von
// der offenen Seite weiter weg liegt - vorn ist offen.
$offeneKante = $offen ? aussenKante($kanten, $offen[0]) : null;
$mitteKante = [($von[0] + $bis[0]) / 2, ($von[1] + $bis[1]) / 2];
$zMitte = ($zVon + $zBis) / 2;
if ($offeneKante) {
$wertVon = $von[0] * $offeneKante["nx"] + $von[1] * $offeneKante["ny"];
$wertBis = $bis[0] * $offeneKante["nx"] + $bis[1] * $offeneKante["ny"];
if ($wertVon > $wertBis) {
$bis = $mitteKante;
$zBis = $zMitte;
} else {
$von = $mitteKante;
$zVon = $zMitte;
}
}
}
// Eine Wand ist ein Kasten und keine Flaeche. Sichtbar wird
// die Dicke nur an einer Oeffnung - dort steht sonst ein
// Blatt Papier im Raum, und genau das sah seltsam aus.
// Innenseite, Oberkante und die beiden Schnittkanten kommen
// deshalb nur dazu, wo der Koerper ueberhaupt offen ist.
$innenVon = [$von[0] - $k["nx"] * $dicke, $von[1] - $k["ny"] * $dicke];
$innenBis = [$bis[0] - $k["nx"] * $dicke, $bis[1] - $k["ny"] * $dicke];
$seiten = [];
$viereck = function ($a, $za, $b, $zb, $c, $zc, $d, $zd) {
return [[$a[0], $a[1], $za], [$b[0], $b[1], $zb],
[$c[0], $c[1], $zc], [$d[0], $d[1], $zd]];
};
// Aussenseite
$seiten[] = [$viereck($von, $z, $bis, $z, $bis, $zBis, $von, $zVon),
aussenBeleuchtung($k["nx"], $k["ny"]),
"Wand " . $k["richtung"] . " aussen"];
if ($dicke > 0) {
// Innenseite - umgekehrter Umlauf, damit sie zaehlt, wenn
// man durch die Oeffnung hineinsieht.
$seiten[] = [$viereck($innenBis, $z, $innenVon, $z,
$innenVon, $zVon, $innenBis, $zBis),
0.60, "Wand " . $k["richtung"] . " innen"];
// Oberkante
$seiten[] = [$viereck($von, $zVon, $bis, $zBis,
$innenBis, $zBis, $innenVon, $zVon),
PLAN_WAAGERECHT,
"Wand " . $k["richtung"] . " Oberkante"];
// Die beiden Schnittkanten an den Enden der Wand
// Eine Schnittkante steht quer zur Wand und bekommt deshalb
// ihr eigenes Licht. Mit einem festen Wert sprangen die
// Ecken je nach Wandhelligkeit als Pfosten hervor.
$laengsL = hypot($bis[0] - $von[0], $bis[1] - $von[1]);
$lx = $laengsL > 0 ? ($bis[0] - $von[0]) / $laengsL : 0;
$ly = $laengsL > 0 ? ($bis[1] - $von[1]) / $laengsL : 0;
$seiten[] = [$viereck($innenVon, $z, $von, $z, $von, $zVon, $innenVon, $zVon),
aussenBeleuchtung(-$lx, -$ly),
"Wand " . $k["richtung"] . " Schnitt A"];
$seiten[] = [$viereck($bis, $z, $innenBis, $z, $innenBis, $zBis, $bis, $zBis),
aussenBeleuchtung($lx, $ly),
"Wand " . $k["richtung"] . " Schnitt B"];
}
$etwasSichtbar = false;
foreach ($seiten as $seite) {
$projiziert = [];
foreach ($seite[0] as $pkt) {
$projiziert[] = aussenPunkt($einpassung, $pkt[0], $pkt[1], $pkt[2]);
}
if (!aussenSichtbar($projiziert)) {
continue;
}
// Erst sammeln - gemalt wird nach der Schleife, von hinten
// nach vorn. In Kantenreihenfolge gemalt uebermalte die
// Innenseite der hinteren Wand die Aussenseite der
// vorderen, und der Koerper sah aus wie ein Tisch, durch
// den man hindurchschaut.
$waende[] = ["tiefe" => aussenTiefe($seite[0]),
"punkte" => $projiziert,
"farbe" => aussenSchatten($farbe, $seite[1]),
"was" => ($f["raum"] ?? "?") . ": " . $seite[2]];
$etwasSichtbar = true;
}
if (!$etwasSichtbar) {
continue;
}
// Das Tor sitzt mittig in seiner Wand und reicht nicht ganz
// bis unter die Traufe.
if (($f["tor"] ?? null) === $k["richtung"]) {
$torHoch = min(2.2, $z + min($zVon, $zBis) - $z);
$a = 0.2;
$b = 0.8;
$pa = [$von[0] + ($bis[0] - $von[0]) * $a, $von[1] + ($bis[1] - $von[1]) * $a];
$pb = [$von[0] + ($bis[0] - $von[0]) * $b, $von[1] + ($bis[1] - $von[1]) * $b];
$torWelt = [[$pa[0], $pa[1], $z], [$pb[0], $pb[1], $z],
[$pb[0], $pb[1], $z + $torHoch], [$pa[0], $pa[1], $z + $torHoch]];
$torPunkte = [];
foreach ($torWelt as $pkt) {
$torPunkte[] = aussenPunkt($einpassung, $pkt[0], $pkt[1], $pkt[2]);
}
// Ein Haar naeher als seine Wand, damit es auf ihr liegt
// und nicht neben ihr einsortiert wird.
$waende[] = ["tiefe" => aussenTiefe($torWelt) + 0.001,
"punkte" => $torPunkte,
"farbe" => aussenSchatten($farbe, 0.52),
"was" => ($f["raum"] ?? "?") . ": Tor"];
}
}
}
// Von hinten nach vorn, innerhalb dieses Koerpers.
usort($waende, function ($a, $b) {
return $a["tiefe"] <=> $b["tiefe"];
});
foreach ($waende as $wandflaeche) {
$polygone[] = $wandflaeche;
}
// --- Dach --------------------------------------------------------
if ($dach && $dach["art"] === "sattel" && count($ecken) === 4) {
// Der First laeuft entlang der laengeren Gebaeudeachse, wenn nichts
// anderes dasteht. Fest an Eckennummern haengt das nicht mehr: die
// Kanten sagen selbst, wo Norden ist.
$achse = $dach["achse"] ?? null;
if (!$achse) {
$nord = aussenKante($kanten, "N");
$ost = aussenKante($kanten, "O");
$achse = ($nord && $ost && $nord["laenge"] >= $ost["laenge"]) ? "OW" : "NS";
}
$enden = $achse === "OW" ? ["O", "W"] : ["N", "S"];
$seiten = $achse === "OW" ? ["N", "S"] : ["O", "W"];
$firstHoch = $z + $wand + $dach["hoehe"];
// Die Firstenden erst in Weltkoordinaten: gebraucht werden sie
// gleich zum Vergleichen von Abstaenden, und das geht nur vor der
// Projektion sinnvoll.
$firstWelt = [];
$first = [];
foreach ($enden as $r) {
$k = aussenKante($kanten, $r);
$firstWelt[$r] = $k ? $k["mitte"] : null;
$first[$r] = $k
? aussenPunkt($einpassung, $k["mitte"][0], $k["mitte"][1], $firstHoch)
: null;
}
// Giebeldreiecke schliessen die Schmalseiten. Ohne sie schiene an
// den Enden die Wand durch das Dach.
foreach ($enden as $r) {
$k = aussenKante($kanten, $r);
if (!$k || !$first[$r]) {
continue;
}
$giebel = [
aussenPunkt($einpassung, $k["a"][0], $k["a"][1], $z + $wand),
aussenPunkt($einpassung, $k["b"][0], $k["b"][1], $z + $wand),
$first[$r],
];
if (aussenSichtbar($giebel)) {
$polygone[] = ["punkte" => $giebel,
"farbe" => aussenSchatten($farbe,
aussenBeleuchtung($k["nx"], $k["ny"]))];
}
}
foreach ($seiten as $r) {
$k = aussenKante($kanten, $r);
if (!$k || !$firstWelt[$enden[0]] || !$firstWelt[$enden[1]]) {
continue;
}
// Jedes Traufende bekommt den Firstpunkt, der bei ihm liegt.
// In fester Reihenfolge genommen wird eine der beiden
// Dachflaechen zur Schleife und zeichnet einen Zacken quer
// ueber das Haus - genau das war zu sehen.
$nahB = $enden[0];
$nahA = $enden[1];
if (hypot($k["b"][0] - $firstWelt[$enden[0]][0], $k["b"][1] - $firstWelt[$enden[0]][1])
> hypot($k["b"][0] - $firstWelt[$enden[1]][0], $k["b"][1] - $firstWelt[$enden[1]][1])) {
$nahB = $enden[1];
$nahA = $enden[0];
}
$schraege = [
aussenPunkt($einpassung, $k["a"][0], $k["a"][1], $z + $wand),
aussenPunkt($einpassung, $k["b"][0], $k["b"][1], $z + $wand),
$first[$nahB],
$first[$nahA],
];
if (aussenSichtbar($schraege)) {
$polygone[] = ["punkte" => $schraege,
"farbe" => aussenSchatten($f["dachfarbe"] ?? "#9aa3ad",
aussenBeleuchtung($k["nx"], $k["ny"]))];
// Module auf genau dieser Schraege. Eingezogen wird hier
// im Bild und nicht in Metern: die Flaeche ist schon
// projiziert, und auf ihrer Ebene bleibt der Einzug auch
// nach der Projektion ein Einzug.
if (($f["pv"] ?? null) === $r) {
$polygone[] = ["punkte" => aussenEinzug($schraege, 4.0),
"farbe" => "#2f3a49"];
}
}
}
$deck = null;
} else {
// Flach, Pult oder gar kein Dach: die Oberseite ist die Deckflaeche.
//
// Eine Dachdicke braucht zwingend einen Ueberstand. Liegt das Dach
// buendig auf der Wand, sind Stirnflaeche und Wandflaeche
// deckungsgleich - die Dicke waere gerechnet und trotzdem nicht zu
// sehen, und der Koerper saehe aus, als fehle ihm eine Wand.
$dachdicke = $f["dachdicke"] ?? 0;
$dachEcken = $dachdicke > 0
? aussenEinzug($ecken, -($f["ueberstand"] ?? 0.25))
: $ecken;
$deckPunkte = [];
foreach ($dachEcken as $i => $pkt) {
$deckPunkte[] = aussenPunkt($einpassung, $pkt[0], $pkt[1], $zOben[$i]);
}
if ($dachdicke > 0) {
foreach ($dachEcken as $i => $pkt) {
$j = ($i + 1) % count($dachEcken);
$stirn = [
aussenPunkt($einpassung, $pkt[0], $pkt[1], $zOben[$i] - $dachdicke),
aussenPunkt($einpassung, $dachEcken[$j][0], $dachEcken[$j][1],
$zOben[$j] - $dachdicke),
aussenPunkt($einpassung, $dachEcken[$j][0], $dachEcken[$j][1], $zOben[$j]),
aussenPunkt($einpassung, $pkt[0], $pkt[1], $zOben[$i]),
];
if (aussenSichtbar($stirn)) {
// Die Stirn laeuft um den ganzen Koerper und zeigt
// dabei in alle Richtungen - ein fester Wert liesse
// die abgewandte Seite so hell wie die besonnte.
$sdx = $dachEcken[$j][0] - $pkt[0];
$sdy = $dachEcken[$j][1] - $pkt[1];
$polygone[] = ["punkte" => $stirn,
"farbe" => aussenSchatten($f["dachfarbe"] ?? $farbe,
aussenBeleuchtung($sdy, -$sdx))];
}
}
}
$polygone[] = ["punkte" => $deckPunkte,
"farbe" => aussenSchatten($f["dachfarbe"] ?? $farbe,
PLAN_WAAGERECHT)];
$deck = $dachEcken;
}
// Module auf einer flachen oder geneigten Deckflaeche - ein Stueck
// eingezogen, damit der Rand des Daches sichtbar bleibt.
if (!empty($f["pv"]) && $f["pv"] === true && $deck) {
$modulEcken = aussenEinzug($deck, 0.4);
$modul = [];
foreach ($modulEcken as $i => $pkt) {
$modul[] = aussenPunkt($einpassung, $pkt[0], $pkt[1], $zOben[$i] + 0.05);
}
$polygone[] = ["punkte" => $modul, "farbe" => "#2f3a49"];
}
$stuecke[] = ["tiefe" => $tiefe, "polygone" => $polygone,
"raum" => $f["raum"] ?? null, "mitte" => aussenMitte($oben)];
}
usort($stuecke, function ($a, $b) {
return $a["tiefe"] <=> $b["tiefe"];
});
$polygone = [];
$anker = [];
foreach ($stuecke as $s) {
foreach ($s["polygone"] as $p) {
$polygone[] = $p;
}
if ($s["raum"] !== null) {
$anker[$s["raum"]] = $s["mitte"];
}
}
return ["polygone" => $polygone, "anker" => $anker];
}
/** Die Zeichnung als fertige SVG-Zeilen. */
function aussenSvg()
{
$zeichnung = aussenZeichnung();
$svg = "";
foreach ($zeichnung["polygone"] as $p) {
$punkte = [];
foreach ($p["punkte"] as $pt) {
$punkte[] = round($pt[0], 1) . "," . round($pt[1], 1);
}
$svg .= '<polygon points="' . implode(" ", $punkte) . '" fill="' . $p["farbe"]
. '" stroke="#ffffff30" stroke-width=".4"/>' . "\n";
}
return $svg;
}