diff --git a/restricted/aussenplan.php b/restricted/aussenplan.php index bf26f68..6e9c7a1 100644 --- a/restricted/aussenplan.php +++ b/restricted/aussenplan.php @@ -102,39 +102,73 @@ const PLAN_RAND = 12; function aussenFlaechen() { return [ - ["raum" => null, "z" => 0, "hoehe" => 6, "dach" => 3, - "farbe" => "#c8cdd6", + ["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], + [19.9, 25.6], [20.2, 28.7], [19.0, 29.8], [19.3, 39.1]]], + + ["raum" => null, "z" => 0, "hoehe" => 0, "farbe" => "#6e7278", // Hof + "ecken" => [[9.3, 26.1], [8.9, 36.4], [11.0, 36.0], [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. + ["raum" => null, "z" => 0, "hoehe" => 6, "farbe" => "#d5d9df", + "dachfarbe" => "#9aa3ad", "dach" => ["art" => "sattel", "hoehe" => 3], "ecken" => [[36.8, 17.2], [39.2, 31.2], [24.2, 33.8], [21.8, 19.8]]], - ["raum" => null, "z" => 0, "hoehe" => 0, "dach" => 0, - "farbe" => "#57585a", - "ecken" => [[9.3, 26.1], [8.9, 36.4], [11.0, 36.0], [18.9, 34.7], [19.0, 29.8], [20.4, 28.7], [19.8, 25.6], [19.6, 24.3]]], - ["raum" => "Carport EG", "z" => 0, "hoehe" => 2.2, "dach" => 0, - "farbe" => "#3f4a63", - "ecken" => [[18.9, 34.7], [19.6, 38.9], [10.7, 40.5], [10.0, 36.2]]], - ["raum" => "Carport OG", "z" => 0, "hoehe" => 2.5, "dach" => 0, - "farbe" => "#6f7680", - "ecken" => [[12.2, 19.1], [13.2, 25.4], [9.3, 26.1], [8.2, 19.8]]], - ["raum" => "Veranda EG", "z" => 0, "hoehe" => 0, "dach" => 0, - "farbe" => "#9a7b52", - "ecken" => [[24.1, 19.4], [26.1, 30.8], [23.7, 31.2], [21.8, 19.8]]], - ["raum" => "Veranda OG", "z" => 3, "hoehe" => 0, "dach" => 0, - "farbe" => "#c9a978", - "ecken" => [[24.1, 19.4], [26.1, 30.8], [23.7, 31.2], [21.8, 19.8]]], - ["raum" => "Terrasse EG", "z" => 0, "hoehe" => 0, "dach" => 0, - "farbe" => "#9a7b52", - "ecken" => [[23.7, 31.2], [28.6, 30.3], [29.1, 32.9], [24.2, 33.8]]], - ["raum" => "Terrasse OG", "z" => 3, "hoehe" => 0, "dach" => 0, - "farbe" => "#c9a978", - "ecken" => [[23.7, 31.2], [28.6, 30.3], [29.1, 32.9], [24.2, 33.8]]], - ["raum" => "Pergola", "z" => 0, "hoehe" => 0, "dach" => 0, - "farbe" => "#7a6a8f", + + // 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" => "#d5d9df", + "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. + ["raum" => "Carport OG", "z" => 0, "hoehe" => 2.5, "farbe" => "#b8b3aa", + "dachfarbe" => "#a8a29a", "pv" => true, + "dach" => ["art" => "pult", "hoehe" => 0.8, "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, ist nach Westen offen, und + // seine Nord- und Suedwand stehen nur in der hinteren Haelfte. + ["raum" => "Carport EG", "z" => 0, "hoehe" => 2.2, "farbe" => "#b8b3aa", + "dachfarbe" => "#a8a29a", "pv" => true, + "dach" => ["art" => "pult", "hoehe" => 0.7, "tief" => "S"], + "offen" => ["W"], "halb" => ["N", "S"], + "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 quer zu dem, was die laengere Achse nahelegen wuerde - + // bei einem fast quadratischen Grundriss entscheidet das sonst der + // Zufall der zweiten Nachkommastelle. + ["raum" => "Garage", "z" => 0, "hoehe" => 2.5, "farbe" => "#c9ced5", + "dachfarbe" => "#7d9470", "tor" => "S", + "dach" => ["art" => "sattel", "hoehe" => 1.5, "achse" => "OW"], + "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]]], + + ["raum" => "Pergola", "z" => 0, "hoehe" => 0, "farbe" => "#9aa3ad", "ecken" => [[48.0, 36.5], [43.8, 36.4], [46.0, 31.5], [49.4, 32.4]]], - ["raum" => "Garage", "z" => 0, "hoehe" => 2.5, "dach" => 1.5, - "farbe" => "#8a9099", - "ecken" => [[12.2, 19.1], [13.2, 25.4], [19.6, 24.3], [18.5, 18.0]]], - ["raum" => "Garten", "z" => 0, "hoehe" => 0, "dach" => 0, - "farbe" => "#2e5c33", - "ecken" => [[8.6, 40.8], [7.9, 45.2], [49.0, 38.2], [50.6, 12.5], [23.7, 17.1], [24.1, 19.4], [36.8, 17.2], [39.2, 31.2], [24.2, 33.8], [22.5, 23.8], [19.8, 25.6], [20.4, 28.7], [19.0, 29.8], [19.6, 38.9]]], ]; } @@ -171,7 +205,8 @@ function aussenEinpassung($flaechen) $xs = []; $ys = []; foreach ($flaechen as $f) { - $oben = ($f["z"] ?? 0) + ($f["hoehe"] ?? 0) + ($f["dach"] ?? 0); + // "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); @@ -232,17 +267,118 @@ function aussenSchatten($hex, $faktor) (int) round(($n & 255) * $faktor)); } +/** + * 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 Grundfläche (Malerreihenfolge). - * Das reicht hier: die Körper stehen nebeneinander auf dem Boden und - * durchdringen sich nicht. Bei ineinander verschachtelten Körpern bräuchte es - * mehr, aber ein Grundstück ist keine Baugruppe. + * 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 Deckfläche + * "anker" => [Raumname => [x, y], ...]] Mitte der Deckflaeche */ function aussenZeichnung() { @@ -254,71 +390,213 @@ function aussenZeichnung() $stuecke = []; foreach ($flaechen as $f) { - $z = $f["z"] ?? 0; - $hoehe = $f["hoehe"] ?? 0; - $dach = $f["dach"] ?? 0; - $farbe = $f["farbe"] ?? "#8a9099"; + $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"] ?? []; - $tiefe = INF; - foreach ($f["ecken"] as $e) { - $p = aussenPunkt($einpassung, $e[0], $e[1], 0); - $tiefe = min($tiefe, $p[1]); + // --- 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 ($f["ecken"] as $e) { - $unten[] = aussenPunkt($einpassung, $e[0], $e[1], $z); - $oben[] = aussenPunkt($einpassung, $e[0], $e[1], $z + $hoehe); + foreach ($ecken as $i => $p) { + $unten[] = aussenPunkt($einpassung, $p[0], $p[1], $z); + $oben[] = aussenPunkt($einpassung, $p[0], $p[1], $zOben[$i]); } $polygone = []; - if ($hoehe > 0) { - $anzahl = count($f["ecken"]); - for ($i = 0; $i < $anzahl; $i++) { - $j = ($i + 1) % $anzahl; - $polygone[] = ["punkte" => [$unten[$i], $unten[$j], $oben[$j], $oben[$i]], - "farbe" => aussenSchatten($farbe, 0.62)]; + $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)]; + } + + // --- 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; + } + } + } + + $wandflaeche = [ + aussenPunkt($einpassung, $von[0], $von[1], $z), + aussenPunkt($einpassung, $bis[0], $bis[1], $z), + aussenPunkt($einpassung, $bis[0], $bis[1], $zBis), + aussenPunkt($einpassung, $von[0], $von[1], $zVon), + ]; + if (!aussenSichtbar($wandflaeche)) { + continue; + } + $polygone[] = ["punkte" => $wandflaeche, "farbe" => aussenSchatten($farbe, 0.72)]; + + // 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]; + $polygone[] = ["punkte" => [ + aussenPunkt($einpassung, $pa[0], $pa[1], $z), + aussenPunkt($einpassung, $pb[0], $pb[1], $z), + aussenPunkt($einpassung, $pb[0], $pb[1], $z + $torHoch), + aussenPunkt($einpassung, $pa[0], $pa[1], $z + $torHoch), + ], "farbe" => aussenSchatten($farbe, 0.52)]; + } } } - $polygone[] = ["punkte" => $oben, "farbe" => $farbe]; - if ($dach > 0 && count($f["ecken"]) === 4) { - $e = $f["ecken"]; - // Der First laeuft ueber die laengere Achse - er verbindet die - // Mitten der beiden kuerzeren Seiten. Fest verdrahtet waere das - // bei einem quer stehenden Gebaeude falsch herum. - $laenge = function ($a, $b) { - return hypot($a[0] - $b[0], $a[1] - $b[1]); - }; - $langsSeite01 = $laenge($e[0], $e[1]) >= $laenge($e[1], $e[2]); - $paar = $langsSeite01 ? [[0, 3], [1, 2]] : [[0, 1], [2, 3]]; + // --- 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 ($paar as $pp) { - $first[] = aussenPunkt($einpassung, - ($e[$pp[0]][0] + $e[$pp[1]][0]) / 2, - ($e[$pp[0]][1] + $e[$pp[1]][1]) / 2, - $z + $hoehe + $dach); + 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; } - $dachfarbe = $f["dachfarbe"] ?? "#b5563a"; - // Erst die beiden Giebeldreiecke, dann die Dachflaechen darueber: - // ohne sie klaffte an den Schmalseiten ein Loch, durch das die - // Wand darunter zu sehen war. - foreach ($paar as $i => $pp) { - $polygone[] = ["punkte" => [$oben[$pp[0]], $oben[$pp[1]], $first[$i]], - "farbe" => aussenSchatten($farbe, 0.88)]; + // 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, 0.86)]; + } } - if ($langsSeite01) { - $polygone[] = ["punkte" => [$oben[0], $oben[1], $first[1], $first[0]], - "farbe" => $dachfarbe]; - $polygone[] = ["punkte" => [$oben[3], $oben[2], $first[1], $first[0]], - "farbe" => aussenSchatten($dachfarbe, 0.78)]; - } else { - $polygone[] = ["punkte" => [$oben[1], $oben[2], $first[1], $first[0]], - "farbe" => $dachfarbe]; - $polygone[] = ["punkte" => [$oben[0], $oben[3], $first[1], $first[0]], - "farbe" => aussenSchatten($dachfarbe, 0.78)]; + 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", + $r === "S" || $r === "W" ? 1.0 : 0.82)]; + } } + $deck = null; + } else { + // Flach, Pult oder gar kein Dach: die Oberseite ist die Deckflaeche. + $polygone[] = ["punkte" => $oben, "farbe" => $f["dachfarbe"] ?? $farbe]; + $deck = $oben; + } + + // Die Module liegen plan auf der Dachflaeche - ein Stueck eingezogen, + // damit der Rand des Daches sichtbar bleibt. + if (!empty($f["pv"]) && $deck) { + $modulEcken = aussenEinzug($ecken, 0.4); + $modul = []; + foreach ($modulEcken as $i => $p) { + $modul[] = aussenPunkt($einpassung, $p[0], $p[1], $zOben[$i] + 0.05); + } + $polygone[] = ["punkte" => $modul, "farbe" => "#2f3a49"]; } $stuecke[] = ["tiefe" => $tiefe, "polygone" => $polygone,