"#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; /** 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 [ ["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]]], // 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]]], ]; } /** * 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"]]; } /** 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 abdunkeln - die Seitenflächen liegen im Schatten. */ function aussenSchatten($hex, $faktor) { $n = hexdec(ltrim($hex, "#")); return sprintf("#%02x%02x%02x", (int) round(($n >> 16) * $faktor), (int) round((($n >> 8) & 255) * $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 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"] ?? []; // --- 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)]; } // --- 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)]; } } } // --- 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, 0.86)]; } } 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, "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 .= '' . "\n"; } return $svg; }