commit 70d239fe9c7ece1ab1b632ffd7b3bcb915bde830 Author: Damian Kellenberger Date: Tue Sep 8 22:59:37 2026 +0200 Begehbare 3D-Visualisierung des Gartens Egglihaldenstrasse 9a Geometrie massstaeblich aus dem Gartenplan-PDF (1:100) extrahiert: Belaege, Mauern, Pflanzenstandorte und 46 Hoehenkoten. Wohnhaus nach Gartenplan und den Fassadenplaenen von EHLE Architekten. - tools/: Pipeline PDF -> Szenenmodell (MuPDF, Node) - src/: three.js-Szene mit prozeduralen Texturen, Rundgang und Uebersicht - Docker/Traefik fuer garten.aicds.ch, Gitea-Workflow auf main Co-Authored-By: Claude Opus 5 diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..e7d678e --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "garten-dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 5173 + } + ] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..09fb03e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +.git +.gitea +.claude +tools/out +Info Dateien +docs +*.md +.claude-launch-static.mjs diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..6ac30db --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,20 @@ +name: Deploy App +on: + push: + branches: + - main +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + - name: Deploy to Server + run: | + APP_DIR="/opt/aicds/apps/${GITHUB_REPOSITORY#*/}" + mkdir -p $APP_DIR + cp -a . $APP_DIR/ + cd $APP_DIR + docker compose down || true + docker compose up -d --build + docker image prune -f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3cc68bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +tools/out/ +.claude-launch-static.mjs +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e9c8144 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# --- Build ------------------------------------------------------------------ +FROM node:22-alpine AS build +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY tsconfig.json vite.config.ts index.html ./ +COPY src ./src +COPY public ./public +RUN npm run build + +# --- Laufzeit --------------------------------------------------------------- +FROM nginx:1.27-alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ + CMD wget -qO- http://localhost/ >/dev/null || exit 1 diff --git a/Info Dateien/20210427 - Projektpläne Haus 9a.pdf b/Info Dateien/20210427 - Projektpläne Haus 9a.pdf new file mode 100644 index 0000000..f3b9aac Binary files /dev/null and b/Info Dateien/20210427 - Projektpläne Haus 9a.pdf differ diff --git a/Info Dateien/AICDS - Technisches-Konzept - v3.docx b/Info Dateien/AICDS - Technisches-Konzept - v3.docx new file mode 100644 index 0000000..914e10e Binary files /dev/null and b/Info Dateien/AICDS - Technisches-Konzept - v3.docx differ diff --git a/Info Dateien/Gartenplan 1 Jasmin und Damian Kellenberger.dwg b/Info Dateien/Gartenplan 1 Jasmin und Damian Kellenberger.dwg new file mode 100644 index 0000000..83f5abf Binary files /dev/null and b/Info Dateien/Gartenplan 1 Jasmin und Damian Kellenberger.dwg differ diff --git a/Info Dateien/Gartenplan 1 Jasmin und Damian Kellenberger.pdf b/Info Dateien/Gartenplan 1 Jasmin und Damian Kellenberger.pdf new file mode 100644 index 0000000..cdf1b81 Binary files /dev/null and b/Info Dateien/Gartenplan 1 Jasmin und Damian Kellenberger.pdf differ diff --git a/Info Dateien/WhatsApp Image 2026-09-08 at 21.49.26 (1).jpeg b/Info Dateien/WhatsApp Image 2026-09-08 at 21.49.26 (1).jpeg new file mode 100644 index 0000000..0f8f7db Binary files /dev/null and b/Info Dateien/WhatsApp Image 2026-09-08 at 21.49.26 (1).jpeg differ diff --git a/Info Dateien/WhatsApp Image 2026-09-08 at 21.49.26.jpeg b/Info Dateien/WhatsApp Image 2026-09-08 at 21.49.26.jpeg new file mode 100644 index 0000000..61a207d Binary files /dev/null and b/Info Dateien/WhatsApp Image 2026-09-08 at 21.49.26.jpeg differ diff --git a/Info Dateien/WhatsApp Image 2026-09-08 at 21.49.27.jpeg b/Info Dateien/WhatsApp Image 2026-09-08 at 21.49.27.jpeg new file mode 100644 index 0000000..399629d Binary files /dev/null and b/Info Dateien/WhatsApp Image 2026-09-08 at 21.49.27.jpeg differ diff --git a/Info Dateien/WhatsApp Image 2026-09-08 at 21.50.32.jpeg b/Info Dateien/WhatsApp Image 2026-09-08 at 21.50.32.jpeg new file mode 100644 index 0000000..d7b6c5d Binary files /dev/null and b/Info Dateien/WhatsApp Image 2026-09-08 at 21.50.32.jpeg differ diff --git a/Info Dateien/WhatsApp Image 2026-09-08 at 21.50.33.jpeg b/Info Dateien/WhatsApp Image 2026-09-08 at 21.50.33.jpeg new file mode 100644 index 0000000..0adc46f Binary files /dev/null and b/Info Dateien/WhatsApp Image 2026-09-08 at 21.50.33.jpeg differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..faf483e --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# Gartenvisualisierung Egglihaldenstrasse 9a + +Begehbare 3D-Visualisierung des geplanten Gartens von Jasmin und Damian +Kellenberger, Egglihaldenstrasse 9a, 9030 Abtwil SG. + +Live: **https://garten.aicds.ch** + +Die gesamte Geometrie ist **massstäblich aus dem Gartenplan** abgeleitet – nicht +nachmodelliert. Ein Node-Werkzeug liest die Vektordaten direkt aus dem Plan-PDF, +rechnet sie in Meter um und erzeugt daraus das Szenenmodell. + +--- + +## Grundlagen + +| Quelle | Inhalt | +|---|---| +| `Info Dateien/Gartenplan 1 …pdf` | Garten Design AG, 14.08.2026, Massstab 1:100, A1 | +| `Info Dateien/20210427 - Projektpläne Haus 9a.pdf` | EHLE Architekten AG, Grundrisse, Fassaden, Umgebung | +| `Info Dateien/WhatsApp Image …jpeg` | Fotos des Ist-Zustands (Fassade, Beläge, Nachbarschaft) | + +Die DWG wird nicht benötigt – das PDF enthält dieselbe Vektorgeometrie. + +### Koordinatensystem + +Alles rechnet im **Site-Frame**, in Metern: + +- **X** – Hausachse ENE, Peilung 61.1615° gegen Nord +- **Z** – senkrecht dazu, SSE, Peilung 151.1615° +- **Y** – Höhe über dem Fixpunkt `0.00` = **651.17 m ü. M.** (OK Fertigboden EG) + +Der Nordpfeil des Plans zeigt exakt zur Blattoberkante, das Zeichnungsraster +liegt um −28.8385° gedreht dazu. Der Massstab ist über das Plattenraster +verifiziert: die Keramikplatten messen sich auf 0.600 m genau. + +### Höhen + +| Bezug | relativ | absolut | +|---|---|---| +| Feuerwehrzufahrt, Nordwestecke | −2.08 | 648.09 m | +| Nachbargarten Zollet (Haus 9, OK FB EG) | −1.70 | 649.47 m | +| Hauptsitzplatz Travertin | −0.30 … −0.02 | 650.87 … 651.15 m | +| Fixpunkt Hauseingang | 0.00 | 651.17 m | +| Ostseite beim Apfelbaum | +0.63 | 651.80 m | +| OK Fertigboden Obergeschoss | +2.90 | 654.07 m | +| OK Attika Haus 9a | +6.15 | 657.32 m | + +--- + +## Datenpipeline + +```bash +npm run data # extract-plan + build-site +``` + +1. **`tools/extract-plan.mjs`** – öffnet das Plan-PDF mit MuPDF (WASM) und + protokolliert alle Zeichenoperationen mit. Ergebnis: rund 5400 Polygone, + 234 Pflanzenkreise, 229 Beschriftungen und 46 Höhenkoten, transformiert in + den Site-Frame. → `tools/out/plan-raw.json` + +2. **`tools/authoring.mjs`** – handgepflegte Ergänzungen: Zuordnung der + Planfarben zu Belagsarten, Mauerachsen und -höhen, Gebäudekubatur, + Geländezonen, Standorte. Alles kommentiert und auf Plan oder + Architekturpläne zurückgeführt. + +3. **`tools/build-site.mjs`** – klassiert die Flächen, leitet Pflanzen und + Leuchten ab, interpoliert das Geländeraster (0.25 m) aus den Höhenkoten je + Zone und schreibt `public/data/site.json` (rund 110 kB). + +Zur Kontrolle: `node tools/debug-svg.mjs` zeichnet die extrahierte Geometrie als +SVG-Draufsicht, `node tools/inspect-colors.mjs` listet die Planfarben mit den +nächstgelegenen Beschriftungen. + +### Warum Zonen statt eines einzigen Geländes + +Das Grundstück fällt über rund 3.5 m ab, die Sprünge liegen aber nicht im +Gelände, sondern in den Stützbauten: Travertinmauer zum Nachbargrundstück, +Winkelelemente aus Beton entlang der Feuerwehrzufahrt, Betonmauer im Süden. +Deshalb wird je Zone getrennt interpoliert – über die Zonengrenze hinweg nie. +Die Mauern erhalten ihr Kronen- und Fussprofil aus dem Gelände beider Seiten. + +--- + +## Anwendung + +```bash +npm install +npm run dev # http://localhost:5173 +npm run build # dist/, rund 640 kB +``` + +Stack: Vite + TypeScript + three.js. **Keine externen Assets** – sämtliche +Texturen (Travertin, Keramik, Tegula, Rasen, Kies, Beton, Holzschalung, +Scheiterwand) werden zur Laufzeit prozedural auf ein Canvas gezeichnet. Die UVs +sind weltbezogen, damit die Plattenraster über Flächengrenzen hinweg durchlaufen +– so wie auf dem Plan. + +### Bedienung + +| | | +|---|---| +| Rundgang | Klicken, dann `W` `A` `S` `D`, `Shift` zum Rennen, `Esc` gibt die Maus frei | +| Handy | links Joystick, rechts wischen zum Umsehen | +| Übersicht | ziehen zum Drehen, Mausrad zum Zoomen | +| Tageszeit | Regler oben rechts, Sonnenstand astronomisch für Abtwil SG gerechnet | + +Der Rundgang folgt dem Gelände, steigt Stufen bis 45 cm und wird von Mauern, +Gebäuden, Hecken und Objekten aufgehalten. Ab Dämmerung schalten die 17 Leuchten +des Plans automatisch ein. + +### Aufbau + +``` +src/ + site.ts Datenmodell und Höhen-Sampler + scene/textures.ts prozedurale Beläge und Materialien + scene/geometry.ts Polygone auf das Gelände legen, Mauern extrudieren + scene/terrain.ts Geländeraster und Umland + scene/surfaces.ts Beläge + scene/structures.ts Mauern, Stelen, Objekte, Leuchten + scene/vegetation.ts Gehölze, Hecken, Gräser, Stauden + scene/building.ts Wohnhaus 9a und Nachbargebäude + scene/environment.ts Himmel, Sonnenstand, Licht + controls/walker.ts Begehen mit Kollision und Geländeführung + ui/hud.ts Bedienoberfläche +``` + +--- + +## Deployment + +Push auf `main` löst den Gitea-Workflow aus; die App landet unter +`/opt/aicds/apps/Gartenvisualisierung/` und wird per `docker compose` gebaut. +Traefik-Router `garten`, Host `garten.aicds.ch`, Netzwerk `agent-net`. + +```bash +docker compose config # Prüfung vor dem Push +docker compose up -d --build +``` + +Das Image enthält nur `dist/` hinter nginx – die Planunterlagen und Fotos aus +`Info Dateien/` werden über `.dockerignore` ausgeschlossen und gelangen nie in +den Webcontainer. + +--- + +## Was bewusst interpretiert ist + +Der Plan ist ein Situationsplan; einiges steht nur als Beschriftung darin. Diese +Annahmen stecken in `tools/authoring.mjs` und sind dort einzeln kommentiert: + +- **Mauerhöhen** aus den Höhenkoten beidseits abgeleitet; die gelbe Kote `25` + bei der Sitzmauer als 25 cm Aufkantung gelesen, `35` bei den Heidelbeeren + analog. +- **Lamellenwand zum Sitzen** als senkrechte Holzlamellen im Planrhythmus + (10 cm breit, 35 cm tief, rund 90 cm Achsabstand), passend zum Foto des + Ist-Zustands. +- **Wohnhaus** in Kubatur und Öffnungen nach Gartenplan und Fassadenplänen, + Materialisierung nach den Fotos: vertikale Holzschalung graubraun, heller + Betonsockel, Flachdach mit extensiver Begrünung. +- **Nachbarhaus Zollet** als einfaches Kontextvolumen, 1.70 m tiefer liegend. +- **Solitärgehölze** mit den Kronenradien des Plans; Wuchshöhen als + Erfahrungswerte. +- **Gartenhaus, Spielhaus, Flammengrill, Bänkli** in Lage und Grundfläche nach + Plan, in der Ausformulierung frei. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..caad123 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + app: + build: . + restart: unless-stopped + networks: + - agent-net + labels: + - "traefik.enable=true" + - "traefik.http.routers.garten.rule=Host(`garten.aicds.ch`)" + - "traefik.http.routers.garten.entrypoints=websecure" + - "traefik.http.routers.garten.tls.certresolver=myresolver" + - "traefik.http.services.garten.loadbalancer.server.port=80" + +networks: + agent-net: + external: true diff --git a/index.html b/index.html new file mode 100644 index 0000000..49189d6 --- /dev/null +++ b/index.html @@ -0,0 +1,65 @@ + + + + + + + + Garten Egglihaldenstrasse 9a · 3D-Visualisierung + + + + + +
+
+
Garten Egglihaldenstrasse 9a
+
+
Gartenplan wird aufgebaut …
+
+
+ + + + + + + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d4a10e9 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,26 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + gzip_min_length 1024; + + # Statische Assets tragen einen Hash im Namen und duerfen lange liegenbleiben. + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Das Szenenmodell wird bei jedem Plan-Update neu gebaut. + location /data/ { + expires 1h; + add_header Cache-Control "public"; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..edf3523 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1254 @@ +{ + "name": "gartenvisualisierung", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gartenvisualisierung", + "version": "0.1.0", + "dependencies": { + "three": "^0.172.0" + }, + "devDependencies": { + "@types/three": "^0.172.0", + "mupdf": "^1.28.1", + "typescript": "^5.7.3", + "vite": "^6.0.11" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.172.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.172.0.tgz", + "integrity": "sha512-LrUtP3FEG26Zg5WiF0nbg8VoXiKokBLTcqM2iLvM9vzcfEiYmmBAPGdBgV0OYx9fvWlY3R/3ERTZcD9X5sc0NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.72", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.72.tgz", + "integrity": "sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mupdf": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/mupdf/-/mupdf-1.28.1.tgz", + "integrity": "sha512-Gi11Ow2G1SlrXKJNZBL1eAIGFVih5+4ZKqjptamTVaj/5hnlrcVrVbyb7lHE2lfFKdxZPyv9ZtfOOq7XgjzEig==", + "dev": true, + "license": "AGPL-3.0-or-later" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/three": { + "version": "0.172.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.172.0.tgz", + "integrity": "sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..13d7a87 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "gartenvisualisierung", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Begehbare 3D-Visualisierung des Gartens Kellenberger, Egglihaldenstrasse 9a, 9030 Abtwil", + "scripts": { + "extract": "node tools/extract-plan.mjs", + "build:site": "node tools/build-site.mjs", + "data": "npm run extract && npm run build:site", + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --port 4173" + }, + "devDependencies": { + "@types/three": "^0.172.0", + "mupdf": "^1.28.1", + "typescript": "^5.7.3", + "vite": "^6.0.11" + }, + "dependencies": { + "three": "^0.172.0" + }, + "allowScripts": { + "esbuild@0.25.12": true + } +} diff --git a/public/data/site.json b/public/data/site.json new file mode 100644 index 0000000..0702f2f --- /dev/null +++ b/public/data/site.json @@ -0,0 +1 @@ +{"meta":{"source":"Gartenplan 1 Jasmin und Damian Kellenberger.pdf","scale":"1:100","ptToM":0.035277777777777776,"gridAngleDeg":-28.8385,"northBearingOfSiteX":61.1615,"originLabel":"0.00 Fixpunkt","originAbsoluteMasl":651.17,"note":"X = Hausachse ENE, Z = SSE, Ursprung = Fixpunkt 0.00 beim Eingang","generated":"2026-09-08","location":{"name":"Egglihaldenstrasse 9a, 9030 Abtwil SG","lat":47.4206,"lon":9.2903,"northBearingOfSiteX":61.1615,"groundMasl":651.17},"plan":"Gartenplan 1, Garten Design AG, 14.08.2026, Massstab 1:100","architecture":"Haus 9a, EHLE Architekten AG, Projektplaene 27.04.2021"},"terrain":{"grid":{"x0":-26,"z0":-14.5,"cell":0.25,"nx":141,"nz":107},"heights":[-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.742,-1.743,-1.744,-1.745,-1.745,-1.746,-1.747,-1.747,-1.748,-1.748,-1.748,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.748,-1.748,-1.747,-1.746,-1.745,-1.744,-1.742,-1.739,-1.735,-1.731,-1.726,-1.72,-1.714,-1.707,-1.7,-1.693,-1.686,-1.679,-1.673,-1.668,-1.664,-1.662,-1.66,-1.659,-1.66,-1.661,-1.664,-1.666,-1.67,-1.673,-1.677,-1.681,-1.684,-1.687,-1.689,-1.692,-1.693,-1.695,-1.696,-1.697,-1.697,-1.698,-1.698,-1.698,-1.699,-1.699,-1.698,-1.698,-1.698,-1.697,-1.697,-1.696,-1.694,-1.692,-1.69,-1.686,-1.682,-1.677,-1.67,-1.662,-1.653,-1.643,-1.631,-1.62,-1.609,-1.599,-1.591,-1.585,-1.581,-1.58,-1.58,-1.583,-1.587,-1.591,-1.596,-1.601,-1.607,-1.611,-1.615,-1.619,-1.622,-1.625,-1.627,-1.629,-1.63,-1.63,-1.63,-1.63,-1.629,-1.7,-1.7,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.302,-1.641,-1.735,-1.744,-1.745,-1.746,-1.746,-1.747,-1.748,-1.748,-1.748,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.748,-1.748,-1.747,-1.746,-1.744,-1.742,-1.739,-1.736,-1.732,-1.726,-1.72,-1.713,-1.706,-1.698,-1.691,-1.684,-1.677,-1.671,-1.666,-1.662,-1.659,-1.658,-1.657,-1.658,-1.66,-1.662,-1.665,-1.669,-1.673,-1.677,-1.681,-1.684,-1.687,-1.69,-1.692,-1.694,-1.695,-1.697,-1.697,-1.698,-1.698,-1.699,-1.699,-1.699,-1.699,-1.699,-1.699,-1.699,-1.698,-1.697,-1.697,-1.695,-1.693,-1.691,-1.688,-1.683,-1.678,-1.671,-1.663,-1.653,-1.642,-1.631,-1.619,-1.607,-1.597,-1.589,-1.583,-1.579,-1.578,-1.579,-1.583,-1.587,-1.592,-1.598,-1.604,-1.609,-1.614,-1.619,-1.623,-1.626,-1.629,-1.631,-1.632,-1.633,-1.634,-1.634,-1.634,-1.61,-1.266,-0.293,0.48,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.632,-1.735,-1.744,-1.745,-1.746,-1.747,-1.747,-1.748,-1.748,-1.749,-1.749,-1.749,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.749,-1.749,-1.749,-1.748,-1.748,-1.747,-1.745,-1.743,-1.74,-1.736,-1.732,-1.726,-1.72,-1.713,-1.705,-1.697,-1.689,-1.681,-1.674,-1.668,-1.663,-1.659,-1.656,-1.655,-1.655,-1.655,-1.657,-1.66,-1.664,-1.668,-1.672,-1.677,-1.681,-1.684,-1.688,-1.69,-1.693,-1.695,-1.696,-1.697,-1.698,-1.698,-1.699,-1.699,-1.699,-1.699,-1.699,-1.699,-1.699,-1.699,-1.699,-1.698,-1.697,-1.696,-1.694,-1.692,-1.689,-1.684,-1.679,-1.672,-1.663,-1.653,-1.642,-1.629,-1.617,-1.605,-1.594,-1.585,-1.579,-1.576,-1.576,-1.578,-1.582,-1.587,-1.593,-1.6,-1.606,-1.612,-1.617,-1.622,-1.626,-1.629,-1.632,-1.634,-1.636,-1.637,-1.637,-1.637,-1.638,-1.544,-0.914,0.129,0.515,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.633,-1.735,-1.745,-1.746,-1.746,-1.747,-1.748,-1.748,-1.749,-1.749,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.749,-1.749,-1.749,-1.748,-1.747,-1.746,-1.743,-1.741,-1.737,-1.732,-1.726,-1.719,-1.711,-1.703,-1.694,-1.686,-1.678,-1.671,-1.664,-1.659,-1.655,-1.653,-1.652,-1.652,-1.653,-1.655,-1.658,-1.662,-1.667,-1.671,-1.676,-1.68,-1.684,-1.688,-1.691,-1.693,-1.695,-1.697,-1.698,-1.698,-1.699,-1.699,-1.699,-1.7,-1.7,-1.7,-1.7,-1.7,-1.699,-1.699,-1.699,-1.698,-1.697,-1.695,-1.693,-1.69,-1.685,-1.68,-1.673,-1.664,-1.653,-1.641,-1.628,-1.614,-1.601,-1.59,-1.581,-1.575,-1.573,-1.573,-1.576,-1.581,-1.587,-1.594,-1.601,-1.608,-1.614,-1.62,-1.625,-1.629,-1.633,-1.635,-1.637,-1.639,-1.64,-1.64,-1.64,-1.605,-1.265,-0.211,0.445,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.633,-1.735,-1.745,-1.746,-1.747,-1.747,-1.748,-1.748,-1.749,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.749,-1.749,-1.748,-1.747,-1.746,-1.744,-1.741,-1.737,-1.732,-1.725,-1.718,-1.709,-1.701,-1.691,-1.682,-1.674,-1.667,-1.66,-1.655,-1.652,-1.649,-1.648,-1.648,-1.65,-1.652,-1.656,-1.66,-1.665,-1.67,-1.675,-1.68,-1.684,-1.688,-1.691,-1.694,-1.695,-1.697,-1.698,-1.699,-1.699,-1.699,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.699,-1.699,-1.698,-1.697,-1.696,-1.694,-1.69,-1.686,-1.68,-1.673,-1.663,-1.652,-1.639,-1.625,-1.611,-1.597,-1.586,-1.577,-1.571,-1.568,-1.569,-1.573,-1.579,-1.586,-1.594,-1.602,-1.61,-1.617,-1.623,-1.628,-1.632,-1.636,-1.638,-1.64,-1.641,-1.642,-1.643,-1.643,-1.608,-1.197,0.07,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.633,-1.735,-1.745,-1.746,-1.747,-1.747,-1.748,-1.749,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.749,-1.749,-1.748,-1.746,-1.744,-1.741,-1.736,-1.731,-1.724,-1.716,-1.707,-1.697,-1.688,-1.678,-1.67,-1.662,-1.656,-1.651,-1.647,-1.645,-1.644,-1.645,-1.646,-1.649,-1.653,-1.657,-1.663,-1.668,-1.674,-1.679,-1.684,-1.688,-1.691,-1.694,-1.696,-1.697,-1.698,-1.699,-1.699,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.699,-1.699,-1.698,-1.696,-1.694,-1.691,-1.686,-1.68,-1.672,-1.663,-1.651,-1.637,-1.622,-1.607,-1.592,-1.58,-1.571,-1.565,-1.563,-1.565,-1.57,-1.576,-1.585,-1.594,-1.602,-1.611,-1.618,-1.625,-1.63,-1.635,-1.638,-1.641,-1.643,-1.644,-1.645,-1.645,-1.645,-1.61,-1.198,0.104,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.633,-1.735,-1.745,-1.746,-1.747,-1.747,-1.748,-1.749,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.749,-1.749,-1.748,-1.746,-1.744,-1.74,-1.736,-1.73,-1.722,-1.713,-1.704,-1.693,-1.683,-1.673,-1.665,-1.657,-1.651,-1.646,-1.643,-1.641,-1.64,-1.641,-1.643,-1.646,-1.65,-1.654,-1.66,-1.666,-1.672,-1.678,-1.683,-1.687,-1.691,-1.694,-1.696,-1.697,-1.698,-1.699,-1.699,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.699,-1.699,-1.698,-1.696,-1.694,-1.691,-1.686,-1.68,-1.672,-1.661,-1.648,-1.634,-1.618,-1.601,-1.586,-1.573,-1.564,-1.558,-1.557,-1.559,-1.565,-1.573,-1.582,-1.592,-1.602,-1.612,-1.62,-1.627,-1.633,-1.637,-1.64,-1.643,-1.645,-1.646,-1.646,-1.647,-1.647,-1.612,-1.2,0.104,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.633,-1.735,-1.745,-1.746,-1.747,-1.747,-1.748,-1.749,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.749,-1.749,-1.748,-1.746,-1.743,-1.74,-1.735,-1.728,-1.719,-1.71,-1.699,-1.688,-1.678,-1.668,-1.659,-1.652,-1.646,-1.641,-1.638,-1.636,-1.636,-1.637,-1.639,-1.642,-1.646,-1.651,-1.657,-1.663,-1.67,-1.676,-1.681,-1.686,-1.69,-1.693,-1.696,-1.697,-1.698,-1.699,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.699,-1.699,-1.698,-1.696,-1.694,-1.691,-1.686,-1.679,-1.67,-1.659,-1.645,-1.63,-1.612,-1.595,-1.579,-1.565,-1.555,-1.55,-1.549,-1.552,-1.559,-1.569,-1.579,-1.59,-1.601,-1.612,-1.621,-1.628,-1.634,-1.639,-1.642,-1.645,-1.646,-1.647,-1.648,-1.648,-1.648,-1.613,-1.201,0.104,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.633,-1.735,-1.745,-1.746,-1.747,-1.747,-1.748,-1.749,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.749,-1.749,-1.748,-1.746,-1.743,-1.739,-1.733,-1.725,-1.716,-1.705,-1.694,-1.682,-1.671,-1.661,-1.653,-1.646,-1.64,-1.636,-1.633,-1.632,-1.632,-1.633,-1.635,-1.638,-1.642,-1.647,-1.653,-1.66,-1.667,-1.674,-1.68,-1.685,-1.689,-1.693,-1.695,-1.697,-1.698,-1.699,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.699,-1.699,-1.698,-1.696,-1.694,-1.69,-1.685,-1.678,-1.668,-1.656,-1.641,-1.624,-1.606,-1.587,-1.57,-1.555,-1.545,-1.54,-1.54,-1.544,-1.552,-1.563,-1.575,-1.587,-1.6,-1.611,-1.621,-1.629,-1.635,-1.64,-1.644,-1.646,-1.647,-1.648,-1.649,-1.649,-1.649,-1.614,-1.202,0.104,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.632,-1.735,-1.744,-1.746,-1.746,-1.747,-1.748,-1.748,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.749,-1.748,-1.747,-1.745,-1.742,-1.737,-1.73,-1.721,-1.711,-1.7,-1.687,-1.675,-1.664,-1.654,-1.646,-1.64,-1.635,-1.63,-1.628,-1.627,-1.628,-1.629,-1.631,-1.634,-1.638,-1.643,-1.649,-1.656,-1.663,-1.671,-1.677,-1.683,-1.688,-1.692,-1.695,-1.697,-1.698,-1.699,-1.699,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.699,-1.699,-1.698,-1.696,-1.693,-1.689,-1.684,-1.676,-1.665,-1.652,-1.636,-1.617,-1.597,-1.577,-1.559,-1.544,-1.533,-1.528,-1.528,-1.534,-1.543,-1.555,-1.568,-1.582,-1.596,-1.609,-1.62,-1.629,-1.636,-1.641,-1.645,-1.647,-1.648,-1.649,-1.649,-1.649,-1.649,-1.615,-1.202,0.103,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.632,-1.734,-1.744,-1.745,-1.746,-1.747,-1.748,-1.748,-1.749,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.749,-1.748,-1.746,-1.744,-1.74,-1.734,-1.726,-1.717,-1.705,-1.692,-1.68,-1.667,-1.656,-1.647,-1.639,-1.633,-1.627,-1.616,-1.612,-1.614,-1.622,-1.625,-1.627,-1.63,-1.634,-1.639,-1.645,-1.652,-1.659,-1.667,-1.674,-1.681,-1.686,-1.691,-1.694,-1.696,-1.698,-1.699,-1.699,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.699,-1.698,-1.697,-1.695,-1.692,-1.688,-1.681,-1.673,-1.661,-1.647,-1.629,-1.609,-1.587,-1.565,-1.546,-1.53,-1.519,-1.514,-1.515,-1.521,-1.531,-1.544,-1.56,-1.575,-1.591,-1.605,-1.618,-1.628,-1.636,-1.641,-1.645,-1.647,-1.649,-1.649,-1.649,-1.649,-1.649,-1.615,-1.202,0.103,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.631,-1.734,-1.744,-1.745,-1.746,-1.747,-1.747,-1.748,-1.749,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.749,-1.749,-1.747,-1.745,-1.742,-1.737,-1.73,-1.721,-1.71,-1.697,-1.684,-1.671,-1.658,-1.648,-1.639,-1.632,-1.627,-1.614,-1.584,-1.575,-1.583,-1.611,-1.621,-1.624,-1.626,-1.63,-1.635,-1.64,-1.647,-1.655,-1.663,-1.67,-1.678,-1.684,-1.689,-1.692,-1.695,-1.697,-1.698,-1.699,-1.699,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.7,-1.699,-1.699,-1.698,-1.696,-1.694,-1.691,-1.686,-1.679,-1.669,-1.656,-1.64,-1.62,-1.598,-1.575,-1.551,-1.53,-1.514,-1.502,-1.497,-1.498,-1.505,-1.516,-1.531,-1.548,-1.566,-1.583,-1.599,-1.614,-1.625,-1.634,-1.641,-1.645,-1.647,-1.649,-1.649,-1.649,-1.649,-1.649,-1.615,-1.202,0.103,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.631,-1.733,-1.743,-1.744,-1.745,-1.746,-1.747,-1.748,-1.748,-1.749,-1.749,-1.749,-1.749,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.749,-1.749,-1.748,-1.746,-1.743,-1.739,-1.733,-1.725,-1.715,-1.702,-1.688,-1.674,-1.661,-1.649,-1.639,-1.632,-1.626,-1.621,-1.608,-1.573,-1.563,-1.573,-1.607,-1.619,-1.621,-1.623,-1.626,-1.63,-1.636,-1.642,-1.649,-1.658,-1.666,-1.674,-1.68,-1.686,-1.69,-1.694,-1.696,-1.697,-1.698,-1.699,-1.699,-1.7,-1.7,-1.7,-1.7,-1.7,-1.699,-1.699,-1.698,-1.697,-1.695,-1.693,-1.689,-1.683,-1.675,-1.664,-1.649,-1.631,-1.61,-1.585,-1.56,-1.535,-1.512,-1.495,-1.482,-1.477,-1.478,-1.485,-1.497,-1.513,-1.532,-1.552,-1.572,-1.591,-1.607,-1.621,-1.631,-1.639,-1.644,-1.647,-1.648,-1.649,-1.649,-1.649,-1.649,-1.615,-1.202,0.103,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.31,-1.63,-1.732,-1.742,-1.743,-1.744,-1.745,-1.746,-1.747,-1.748,-1.748,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.748,-1.746,-1.744,-1.741,-1.735,-1.728,-1.718,-1.706,-1.692,-1.677,-1.663,-1.65,-1.639,-1.631,-1.624,-1.62,-1.617,-1.605,-1.571,-1.562,-1.572,-1.606,-1.617,-1.619,-1.621,-1.623,-1.627,-1.631,-1.637,-1.644,-1.652,-1.66,-1.669,-1.676,-1.683,-1.688,-1.692,-1.694,-1.696,-1.698,-1.698,-1.699,-1.699,-1.699,-1.699,-1.699,-1.699,-1.699,-1.698,-1.697,-1.696,-1.694,-1.69,-1.686,-1.679,-1.669,-1.657,-1.641,-1.62,-1.597,-1.57,-1.543,-1.516,-1.492,-1.472,-1.459,-1.453,-1.454,-1.461,-1.474,-1.491,-1.511,-1.533,-1.556,-1.577,-1.597,-1.613,-1.626,-1.636,-1.642,-1.646,-1.648,-1.649,-1.649,-1.649,-1.649,-1.615,-1.202,0.103,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.309,-1.629,-1.731,-1.741,-1.742,-1.743,-1.744,-1.745,-1.746,-1.747,-1.747,-1.748,-1.748,-1.749,-1.749,-1.749,-1.749,-1.749,-1.749,-1.748,-1.747,-1.746,-1.744,-1.741,-1.736,-1.73,-1.721,-1.709,-1.695,-1.68,-1.665,-1.651,-1.639,-1.629,-1.623,-1.618,-1.615,-1.613,-1.603,-1.571,-1.562,-1.571,-1.605,-1.616,-1.618,-1.62,-1.621,-1.624,-1.628,-1.632,-1.639,-1.646,-1.654,-1.663,-1.671,-1.678,-1.684,-1.689,-1.692,-1.695,-1.696,-1.697,-1.698,-1.698,-1.699,-1.699,-1.699,-1.698,-1.698,-1.697,-1.696,-1.694,-1.691,-1.687,-1.682,-1.674,-1.663,-1.648,-1.63,-1.607,-1.581,-1.552,-1.522,-1.493,-1.468,-1.447,-1.433,-1.426,-1.426,-1.433,-1.445,-1.463,-1.484,-1.508,-1.533,-1.558,-1.581,-1.601,-1.617,-1.629,-1.638,-1.643,-1.646,-1.648,-1.648,-1.649,-1.649,-1.614,-1.201,0.104,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.309,-1.628,-1.73,-1.74,-1.741,-1.742,-1.743,-1.744,-1.745,-1.746,-1.746,-1.747,-1.747,-1.748,-1.748,-1.748,-1.748,-1.748,-1.747,-1.747,-1.745,-1.744,-1.741,-1.737,-1.73,-1.722,-1.711,-1.698,-1.683,-1.667,-1.652,-1.639,-1.628,-1.621,-1.616,-1.613,-1.612,-1.611,-1.602,-1.571,-1.562,-1.572,-1.606,-1.617,-1.618,-1.619,-1.62,-1.622,-1.624,-1.628,-1.633,-1.64,-1.648,-1.656,-1.665,-1.673,-1.679,-1.685,-1.689,-1.692,-1.694,-1.696,-1.697,-1.697,-1.698,-1.698,-1.698,-1.697,-1.697,-1.695,-1.694,-1.692,-1.688,-1.683,-1.676,-1.667,-1.654,-1.638,-1.617,-1.592,-1.563,-1.531,-1.499,-1.468,-1.44,-1.418,-1.403,-1.394,-1.393,-1.398,-1.41,-1.428,-1.45,-1.475,-1.502,-1.53,-1.557,-1.581,-1.602,-1.618,-1.63,-1.638,-1.643,-1.645,-1.647,-1.647,-1.647,-1.612,-1.2,0.104,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.309,-1.627,-1.728,-1.738,-1.739,-1.741,-1.742,-1.743,-1.744,-1.744,-1.745,-1.746,-1.746,-1.746,-1.747,-1.747,-1.746,-1.746,-1.745,-1.744,-1.742,-1.74,-1.736,-1.73,-1.722,-1.712,-1.699,-1.684,-1.668,-1.653,-1.638,-1.627,-1.618,-1.613,-1.61,-1.609,-1.609,-1.61,-1.601,-1.571,-1.562,-1.572,-1.607,-1.618,-1.62,-1.62,-1.62,-1.621,-1.622,-1.625,-1.629,-1.634,-1.641,-1.649,-1.658,-1.666,-1.674,-1.68,-1.685,-1.689,-1.691,-1.693,-1.695,-1.695,-1.696,-1.696,-1.696,-1.695,-1.694,-1.693,-1.691,-1.688,-1.684,-1.678,-1.669,-1.658,-1.643,-1.624,-1.601,-1.573,-1.541,-1.507,-1.473,-1.439,-1.41,-1.386,-1.369,-1.358,-1.355,-1.359,-1.369,-1.385,-1.406,-1.432,-1.46,-1.491,-1.522,-1.551,-1.577,-1.598,-1.615,-1.627,-1.635,-1.639,-1.642,-1.643,-1.643,-1.608,-1.196,0.105,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.308,-1.625,-1.726,-1.736,-1.737,-1.739,-1.74,-1.741,-1.742,-1.743,-1.743,-1.744,-1.744,-1.745,-1.745,-1.745,-1.744,-1.743,-1.742,-1.741,-1.738,-1.734,-1.729,-1.722,-1.712,-1.7,-1.685,-1.669,-1.653,-1.638,-1.625,-1.616,-1.609,-1.606,-1.605,-1.605,-1.607,-1.609,-1.602,-1.571,-1.562,-1.573,-1.609,-1.621,-1.622,-1.621,-1.621,-1.621,-1.621,-1.622,-1.625,-1.629,-1.635,-1.642,-1.65,-1.659,-1.667,-1.674,-1.679,-1.684,-1.687,-1.69,-1.692,-1.693,-1.693,-1.693,-1.693,-1.692,-1.691,-1.689,-1.687,-1.683,-1.678,-1.67,-1.66,-1.647,-1.63,-1.609,-1.582,-1.551,-1.517,-1.48,-1.443,-1.408,-1.376,-1.35,-1.331,-1.318,-1.313,-1.313,-1.321,-1.334,-1.353,-1.377,-1.406,-1.438,-1.471,-1.505,-1.536,-1.564,-1.587,-1.605,-1.618,-1.626,-1.631,-1.633,-1.634,-1.598,-1.188,0.107,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.308,-1.623,-1.724,-1.734,-1.735,-1.736,-1.738,-1.739,-1.74,-1.74,-1.741,-1.741,-1.742,-1.742,-1.742,-1.742,-1.741,-1.74,-1.738,-1.735,-1.732,-1.727,-1.718,-1.709,-1.697,-1.684,-1.668,-1.652,-1.637,-1.623,-1.613,-1.605,-1.601,-1.6,-1.6,-1.603,-1.606,-1.609,-1.602,-1.571,-1.562,-1.573,-1.613,-1.625,-1.625,-1.624,-1.623,-1.621,-1.62,-1.62,-1.622,-1.624,-1.629,-1.635,-1.643,-1.651,-1.659,-1.666,-1.673,-1.678,-1.682,-1.685,-1.687,-1.689,-1.69,-1.69,-1.689,-1.689,-1.687,-1.685,-1.681,-1.677,-1.67,-1.661,-1.649,-1.634,-1.614,-1.59,-1.561,-1.527,-1.489,-1.45,-1.411,-1.373,-1.34,-1.312,-1.29,-1.275,-1.266,-1.264,-1.267,-1.276,-1.292,-1.312,-1.338,-1.369,-1.403,-1.439,-1.476,-1.51,-1.54,-1.565,-1.585,-1.598,-1.607,-1.613,-1.614,-1.58,-1.173,0.111,0.516,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.208,-1.307,-1.621,-1.722,-1.731,-1.733,-1.734,-1.735,-1.736,-1.737,-1.737,-1.738,-1.738,-1.739,-1.739,-1.738,-1.738,-1.736,-1.735,-1.732,-1.729,-1.724,-1.713,-1.686,-1.673,-1.662,-1.65,-1.638,-1.625,-1.613,-1.603,-1.596,-1.591,-1.589,-1.589,-1.59,-1.593,-1.597,-1.6,-1.595,-1.57,-1.562,-1.575,-1.617,-1.63,-1.63,-1.628,-1.625,-1.623,-1.621,-1.62,-1.619,-1.621,-1.624,-1.629,-1.635,-1.642,-1.65,-1.658,-1.665,-1.671,-1.676,-1.679,-1.682,-1.684,-1.685,-1.685,-1.685,-1.683,-1.681,-1.678,-1.674,-1.668,-1.66,-1.65,-1.636,-1.618,-1.595,-1.568,-1.536,-1.499,-1.459,-1.417,-1.376,-1.337,-1.302,-1.272,-1.247,-1.229,-1.217,-1.21,-1.209,-1.213,-1.223,-1.238,-1.259,-1.286,-1.317,-1.353,-1.391,-1.43,-1.467,-1.501,-1.529,-1.55,-1.566,-1.576,-1.581,-1.55,-1.15,0.117,0.517,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.222,-1.197,-1.197,-1.197,-1.197,-1.205,-1.303,-1.608,-1.707,-1.717,-1.718,-1.719,-1.72,-1.721,-1.722,-1.741,-1.741,-1.741,-1.741,-1.74,-1.733,-1.732,-1.73,-1.728,-1.724,-1.72,-1.711,-1.682,-1.615,-1.591,-1.586,-1.582,-1.579,-1.576,-1.573,-1.57,-1.569,-1.568,-1.567,-1.567,-1.568,-1.568,-1.569,-1.571,-1.57,-1.562,-1.561,-1.576,-1.623,-1.636,-1.635,-1.632,-1.629,-1.625,-1.622,-1.619,-1.618,-1.618,-1.619,-1.623,-1.628,-1.634,-1.642,-1.649,-1.656,-1.663,-1.668,-1.672,-1.675,-1.677,-1.678,-1.679,-1.678,-1.677,-1.674,-1.67,-1.665,-1.658,-1.648,-1.636,-1.62,-1.599,-1.574,-1.544,-1.508,-1.469,-1.427,-1.383,-1.34,-1.299,-1.262,-1.23,-1.203,-1.182,-1.166,-1.155,-1.149,-1.148,-1.151,-1.16,-1.173,-1.193,-1.218,-1.25,-1.286,-1.326,-1.368,-1.409,-1.446,-1.478,-1.503,-1.522,-1.535,-1.511,-1.121,0.124,0.517,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.408,-1.205,-1.155,-1.155,-1.155,-1.163,-1.245,-1.483,-1.565,-1.574,-1.575,-1.576,-1.577,-1.578,-1.615,-1.78,-1.814,-1.812,-1.81,-1.796,-1.74,-1.726,-1.723,-1.72,-1.715,-1.709,-1.699,-1.667,-1.589,-1.564,-1.562,-1.562,-1.561,-1.561,-1.561,-1.561,-1.561,-1.561,-1.561,-1.561,-1.561,-1.561,-1.561,-1.561,-1.561,-1.56,-1.561,-1.578,-1.629,-1.644,-1.642,-1.637,-1.632,-1.627,-1.623,-1.619,-1.616,-1.615,-1.616,-1.618,-1.622,-1.627,-1.633,-1.64,-1.647,-1.654,-1.659,-1.664,-1.667,-1.669,-1.671,-1.671,-1.67,-1.668,-1.665,-1.661,-1.654,-1.646,-1.634,-1.62,-1.601,-1.578,-1.55,-1.517,-1.479,-1.437,-1.392,-1.347,-1.302,-1.26,-1.222,-1.188,-1.159,-1.136,-1.116,-1.101,-1.09,-1.083,-1.08,-1.081,-1.087,-1.098,-1.116,-1.14,-1.171,-1.209,-1.252,-1.299,-1.346,-1.391,-1.43,-1.462,-1.486,-1.472,-1.095,0.13,0.518,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-2.571,-2.045,-1.299,-1.05,-1.025,-1.025,-1.025,-1.041,-1.115,-1.131,-1.131,-1.132,-1.132,-1.132,-1.151,-1.334,-1.861,-2.035,-2.046,-2.034,-1.968,-1.788,-1.724,-1.714,-1.71,-1.704,-1.697,-1.686,-1.656,-1.584,-1.562,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.562,-1.58,-1.637,-1.652,-1.648,-1.642,-1.635,-1.629,-1.623,-1.619,-1.615,-1.613,-1.612,-1.613,-1.616,-1.62,-1.625,-1.632,-1.638,-1.644,-1.65,-1.654,-1.658,-1.66,-1.662,-1.662,-1.661,-1.658,-1.655,-1.649,-1.641,-1.631,-1.618,-1.601,-1.58,-1.554,-1.524,-1.488,-1.447,-1.403,-1.357,-1.31,-1.265,-1.222,-1.182,-1.147,-1.117,-1.09,-1.068,-1.05,-1.034,-1.022,-1.013,-1.008,-1.007,-1.01,-1.019,-1.035,-1.059,-1.092,-1.135,-1.186,-1.244,-1.304,-1.361,-1.41,-1.449,-1.446,-1.08,0.134,0.518,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-2.576,-2.234,-1.309,-1.008,-0.983,-0.983,-0.983,-0.983,-0.992,-0.992,-0.992,-0.992,-0.992,-0.992,-1.01,-1.226,-1.89,-2.098,-2.109,-2.098,-2.021,-1.792,-1.715,-1.704,-1.699,-1.692,-1.683,-1.672,-1.643,-1.581,-1.562,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.562,-1.582,-1.644,-1.66,-1.655,-1.646,-1.638,-1.63,-1.623,-1.618,-1.614,-1.611,-1.609,-1.609,-1.611,-1.614,-1.618,-1.623,-1.629,-1.635,-1.64,-1.645,-1.648,-1.651,-1.652,-1.652,-1.65,-1.647,-1.642,-1.636,-1.627,-1.615,-1.6,-1.581,-1.557,-1.529,-1.496,-1.457,-1.415,-1.369,-1.322,-1.274,-1.228,-1.184,-1.144,-1.108,-1.076,-1.048,-1.024,-1.002,-0.983,-0.967,-0.954,-0.944,-0.937,-0.934,-0.936,-0.944,-0.962,-0.989,-1.03,-1.085,-1.153,-1.231,-1.309,-1.378,-1.433,-1.44,-1.078,0.134,0.518,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-2.579,-2.258,-1.306,-1.005,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.998,-1.21,-1.882,-2.089,-2.103,-2.094,-2.019,-1.782,-1.705,-1.693,-1.687,-1.679,-1.669,-1.657,-1.632,-1.578,-1.561,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.562,-1.583,-1.648,-1.665,-1.659,-1.649,-1.639,-1.63,-1.622,-1.616,-1.611,-1.608,-1.606,-1.605,-1.606,-1.608,-1.612,-1.616,-1.621,-1.626,-1.63,-1.635,-1.638,-1.64,-1.641,-1.64,-1.638,-1.635,-1.629,-1.621,-1.611,-1.597,-1.58,-1.559,-1.533,-1.502,-1.466,-1.426,-1.382,-1.335,-1.287,-1.239,-1.192,-1.149,-1.108,-1.072,-1.039,-1.01,-0.984,-0.96,-0.939,-0.921,-0.904,-0.89,-0.88,-0.872,-0.869,-0.873,-0.884,-0.906,-0.943,-0.999,-1.077,-1.173,-1.275,-1.366,-1.435,-1.449,-1.087,0.132,0.518,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-2.58,-2.259,-1.306,-1.005,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.207,-1.871,-2.078,-2.094,-2.087,-2.011,-1.773,-1.694,-1.682,-1.674,-1.666,-1.656,-1.643,-1.62,-1.575,-1.561,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.562,-1.582,-1.645,-1.665,-1.659,-1.649,-1.638,-1.628,-1.62,-1.614,-1.609,-1.605,-1.603,-1.602,-1.602,-1.603,-1.606,-1.609,-1.613,-1.617,-1.621,-1.625,-1.628,-1.629,-1.63,-1.628,-1.626,-1.621,-1.614,-1.605,-1.593,-1.578,-1.559,-1.535,-1.507,-1.474,-1.437,-1.395,-1.35,-1.302,-1.253,-1.205,-1.159,-1.116,-1.076,-1.039,-1.006,-0.976,-0.949,-0.924,-0.902,-0.882,-0.864,-0.848,-0.835,-0.826,-0.82,-0.819,-0.825,-0.841,-0.872,-0.927,-1.011,-1.125,-1.253,-1.367,-1.449,-1.465,-1.1,0.128,0.518,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-2.58,-2.259,-1.306,-1.005,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.205,-1.864,-2.071,-2.088,-2.082,-2.006,-1.764,-1.683,-1.67,-1.662,-1.653,-1.642,-1.63,-1.61,-1.572,-1.561,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.561,-1.579,-1.633,-1.655,-1.653,-1.643,-1.633,-1.623,-1.616,-1.609,-1.605,-1.601,-1.599,-1.598,-1.598,-1.598,-1.6,-1.603,-1.606,-1.609,-1.613,-1.615,-1.617,-1.618,-1.618,-1.616,-1.613,-1.607,-1.599,-1.589,-1.575,-1.558,-1.537,-1.512,-1.482,-1.447,-1.408,-1.365,-1.318,-1.27,-1.222,-1.174,-1.129,-1.086,-1.046,-1.01,-0.977,-0.947,-0.919,-0.894,-0.872,-0.851,-0.832,-0.816,-0.802,-0.792,-0.784,-0.781,-0.783,-0.794,-0.818,-0.866,-0.951,-1.081,-1.236,-1.374,-1.466,-1.483,-1.112,0.125,0.517,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-2.577,-2.257,-1.306,-1.005,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.205,-1.861,-2.067,-2.085,-2.078,-2.001,-1.755,-1.672,-1.658,-1.649,-1.64,-1.63,-1.618,-1.6,-1.57,-1.561,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.561,-1.573,-1.613,-1.635,-1.638,-1.632,-1.624,-1.616,-1.609,-1.604,-1.6,-1.597,-1.595,-1.594,-1.594,-1.594,-1.595,-1.597,-1.6,-1.602,-1.605,-1.607,-1.608,-1.608,-1.607,-1.604,-1.6,-1.593,-1.584,-1.572,-1.557,-1.538,-1.515,-1.488,-1.456,-1.42,-1.379,-1.335,-1.289,-1.241,-1.193,-1.146,-1.102,-1.059,-1.021,-0.985,-0.952,-0.922,-0.895,-0.87,-0.848,-0.827,-0.809,-0.792,-0.779,-0.768,-0.76,-0.756,-0.756,-0.762,-0.779,-0.818,-0.897,-1.036,-1.217,-1.381,-1.482,-1.497,-1.121,0.123,0.517,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-2.572,-2.254,-1.305,-1.005,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.204,-1.859,-2.066,-2.083,-2.077,-1.998,-1.746,-1.662,-1.645,-1.627,-1.618,-1.609,-1.601,-1.587,-1.567,-1.561,-1.56,-1.56,-1.56,-1.56,-1.56,-1.559,-1.559,-1.559,-1.559,-1.559,-1.559,-1.559,-1.558,-1.558,-1.559,-1.559,-1.56,-1.566,-1.59,-1.61,-1.617,-1.616,-1.611,-1.605,-1.601,-1.597,-1.594,-1.592,-1.591,-1.59,-1.589,-1.59,-1.591,-1.592,-1.594,-1.596,-1.597,-1.599,-1.599,-1.598,-1.596,-1.592,-1.587,-1.579,-1.568,-1.555,-1.538,-1.518,-1.493,-1.464,-1.431,-1.394,-1.352,-1.308,-1.262,-1.214,-1.167,-1.121,-1.078,-1.037,-0.999,-0.964,-0.932,-0.903,-0.876,-0.851,-0.829,-0.809,-0.791,-0.776,-0.763,-0.753,-0.745,-0.741,-0.74,-0.743,-0.754,-0.783,-0.852,-0.99,-1.193,-1.381,-1.491,-1.505,-1.125,0.122,0.517,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-2.564,-2.247,-1.304,-1.005,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.204,-1.859,-2.065,-2.082,-2.075,-1.995,-1.738,-1.65,-1.626,-1.59,-1.576,-1.573,-1.57,-1.567,-1.561,-1.56,-1.56,-1.558,-1.556,-1.555,-1.554,-1.553,-1.552,-1.55,-1.549,-1.547,-1.545,-1.542,-1.54,-1.54,-1.542,-1.546,-1.55,-1.554,-1.566,-1.581,-1.592,-1.595,-1.595,-1.594,-1.592,-1.59,-1.588,-1.587,-1.586,-1.585,-1.585,-1.586,-1.586,-1.588,-1.589,-1.59,-1.591,-1.591,-1.591,-1.589,-1.586,-1.581,-1.574,-1.565,-1.553,-1.539,-1.52,-1.498,-1.472,-1.442,-1.408,-1.369,-1.327,-1.283,-1.237,-1.19,-1.144,-1.099,-1.057,-1.017,-0.98,-0.946,-0.915,-0.887,-0.861,-0.837,-0.816,-0.796,-0.779,-0.765,-0.752,-0.743,-0.736,-0.733,-0.732,-0.734,-0.741,-0.761,-0.817,-0.948,-1.162,-1.372,-1.493,-1.507,-1.126,0.122,0.517,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-2.551,-2.238,-1.301,-1.005,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.204,-1.859,-2.065,-2.082,-2.075,-1.992,-1.709,-1.62,-1.597,-1.563,-1.55,-1.547,-1.545,-1.544,-1.541,-1.54,-1.537,-1.531,-1.526,-1.521,-1.516,-1.511,-1.506,-1.501,-1.495,-1.488,-1.48,-1.471,-1.464,-1.462,-1.469,-1.482,-1.494,-1.504,-1.517,-1.533,-1.547,-1.555,-1.559,-1.561,-1.561,-1.561,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.56,-1.561,-1.562,-1.562,-1.562,-1.561,-1.56,-1.557,-1.553,-1.548,-1.54,-1.53,-1.517,-1.501,-1.481,-1.458,-1.431,-1.4,-1.366,-1.327,-1.286,-1.243,-1.198,-1.154,-1.11,-1.067,-1.027,-0.989,-0.955,-0.923,-0.893,-0.867,-0.843,-0.82,-0.801,-0.783,-0.767,-0.754,-0.744,-0.736,-0.73,-0.728,-0.727,-0.73,-0.735,-0.75,-0.794,-0.911,-1.122,-1.343,-1.475,-1.492,-1.113,0.104,0.498,0.531,0.531,0.531,0.531,0.531,0.531,0.532,0.533,0.534,0.536,0.538,0.54,0.541,0.542,0.542,0.542,0.543,0.543,0.543,0.543,0.543,0.543,0.55,-2.532,-2.223,-1.297,-1.004,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.204,-1.858,-2.065,-2.081,-2.074,-1.947,-1.492,-1.362,-1.347,-1.331,-1.324,-1.319,-1.314,-1.308,-1.303,-1.298,-1.293,-1.287,-1.28,-1.273,-1.266,-1.259,-1.253,-1.247,-1.242,-1.237,-1.23,-1.223,-1.217,-1.214,-1.217,-1.227,-1.237,-1.248,-1.262,-1.277,-1.289,-1.297,-1.301,-1.303,-1.303,-1.302,-1.301,-1.299,-1.298,-1.296,-1.295,-1.293,-1.292,-1.29,-1.289,-1.287,-1.285,-1.282,-1.279,-1.275,-1.269,-1.263,-1.254,-1.244,-1.232,-1.218,-1.201,-1.182,-1.16,-1.135,-1.107,-1.077,-1.046,-1.013,-0.98,-0.948,-0.916,-0.886,-0.859,-0.833,-0.811,-0.791,-0.774,-0.758,-0.746,-0.735,-0.726,-0.719,-0.714,-0.71,-0.709,-0.709,-0.711,-0.714,-0.718,-0.722,-0.728,-0.74,-0.772,-0.859,-1.024,-1.207,-1.321,-1.335,-1.017,-0.056,0.273,0.303,0.302,0.301,0.301,0.303,0.307,0.314,0.325,0.343,0.366,0.392,0.414,0.431,0.442,0.448,0.451,0.453,0.454,0.455,0.455,0.454,0.469,0.55,-2.505,-2.202,-1.292,-1.004,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.204,-1.858,-2.064,-2.081,-2.051,-1.755,-0.877,-0.581,-0.549,-0.535,-0.531,-0.53,-0.528,-0.526,-0.524,-0.522,-0.519,-0.515,-0.509,-0.502,-0.493,-0.485,-0.479,-0.477,-0.478,-0.482,-0.486,-0.491,-0.494,-0.496,-0.498,-0.501,-0.504,-0.509,-0.521,-0.527,-0.527,-0.524,-0.519,-0.514,-0.507,-0.5,-0.493,-0.486,-0.479,-0.471,-0.464,-0.457,-0.449,-0.442,-0.435,-0.427,-0.42,-0.412,-0.405,-0.398,-0.39,-0.383,-0.377,-0.37,-0.364,-0.358,-0.353,-0.348,-0.343,-0.339,-0.336,-0.334,-0.333,-0.333,-0.335,-0.338,-0.343,-0.349,-0.358,-0.369,-0.383,-0.398,-0.415,-0.434,-0.455,-0.478,-0.502,-0.527,-0.553,-0.579,-0.605,-0.63,-0.652,-0.672,-0.688,-0.701,-0.711,-0.718,-0.727,-0.749,-0.793,-0.844,-0.873,-0.882,-0.806,-0.478,-0.411,-0.42,-0.426,-0.429,-0.428,-0.421,-0.406,-0.379,-0.333,-0.262,-0.171,-0.071,0.018,0.084,0.126,0.151,0.165,0.172,0.176,0.177,0.177,0.184,0.263,0.55,-2.468,-2.173,-1.285,-1.003,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.204,-1.858,-2.064,-2.079,-2.048,-1.709,-0.664,-0.327,-0.286,-0.25,-0.237,-0.236,-0.234,-0.234,-0.234,-0.234,-0.233,-0.232,-0.231,-0.228,-0.226,-0.224,-0.222,-0.222,-0.223,-0.224,-0.226,-0.228,-0.23,-0.231,-0.231,-0.232,-0.233,-0.243,-0.27,-0.286,-0.284,-0.278,-0.27,-0.261,-0.252,-0.243,-0.234,-0.225,-0.217,-0.208,-0.199,-0.19,-0.18,-0.171,-0.162,-0.153,-0.144,-0.135,-0.126,-0.117,-0.109,-0.102,-0.096,-0.09,-0.086,-0.082,-0.08,-0.08,-0.081,-0.083,-0.088,-0.094,-0.102,-0.112,-0.124,-0.138,-0.154,-0.172,-0.192,-0.214,-0.238,-0.264,-0.291,-0.321,-0.352,-0.385,-0.419,-0.455,-0.491,-0.527,-0.563,-0.596,-0.627,-0.654,-0.677,-0.693,-0.705,-0.712,-0.716,-0.719,-0.723,-0.727,-0.729,-0.728,-0.713,-0.642,-0.643,-0.656,-0.665,-0.67,-0.669,-0.662,-0.647,-0.615,-0.559,-0.468,-0.344,-0.208,-0.089,-0.006,0.043,0.07,0.084,0.091,0.094,0.095,0.094,0.1,0.193,0.55,-2.42,-2.134,-1.275,-1.003,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.204,-1.857,-2.061,-2.074,-2.042,-1.702,-0.641,-0.306,-0.263,-0.216,-0.201,-0.198,-0.188,-0.186,-0.186,-0.186,-0.186,-0.186,-0.186,-0.185,-0.185,-0.185,-0.185,-0.185,-0.185,-0.185,-0.185,-0.186,-0.186,-0.186,-0.186,-0.186,-0.186,-0.193,-0.229,-0.254,-0.257,-0.251,-0.242,-0.233,-0.224,-0.215,-0.207,-0.198,-0.189,-0.181,-0.172,-0.163,-0.154,-0.144,-0.135,-0.125,-0.116,-0.107,-0.098,-0.089,-0.081,-0.073,-0.067,-0.061,-0.056,-0.053,-0.051,-0.05,-0.052,-0.055,-0.059,-0.066,-0.074,-0.085,-0.097,-0.112,-0.128,-0.146,-0.166,-0.188,-0.212,-0.238,-0.266,-0.295,-0.326,-0.359,-0.394,-0.43,-0.467,-0.505,-0.542,-0.579,-0.613,-0.643,-0.668,-0.688,-0.702,-0.71,-0.715,-0.716,-0.717,-0.717,-0.716,-0.714,-0.7,-0.663,-0.667,-0.683,-0.694,-0.699,-0.699,-0.694,-0.682,-0.654,-0.598,-0.501,-0.362,-0.208,-0.078,0.007,0.054,0.076,0.086,0.091,0.092,0.093,0.092,0.097,0.19,0.55,-2.358,-2.085,-1.263,-1.002,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.202,-1.851,-2.052,-2.063,-2.028,-1.688,-0.637,-0.305,-0.263,-0.215,-0.199,-0.187,-0.156,-0.145,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.144,-0.146,-0.165,-0.223,-0.248,-0.246,-0.239,-0.231,-0.223,-0.215,-0.207,-0.199,-0.191,-0.183,-0.174,-0.166,-0.157,-0.148,-0.139,-0.129,-0.12,-0.111,-0.101,-0.092,-0.083,-0.075,-0.067,-0.06,-0.054,-0.05,-0.046,-0.044,-0.043,-0.043,-0.046,-0.05,-0.056,-0.064,-0.074,-0.086,-0.1,-0.115,-0.133,-0.152,-0.173,-0.196,-0.221,-0.247,-0.275,-0.305,-0.337,-0.37,-0.405,-0.442,-0.479,-0.517,-0.555,-0.592,-0.625,-0.654,-0.678,-0.695,-0.706,-0.712,-0.714,-0.715,-0.715,-0.715,-0.712,-0.697,-0.66,-0.669,-0.688,-0.699,-0.705,-0.706,-0.703,-0.693,-0.669,-0.617,-0.517,-0.366,-0.196,-0.058,0.026,0.067,0.085,0.092,0.095,0.096,0.096,0.095,0.101,0.192,0.55,-2.281,-2.024,-1.247,-1.001,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.997,-1.199,-1.837,-2.034,-2.041,-2.002,-1.664,-0.63,-0.304,-0.262,-0.215,-0.199,-0.185,-0.145,-0.132,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.131,-0.133,-0.152,-0.208,-0.227,-0.226,-0.221,-0.215,-0.209,-0.203,-0.196,-0.189,-0.182,-0.175,-0.167,-0.159,-0.151,-0.142,-0.133,-0.124,-0.114,-0.105,-0.095,-0.086,-0.077,-0.069,-0.061,-0.054,-0.047,-0.042,-0.038,-0.036,-0.034,-0.035,-0.037,-0.041,-0.046,-0.054,-0.063,-0.074,-0.087,-0.102,-0.119,-0.137,-0.157,-0.179,-0.202,-0.227,-0.253,-0.282,-0.311,-0.343,-0.376,-0.411,-0.447,-0.484,-0.523,-0.56,-0.597,-0.63,-0.658,-0.68,-0.695,-0.705,-0.709,-0.711,-0.712,-0.71,-0.706,-0.688,-0.649,-0.663,-0.686,-0.7,-0.706,-0.708,-0.707,-0.7,-0.679,-0.629,-0.528,-0.368,-0.185,-0.042,0.04,0.076,0.09,0.095,0.097,0.097,0.097,0.097,0.102,0.194,0.55,-2.19,-1.951,-1.229,-0.999,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.996,-1.193,-1.811,-1.999,-2.002,-1.959,-1.626,-0.619,-0.301,-0.261,-0.215,-0.199,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.131,-0.142,-0.178,-0.193,-0.197,-0.197,-0.196,-0.193,-0.189,-0.184,-0.179,-0.173,-0.167,-0.159,-0.152,-0.144,-0.135,-0.126,-0.117,-0.108,-0.098,-0.089,-0.08,-0.071,-0.062,-0.054,-0.047,-0.04,-0.035,-0.03,-0.027,-0.026,-0.026,-0.028,-0.031,-0.036,-0.043,-0.052,-0.062,-0.075,-0.088,-0.104,-0.121,-0.14,-0.16,-0.182,-0.205,-0.23,-0.256,-0.283,-0.312,-0.342,-0.374,-0.408,-0.443,-0.479,-0.517,-0.554,-0.59,-0.623,-0.65,-0.672,-0.686,-0.695,-0.699,-0.7,-0.698,-0.69,-0.667,-0.624,-0.644,-0.676,-0.694,-0.702,-0.706,-0.707,-0.702,-0.683,-0.636,-0.534,-0.367,-0.178,-0.031,0.048,0.081,0.092,0.096,0.097,0.098,0.098,0.098,0.104,0.195,0.55,-2.084,-1.867,-1.207,-0.998,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.996,-1.181,-1.765,-1.94,-1.939,-1.892,-1.568,-0.602,-0.298,-0.259,-0.214,-0.199,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.133,-0.144,-0.156,-0.166,-0.172,-0.175,-0.175,-0.174,-0.172,-0.168,-0.163,-0.158,-0.151,-0.144,-0.136,-0.128,-0.119,-0.11,-0.101,-0.092,-0.082,-0.073,-0.064,-0.055,-0.046,-0.039,-0.032,-0.026,-0.022,-0.019,-0.017,-0.017,-0.018,-0.021,-0.025,-0.032,-0.04,-0.05,-0.061,-0.074,-0.089,-0.105,-0.122,-0.141,-0.161,-0.183,-0.205,-0.229,-0.253,-0.279,-0.306,-0.334,-0.363,-0.394,-0.426,-0.459,-0.494,-0.529,-0.564,-0.595,-0.622,-0.643,-0.657,-0.665,-0.667,-0.663,-0.653,-0.625,-0.579,-0.605,-0.65,-0.679,-0.694,-0.701,-0.704,-0.7,-0.682,-0.635,-0.532,-0.365,-0.174,-0.027,0.051,0.082,0.093,0.096,0.097,0.098,0.098,0.098,0.104,0.195,0.55,-1.967,-1.774,-1.183,-0.996,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.994,-1.164,-1.695,-1.851,-1.845,-1.797,-1.488,-0.578,-0.292,-0.255,-0.213,-0.199,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.126,-0.119,-0.127,-0.139,-0.149,-0.156,-0.159,-0.16,-0.16,-0.157,-0.153,-0.149,-0.143,-0.136,-0.129,-0.121,-0.112,-0.103,-0.094,-0.084,-0.075,-0.065,-0.056,-0.047,-0.039,-0.031,-0.024,-0.018,-0.013,-0.01,-0.007,-0.007,-0.008,-0.01,-0.014,-0.02,-0.028,-0.037,-0.047,-0.06,-0.073,-0.088,-0.104,-0.122,-0.14,-0.16,-0.18,-0.201,-0.222,-0.245,-0.267,-0.29,-0.314,-0.339,-0.364,-0.39,-0.418,-0.447,-0.472,-0.501,-0.528,-0.551,-0.569,-0.581,-0.585,-0.586,-0.575,-0.55,-0.514,-0.545,-0.602,-0.648,-0.676,-0.691,-0.697,-0.693,-0.675,-0.626,-0.524,-0.36,-0.174,-0.03,0.048,0.081,0.092,0.096,0.097,0.098,0.098,0.098,0.104,0.195,0.55,-1.845,-1.676,-1.158,-0.994,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.992,-1.139,-1.599,-1.731,-1.721,-1.672,-1.385,-0.547,-0.283,-0.243,-0.21,-0.199,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.129,-0.129,-0.122,-0.106,-0.109,-0.121,-0.132,-0.14,-0.145,-0.148,-0.148,-0.147,-0.144,-0.14,-0.134,-0.128,-0.121,-0.113,-0.104,-0.096,-0.086,-0.077,-0.067,-0.058,-0.048,-0.039,-0.03,-0.022,-0.015,-0.009,-0.004,0,0.002,0.003,0.003,0.001,-0.003,-0.008,-0.015,-0.023,-0.033,-0.045,-0.057,-0.071,-0.086,-0.102,-0.119,-0.136,-0.154,-0.173,-0.191,-0.21,-0.229,-0.247,-0.265,-0.282,-0.299,-0.316,-0.332,-0.342,-0.329,-0.339,-0.357,-0.373,-0.387,-0.397,-0.411,-0.444,-0.449,-0.443,-0.445,-0.478,-0.535,-0.595,-0.642,-0.67,-0.682,-0.679,-0.659,-0.609,-0.509,-0.353,-0.177,-0.039,0.041,0.077,0.09,0.095,0.097,0.098,0.098,0.097,0.103,0.195,0.55,-1.723,-1.578,-1.133,-0.992,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.99,-1.109,-1.482,-1.584,-1.57,-1.523,-1.263,-0.509,-0.265,-0.22,-0.202,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.121,-0.119,-0.113,-0.098,-0.1,-0.11,-0.12,-0.128,-0.134,-0.137,-0.138,-0.137,-0.135,-0.131,-0.126,-0.12,-0.113,-0.105,-0.097,-0.088,-0.078,-0.069,-0.059,-0.049,-0.04,-0.031,-0.022,-0.014,-0.006,0,0.005,0.01,0.012,0.014,0.014,0.012,0.009,0.004,-0.002,-0.01,-0.019,-0.03,-0.041,-0.054,-0.068,-0.083,-0.098,-0.114,-0.13,-0.146,-0.161,-0.177,-0.191,-0.205,-0.217,-0.227,-0.236,-0.243,-0.244,-0.216,-0.132,-0.104,-0.104,-0.108,-0.112,-0.118,-0.153,-0.236,-0.278,-0.321,-0.388,-0.428,-0.47,-0.526,-0.584,-0.628,-0.65,-0.652,-0.632,-0.582,-0.487,-0.344,-0.184,-0.052,0.028,0.068,0.086,0.093,0.095,0.096,0.097,0.096,0.103,0.194,0.55,-1.61,-1.487,-1.109,-0.99,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.987,-1.076,-1.352,-1.423,-1.406,-1.363,-1.133,-0.469,-0.253,-0.209,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.129,-0.121,-0.097,-0.089,-0.09,-0.089,-0.096,-0.105,-0.113,-0.12,-0.125,-0.128,-0.129,-0.128,-0.126,-0.122,-0.117,-0.111,-0.104,-0.097,-0.088,-0.079,-0.07,-0.06,-0.051,-0.041,-0.031,-0.022,-0.013,-0.004,0.003,0.01,0.015,0.02,0.023,0.025,0.025,0.024,0.021,0.017,0.011,0.004,-0.005,-0.014,-0.025,-0.037,-0.05,-0.063,-0.077,-0.092,-0.106,-0.12,-0.133,-0.146,-0.157,-0.167,-0.174,-0.179,-0.181,-0.18,-0.174,-0.143,-0.059,-0.033,-0.031,-0.031,-0.031,-0.032,-0.041,-0.078,-0.112,-0.207,-0.348,-0.399,-0.425,-0.464,-0.518,-0.568,-0.601,-0.61,-0.594,-0.547,-0.46,-0.333,-0.191,-0.069,0.012,0.056,0.078,0.088,0.092,0.094,0.095,0.095,0.102,0.194,0.55,-1.513,-1.409,-1.089,-0.988,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.985,-1.042,-1.223,-1.263,-1.244,-1.205,-1.005,-0.428,-0.242,-0.206,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.129,-0.119,-0.09,-0.082,-0.084,-0.089,-0.096,-0.103,-0.11,-0.115,-0.119,-0.121,-0.122,-0.121,-0.118,-0.114,-0.109,-0.103,-0.096,-0.089,-0.08,-0.071,-0.062,-0.052,-0.042,-0.032,-0.022,-0.012,-0.003,0.005,0.013,0.02,0.026,0.03,0.034,0.036,0.036,0.035,0.033,0.029,0.024,0.017,0.01,0.001,-0.01,-0.021,-0.032,-0.045,-0.058,-0.07,-0.083,-0.096,-0.107,-0.118,-0.126,-0.133,-0.137,-0.138,-0.136,-0.131,-0.121,-0.097,-0.042,-0.026,-0.025,-0.025,-0.025,-0.025,-0.026,-0.034,-0.054,-0.149,-0.326,-0.385,-0.401,-0.425,-0.464,-0.51,-0.546,-0.561,-0.55,-0.508,-0.431,-0.32,-0.196,-0.086,-0.007,0.041,0.068,0.081,0.088,0.091,0.093,0.095,0.103,0.196,0.55,-1.436,-1.346,-1.073,-0.987,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.982,-1.013,-1.107,-1.119,-1.098,-1.063,-0.89,-0.391,-0.231,-0.204,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.128,-0.119,-0.093,-0.087,-0.089,-0.093,-0.098,-0.103,-0.108,-0.112,-0.114,-0.116,-0.115,-0.114,-0.111,-0.107,-0.101,-0.095,-0.088,-0.08,-0.072,-0.062,-0.053,-0.043,-0.033,-0.022,-0.012,-0.003,0.007,0.015,0.023,0.03,0.036,0.041,0.045,0.047,0.048,0.047,0.045,0.042,0.037,0.031,0.024,0.016,0.006,-0.004,-0.015,-0.027,-0.039,-0.05,-0.062,-0.073,-0.083,-0.092,-0.1,-0.105,-0.107,-0.106,-0.102,-0.095,-0.085,-0.067,-0.035,-0.026,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.041,-0.122,-0.315,-0.378,-0.39,-0.405,-0.433,-0.468,-0.498,-0.513,-0.504,-0.467,-0.399,-0.303,-0.196,-0.098,-0.022,0.027,0.058,0.075,0.085,0.09,0.094,0.097,0.107,0.2,0.55,-1.381,-1.302,-1.061,-0.986,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.981,-0.988,-1.011,-0.999,-0.976,-0.943,-0.792,-0.359,-0.222,-0.202,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.127,-0.111,-0.107,-0.101,-0.086,-0.094,-0.097,-0.099,-0.102,-0.105,-0.108,-0.11,-0.111,-0.111,-0.11,-0.107,-0.104,-0.099,-0.094,-0.087,-0.08,-0.072,-0.063,-0.053,-0.043,-0.033,-0.023,-0.013,-0.003,0.007,0.017,0.026,0.034,0.041,0.047,0.052,0.056,0.059,0.06,0.06,0.058,0.055,0.051,0.045,0.038,0.031,0.022,0.012,0.002,-0.009,-0.02,-0.032,-0.042,-0.053,-0.062,-0.07,-0.077,-0.081,-0.083,-0.082,-0.078,-0.071,-0.062,-0.05,-0.031,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.037,-0.108,-0.309,-0.375,-0.386,-0.397,-0.416,-0.441,-0.463,-0.473,-0.462,-0.426,-0.364,-0.28,-0.187,-0.099,-0.029,0.021,0.054,0.074,0.087,0.095,0.102,0.107,0.118,0.21,0.55,-1.348,-1.275,-1.055,-0.986,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.979,-0.97,-0.937,-0.906,-0.88,-0.847,-0.713,-0.334,-0.214,-0.2,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.111,-0.061,-0.044,-0.043,-0.052,-0.092,-0.105,-0.106,-0.106,-0.107,-0.108,-0.108,-0.107,-0.106,-0.104,-0.101,-0.097,-0.092,-0.086,-0.079,-0.071,-0.063,-0.054,-0.044,-0.034,-0.024,-0.013,-0.002,0.008,0.018,0.027,0.036,0.045,0.052,0.058,0.064,0.068,0.07,0.072,0.072,0.071,0.068,0.064,0.059,0.053,0.045,0.037,0.028,0.018,0.007,-0.003,-0.014,-0.024,-0.034,-0.043,-0.051,-0.057,-0.061,-0.063,-0.063,-0.06,-0.055,-0.048,-0.039,-0.028,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.036,-0.102,-0.308,-0.375,-0.385,-0.394,-0.408,-0.424,-0.436,-0.438,-0.422,-0.384,-0.324,-0.247,-0.163,-0.084,-0.019,0.03,0.065,0.088,0.103,0.114,0.122,0.129,0.141,0.229,0.55,-1.333,-1.265,-1.052,-0.986,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.978,-0.956,-0.882,-0.835,-0.806,-0.774,-0.652,-0.315,-0.209,-0.199,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.108,-0.044,-0.023,-0.023,-0.041,-0.098,-0.113,-0.112,-0.109,-0.108,-0.107,-0.106,-0.104,-0.102,-0.099,-0.095,-0.091,-0.085,-0.078,-0.071,-0.063,-0.054,-0.045,-0.035,-0.024,-0.013,-0.003,0.008,0.019,0.029,0.039,0.048,0.056,0.063,0.07,0.075,0.08,0.083,0.084,0.085,0.084,0.082,0.078,0.073,0.067,0.06,0.052,0.043,0.034,0.024,0.013,0.003,-0.007,-0.017,-0.026,-0.034,-0.04,-0.045,-0.047,-0.047,-0.045,-0.042,-0.038,-0.032,-0.027,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.035,-0.1,-0.309,-0.377,-0.386,-0.393,-0.401,-0.409,-0.413,-0.405,-0.381,-0.338,-0.276,-0.201,-0.122,-0.048,0.015,0.063,0.098,0.123,0.14,0.152,0.161,0.168,0.179,0.259,0.55,-1.335,-1.267,-1.053,-0.986,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.977,-0.946,-0.842,-0.784,-0.751,-0.717,-0.605,-0.302,-0.208,-0.199,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.108,-0.042,-0.022,-0.022,-0.041,-0.101,-0.116,-0.113,-0.11,-0.107,-0.105,-0.103,-0.1,-0.097,-0.094,-0.089,-0.084,-0.078,-0.071,-0.063,-0.054,-0.045,-0.035,-0.025,-0.014,-0.003,0.008,0.019,0.03,0.04,0.05,0.059,0.068,0.075,0.082,0.087,0.092,0.095,0.097,0.098,0.097,0.095,0.092,0.087,0.082,0.075,0.067,0.058,0.049,0.039,0.029,0.019,0.008,-0.001,-0.01,-0.019,-0.025,-0.031,-0.033,-0.031,-0.03,-0.029,-0.028,-0.027,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.035,-0.1,-0.31,-0.378,-0.386,-0.39,-0.393,-0.394,-0.387,-0.369,-0.335,-0.283,-0.215,-0.138,-0.059,0.013,0.074,0.121,0.156,0.181,0.199,0.21,0.218,0.223,0.231,0.298,0.55,-1.348,-1.28,-1.057,-0.986,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.977,-0.939,-0.813,-0.745,-0.708,-0.672,-0.567,-0.294,-0.209,-0.199,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.108,-0.042,-0.022,-0.022,-0.041,-0.1,-0.114,-0.111,-0.107,-0.104,-0.102,-0.099,-0.096,-0.093,-0.088,-0.083,-0.077,-0.071,-0.063,-0.055,-0.045,-0.036,-0.025,-0.014,-0.003,0.008,0.02,0.031,0.042,0.052,0.062,0.071,0.08,0.087,0.094,0.1,0.104,0.108,0.11,0.111,0.11,0.109,0.106,0.101,0.096,0.089,0.082,0.073,0.064,0.054,0.044,0.033,0.023,0.013,0.004,-0.005,-0.012,-0.019,-0.023,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.035,-0.1,-0.31,-0.377,-0.383,-0.383,-0.38,-0.372,-0.355,-0.325,-0.279,-0.216,-0.14,-0.058,0.022,0.093,0.152,0.197,0.23,0.254,0.269,0.279,0.284,0.286,0.289,0.342,0.55,-1.37,-1.299,-1.062,-0.986,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.976,-0.934,-0.792,-0.715,-0.672,-0.634,-0.536,-0.29,-0.213,-0.201,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.108,-0.042,-0.022,-0.021,-0.039,-0.094,-0.107,-0.104,-0.101,-0.099,-0.097,-0.094,-0.091,-0.087,-0.083,-0.077,-0.071,-0.063,-0.055,-0.046,-0.036,-0.026,-0.015,-0.003,0.008,0.02,0.032,0.043,0.054,0.065,0.074,0.084,0.092,0.1,0.106,0.112,0.117,0.12,0.123,0.124,0.124,0.122,0.12,0.116,0.11,0.104,0.096,0.088,0.078,0.068,0.058,0.047,0.037,0.027,0.017,0.008,-0.001,-0.008,-0.015,-0.023,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.035,-0.1,-0.307,-0.371,-0.374,-0.37,-0.36,-0.343,-0.314,-0.271,-0.211,-0.137,-0.053,0.033,0.113,0.181,0.235,0.275,0.304,0.323,0.335,0.342,0.344,0.343,0.342,0.382,0.55,-1.397,-1.322,-1.068,-0.987,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.976,-0.93,-0.776,-0.69,-0.641,-0.599,-0.508,-0.289,-0.219,-0.202,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.108,-0.042,-0.022,-0.021,-0.036,-0.083,-0.095,-0.094,-0.093,-0.093,-0.091,-0.089,-0.086,-0.082,-0.077,-0.071,-0.064,-0.056,-0.047,-0.037,-0.026,-0.015,-0.004,0.008,0.02,0.033,0.044,0.056,0.067,0.077,0.087,0.096,0.105,0.112,0.119,0.125,0.13,0.133,0.136,0.137,0.138,0.136,0.134,0.13,0.125,0.118,0.111,0.102,0.093,0.082,0.072,0.061,0.05,0.039,0.029,0.019,0.01,0.002,-0.008,-0.021,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.035,-0.099,-0.3,-0.36,-0.359,-0.348,-0.33,-0.302,-0.261,-0.204,-0.132,-0.048,0.041,0.127,0.202,0.262,0.308,0.341,0.363,0.377,0.385,0.389,0.389,0.386,0.384,0.414,0.55,-1.424,-1.346,-1.074,-0.987,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.976,-0.927,-0.763,-0.666,-0.61,-0.564,-0.48,-0.289,-0.226,-0.204,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.108,-0.042,-0.022,-0.021,-0.033,-0.071,-0.082,-0.084,-0.085,-0.086,-0.085,-0.083,-0.08,-0.076,-0.071,-0.064,-0.056,-0.048,-0.038,-0.027,-0.016,-0.004,0.008,0.021,0.033,0.046,0.058,0.069,0.08,0.091,0.101,0.109,0.118,0.125,0.132,0.138,0.143,0.146,0.149,0.151,0.151,0.151,0.148,0.145,0.14,0.133,0.125,0.117,0.107,0.096,0.085,0.073,0.062,0.05,0.04,0.029,0.02,0.011,-0.001,-0.019,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.035,-0.096,-0.287,-0.341,-0.335,-0.317,-0.289,-0.25,-0.197,-0.128,-0.046,0.043,0.132,0.212,0.278,0.328,0.364,0.388,0.403,0.413,0.417,0.419,0.418,0.415,0.412,0.437,0.55,-1.451,-1.369,-1.08,-0.988,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.976,-0.925,-0.753,-0.643,-0.576,-0.526,-0.451,-0.289,-0.233,-0.207,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.108,-0.042,-0.022,-0.021,-0.03,-0.061,-0.07,-0.074,-0.077,-0.079,-0.079,-0.078,-0.074,-0.07,-0.064,-0.057,-0.048,-0.039,-0.029,-0.017,-0.005,0.007,0.02,0.034,0.047,0.059,0.072,0.083,0.094,0.105,0.114,0.123,0.131,0.138,0.145,0.151,0.156,0.16,0.163,0.165,0.165,0.165,0.163,0.16,0.155,0.148,0.14,0.131,0.121,0.109,0.097,0.085,0.073,0.061,0.049,0.038,0.028,0.019,0.005,-0.018,-0.024,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.035,-0.091,-0.269,-0.315,-0.301,-0.275,-0.238,-0.188,-0.124,-0.046,0.04,0.129,0.212,0.282,0.336,0.375,0.401,0.417,0.427,0.432,0.435,0.436,0.435,0.432,0.43,0.451,0.55,-1.473,-1.388,-1.085,-0.988,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.976,-0.923,-0.743,-0.617,-0.538,-0.484,-0.419,-0.287,-0.233,-0.207,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.108,-0.042,-0.022,-0.021,-0.028,-0.053,-0.062,-0.066,-0.07,-0.072,-0.073,-0.071,-0.068,-0.063,-0.057,-0.049,-0.04,-0.03,-0.019,-0.006,0.007,0.02,0.034,0.048,0.061,0.074,0.086,0.098,0.109,0.119,0.128,0.136,0.144,0.151,0.158,0.163,0.169,0.173,0.176,0.179,0.18,0.18,0.178,0.175,0.17,0.164,0.156,0.146,0.135,0.123,0.11,0.096,0.083,0.07,0.057,0.046,0.035,0.025,0.01,-0.016,-0.024,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.034,-0.085,-0.244,-0.28,-0.259,-0.224,-0.179,-0.12,-0.048,0.034,0.12,0.203,0.275,0.334,0.376,0.405,0.423,0.433,0.439,0.442,0.444,0.444,0.443,0.441,0.44,0.46,0.55,-1.491,-1.403,-1.089,-0.988,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.976,-0.922,-0.733,-0.59,-0.499,-0.444,-0.388,-0.278,-0.22,-0.201,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.128,-0.108,-0.042,-0.022,-0.021,-0.027,-0.049,-0.057,-0.061,-0.065,-0.067,-0.067,-0.065,-0.061,-0.056,-0.049,-0.04,-0.031,-0.02,-0.008,0.005,0.019,0.034,0.048,0.062,0.076,0.09,0.102,0.113,0.124,0.133,0.142,0.15,0.157,0.164,0.171,0.176,0.181,0.186,0.19,0.193,0.194,0.195,0.193,0.191,0.186,0.18,0.171,0.161,0.149,0.136,0.122,0.107,0.092,0.078,0.064,0.052,0.041,0.03,0.014,-0.015,-0.024,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.034,-0.078,-0.213,-0.237,-0.209,-0.167,-0.114,-0.049,0.026,0.107,0.188,0.262,0.323,0.369,0.402,0.423,0.435,0.442,0.445,0.447,0.447,0.447,0.447,0.446,0.445,0.465,0.55,-1.502,-1.413,-1.092,-0.989,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.976,-0.92,-0.724,-0.571,-0.471,-0.411,-0.363,-0.274,-0.214,-0.2,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.131,-0.129,-0.108,-0.042,-0.022,-0.021,-0.027,-0.047,-0.054,-0.058,-0.06,-0.062,-0.061,-0.058,-0.054,-0.048,-0.04,-0.031,-0.021,-0.009,0.004,0.018,0.033,0.048,0.063,0.078,0.093,0.106,0.118,0.129,0.139,0.148,0.156,0.164,0.171,0.177,0.183,0.189,0.194,0.199,0.203,0.207,0.209,0.21,0.209,0.207,0.203,0.196,0.188,0.177,0.165,0.15,0.134,0.118,0.101,0.085,0.069,0.056,0.044,0.033,0.016,-0.015,-0.024,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.033,-0.069,-0.178,-0.189,-0.155,-0.107,-0.049,0.019,0.093,0.17,0.242,0.306,0.356,0.393,0.418,0.433,0.441,0.446,0.448,0.448,0.449,0.449,0.448,0.448,0.448,0.468,0.55,-1.507,-1.417,-1.093,-0.989,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.975,-0.918,-0.717,-0.565,-0.461,-0.395,-0.348,-0.274,-0.214,-0.2,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.132,-0.142,-0.142,-0.117,-0.045,-0.023,-0.021,-0.027,-0.047,-0.053,-0.055,-0.056,-0.055,-0.054,-0.05,-0.045,-0.039,-0.031,-0.021,-0.01,0.003,0.017,0.032,0.048,0.064,0.08,0.095,0.11,0.123,0.135,0.146,0.155,0.163,0.171,0.177,0.184,0.19,0.196,0.201,0.207,0.212,0.217,0.221,0.224,0.226,0.226,0.224,0.22,0.214,0.206,0.195,0.181,0.165,0.147,0.128,0.109,0.09,0.073,0.058,0.045,0.033,0.017,-0.015,-0.024,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.032,-0.059,-0.139,-0.137,-0.097,-0.046,0.014,0.081,0.151,0.221,0.284,0.337,0.379,0.408,0.427,0.438,0.444,0.447,0.449,0.449,0.449,0.449,0.449,0.449,0.449,0.469,0.55,-1.504,-1.415,-1.092,-0.989,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.975,-0.916,-0.712,-0.567,-0.462,-0.39,-0.342,-0.275,-0.215,-0.2,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.13,-0.131,-0.142,-0.168,-0.173,-0.14,-0.066,-0.036,-0.03,-0.033,-0.047,-0.05,-0.05,-0.049,-0.048,-0.045,-0.042,-0.036,-0.029,-0.021,-0.011,0.001,0.015,0.03,0.046,0.063,0.08,0.097,0.113,0.128,0.141,0.152,0.162,0.171,0.178,0.185,0.191,0.196,0.202,0.207,0.213,0.219,0.224,0.23,0.235,0.239,0.242,0.243,0.242,0.239,0.233,0.225,0.213,0.199,0.181,0.161,0.139,0.117,0.095,0.075,0.058,0.043,0.032,0.015,-0.015,-0.024,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.031,-0.049,-0.098,-0.084,-0.041,0.012,0.071,0.135,0.2,0.261,0.315,0.36,0.394,0.417,0.433,0.441,0.446,0.448,0.449,0.449,0.449,0.449,0.449,0.449,0.45,0.47,0.55,-1.491,-1.406,-1.09,-0.989,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.975,-0.914,-0.703,-0.558,-0.453,-0.383,-0.336,-0.274,-0.216,-0.2,-0.199,-0.198,-0.185,-0.144,-0.131,-0.13,-0.132,-0.13,-0.129,-0.129,-0.128,-0.128,-0.128,-0.128,-0.129,-0.133,-0.147,-0.155,-0.138,-0.097,-0.071,-0.055,-0.046,-0.045,-0.043,-0.042,-0.04,-0.039,-0.036,-0.032,-0.027,-0.019,-0.01,0.001,0.014,0.028,0.045,0.062,0.08,0.098,0.115,0.132,0.146,0.158,0.169,0.178,0.186,0.193,0.198,0.203,0.208,0.213,0.219,0.224,0.23,0.237,0.243,0.249,0.254,0.258,0.26,0.261,0.258,0.253,0.245,0.234,0.218,0.199,0.177,0.152,0.126,0.1,0.077,0.057,0.041,0.028,0.013,-0.015,-0.024,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.025,-0.026,-0.029,-0.031,-0.038,-0.056,-0.031,0.014,0.066,0.123,0.182,0.239,0.293,0.339,0.376,0.404,0.423,0.436,0.443,0.447,0.448,0.449,0.449,0.45,0.45,0.45,0.449,0.45,0.471,0.55,-1.467,-1.388,-1.086,-0.988,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.975,-0.909,-0.681,-0.525,-0.403,-0.343,-0.308,-0.26,-0.213,-0.2,-0.199,-0.198,-0.185,-0.144,-0.131,-0.133,-0.144,-0.133,-0.119,-0.112,-0.11,-0.109,-0.109,-0.11,-0.11,-0.109,-0.109,-0.114,-0.106,-0.085,-0.065,-0.049,-0.037,-0.033,-0.031,-0.032,-0.031,-0.03,-0.027,-0.023,-0.017,-0.01,0,0.012,0.025,0.041,0.058,0.077,0.096,0.114,0.132,0.147,0.161,0.173,0.183,0.191,0.197,0.203,0.207,0.212,0.216,0.22,0.225,0.231,0.237,0.244,0.251,0.258,0.264,0.269,0.273,0.275,0.274,0.27,0.262,0.251,0.235,0.215,0.191,0.163,0.134,0.104,0.077,0.055,0.037,0.024,0.01,-0.011,-0.018,-0.019,-0.019,-0.02,-0.022,-0.022,-0.022,-0.022,-0.022,-0.022,-0.023,-0.029,-0.03,-0.028,-0.015,0.02,0.066,0.116,0.168,0.221,0.272,0.318,0.357,0.388,0.412,0.428,0.438,0.444,0.447,0.449,0.449,0.45,0.45,0.45,0.45,0.45,0.451,0.471,0.55,-1.428,-1.358,-1.078,-0.988,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.974,-0.899,-0.639,-0.458,-0.296,-0.239,-0.227,-0.214,-0.201,-0.199,-0.199,-0.198,-0.185,-0.144,-0.133,-0.145,-0.174,-0.14,-0.091,-0.063,-0.052,-0.049,-0.049,-0.051,-0.054,-0.062,-0.07,-0.078,-0.073,-0.059,-0.044,-0.032,-0.02,-0.016,-0.017,-0.021,-0.022,-0.021,-0.019,-0.015,-0.01,-0.003,0.005,0.015,0.027,0.04,0.055,0.071,0.086,0.102,0.116,0.129,0.14,0.149,0.156,0.162,0.166,0.17,0.173,0.176,0.179,0.182,0.186,0.191,0.196,0.202,0.209,0.215,0.221,0.226,0.23,0.232,0.232,0.23,0.225,0.216,0.203,0.186,0.165,0.14,0.114,0.087,0.062,0.042,0.026,0.016,0.008,-0.001,-0.003,-0.004,-0.004,-0.006,-0.011,-0.013,-0.013,-0.013,-0.013,-0.013,-0.017,-0.026,-0.029,-0.018,0.025,0.07,0.114,0.161,0.209,0.256,0.3,0.34,0.373,0.399,0.418,0.431,0.44,0.445,0.447,0.449,0.449,0.45,0.45,0.45,0.45,0.45,0.452,0.472,0.55,-1.37,-1.311,-1.067,-0.987,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.973,-0.882,-0.578,-0.403,-0.25,-0.205,-0.201,-0.2,-0.199,-0.199,-0.199,-0.198,-0.185,-0.144,-0.132,-0.146,-0.179,-0.137,-0.08,-0.046,-0.033,-0.029,-0.028,-0.03,-0.033,-0.04,-0.049,-0.054,-0.05,-0.04,-0.031,-0.024,-0.014,-0.011,-0.011,-0.013,-0.013,-0.013,-0.012,-0.011,-0.01,-0.008,-0.006,-0.003,0,0.003,0.007,0.011,0.015,0.019,0.023,0.026,0.029,0.031,0.033,0.034,0.036,0.036,0.037,0.038,0.039,0.039,0.04,0.042,0.043,0.044,0.046,0.048,0.05,0.051,0.052,0.053,0.053,0.052,0.051,0.049,0.045,0.041,0.036,0.029,0.022,0.015,0.008,0.003,-0.001,-0.004,-0.005,-0.007,-0.007,-0.007,-0.007,-0.008,-0.01,-0.01,-0.01,-0.01,-0.01,-0.011,-0.014,-0.026,-0.028,-0.008,0.064,0.116,0.16,0.203,0.246,0.288,0.326,0.359,0.387,0.409,0.424,0.435,0.442,0.446,0.449,0.45,0.45,0.45,0.451,0.451,0.451,0.451,0.453,0.474,0.55,-1.291,-1.247,-1.05,-0.985,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.971,-0.86,-0.506,-0.346,-0.232,-0.201,-0.199,-0.199,-0.199,-0.199,-0.199,-0.198,-0.185,-0.144,-0.132,-0.142,-0.166,-0.126,-0.075,-0.044,-0.031,-0.027,-0.026,-0.026,-0.029,-0.033,-0.038,-0.039,-0.035,-0.029,-0.024,-0.02,-0.012,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.009,-0.009,-0.009,-0.009,-0.008,-0.008,-0.008,-0.007,-0.007,-0.007,-0.007,-0.007,-0.007,-0.006,-0.006,-0.006,-0.006,-0.006,-0.006,-0.006,-0.006,-0.006,-0.006,-0.006,-0.006,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.006,-0.006,-0.006,-0.007,-0.008,-0.008,-0.009,-0.009,-0.009,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.014,-0.026,-0.027,0.002,0.101,0.161,0.203,0.242,0.281,0.318,0.351,0.379,0.402,0.42,0.433,0.442,0.448,0.451,0.452,0.453,0.453,0.453,0.453,0.453,0.454,0.455,0.457,0.478,0.55,-1.192,-1.165,-1.029,-0.984,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.97,-0.845,-0.452,-0.301,-0.221,-0.2,-0.199,-0.199,-0.199,-0.199,-0.199,-0.198,-0.185,-0.144,-0.132,-0.136,-0.142,-0.107,-0.067,-0.042,-0.03,-0.026,-0.025,-0.025,-0.026,-0.028,-0.029,-0.028,-0.025,-0.022,-0.02,-0.018,-0.012,-0.01,-0.01,-0.01,-0.01,-0.01,-0.009,-0.009,-0.009,-0.009,-0.009,-0.008,-0.008,-0.008,-0.007,-0.007,-0.007,-0.006,-0.006,-0.006,-0.006,-0.006,-0.006,-0.006,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.004,-0.003,-0.003,-0.003,-0.004,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.009,-0.009,-0.009,-0.009,-0.009,-0.009,-0.009,-0.009,-0.013,-0.026,-0.027,0.013,0.138,0.205,0.244,0.281,0.316,0.348,0.377,0.401,0.421,0.436,0.447,0.454,0.458,0.459,0.46,0.46,0.459,0.459,0.459,0.46,0.461,0.463,0.466,0.485,0.55,-1.079,-1.07,-1.004,-0.982,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.969,-0.837,-0.419,-0.273,-0.215,-0.2,-0.199,-0.199,-0.198,-0.198,-0.198,-0.196,-0.184,-0.144,-0.131,-0.126,-0.109,-0.078,-0.05,-0.034,-0.026,-0.023,-0.022,-0.021,-0.021,-0.021,-0.02,-0.018,-0.016,-0.015,-0.015,-0.014,-0.011,-0.01,-0.009,-0.008,-0.006,-0.004,-0.001,0.001,0.003,0.006,0.008,0.011,0.015,0.02,0.025,0.03,0.034,0.038,0.04,0.042,0.043,0.044,0.044,0.036,-0.002,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,0.003,0.061,0.075,0.075,0.075,0.071,0.054,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.042,0.006,-0.001,0.002,0.005,0.008,0.009,0.01,0.01,0.003,-0.023,-0.019,0.039,0.177,0.247,0.285,0.319,0.351,0.38,0.405,0.427,0.444,0.457,0.466,0.472,0.475,0.476,0.476,0.475,0.474,0.474,0.474,0.475,0.477,0.479,0.483,0.499,0.55,-0.96,-0.97,-0.978,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.969,-0.831,-0.398,-0.254,-0.21,-0.199,-0.199,-0.196,-0.187,-0.185,-0.185,-0.184,-0.173,-0.142,-0.13,-0.115,-0.069,-0.039,-0.023,-0.015,-0.012,-0.012,-0.013,-0.013,-0.012,-0.009,-0.006,-0.004,-0.003,-0.004,-0.006,-0.007,-0.007,-0.006,-0.003,0.002,0.009,0.018,0.027,0.036,0.045,0.054,0.064,0.076,0.091,0.109,0.128,0.148,0.165,0.179,0.189,0.195,0.199,0.202,0.199,0.158,0.036,-0.006,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.004,0.061,0.25,0.318,0.327,0.325,0.304,0.247,0.228,0.226,0.226,0.226,0.226,0.226,0.226,0.226,0.226,0.226,0.222,0.182,0.07,0.036,0.043,0.056,0.067,0.075,0.079,0.08,0.06,0.006,0.03,0.106,0.229,0.29,0.325,0.357,0.387,0.414,0.437,0.457,0.473,0.486,0.495,0.5,0.503,0.505,0.505,0.504,0.503,0.503,0.503,0.504,0.506,0.508,0.512,0.522,0.55,-0.846,-0.875,-0.953,-0.978,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.968,-0.828,-0.383,-0.238,-0.206,-0.199,-0.198,-0.187,-0.156,-0.145,-0.144,-0.144,-0.142,-0.132,-0.128,-0.105,-0.033,0,0.01,0.012,0.01,0.006,0.003,0.002,0.003,0.007,0.011,0.014,0.015,0.013,0.01,0.008,0.007,0.008,0.011,0.018,0.027,0.038,0.05,0.06,0.07,0.08,0.091,0.106,0.124,0.147,0.172,0.196,0.216,0.232,0.242,0.249,0.253,0.255,0.252,0.202,0.045,-0.006,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.003,0.076,0.321,0.405,0.416,0.415,0.388,0.309,0.284,0.282,0.282,0.282,0.282,0.282,0.282,0.282,0.282,0.282,0.278,0.236,0.111,0.078,0.089,0.108,0.126,0.141,0.154,0.162,0.146,0.101,0.149,0.211,0.287,0.333,0.366,0.396,0.425,0.45,0.473,0.492,0.509,0.522,0.531,0.538,0.543,0.546,0.548,0.549,0.549,0.549,0.55,0.551,0.552,0.553,0.555,0.555,0.55,-0.747,-0.793,-0.931,-0.976,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.968,-0.824,-0.369,-0.224,-0.202,-0.199,-0.198,-0.185,-0.145,-0.132,-0.131,-0.131,-0.131,-0.13,-0.128,-0.099,-0.006,0.032,0.04,0.04,0.035,0.028,0.023,0.02,0.021,0.025,0.03,0.035,0.036,0.035,0.033,0.029,0.025,0.024,0.026,0.033,0.043,0.055,0.067,0.078,0.087,0.096,0.106,0.12,0.139,0.162,0.188,0.212,0.231,0.245,0.253,0.258,0.26,0.261,0.258,0.207,0.045,-0.006,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.003,0.078,0.335,0.421,0.432,0.431,0.402,0.316,0.289,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.284,0.255,0.165,0.142,0.152,0.171,0.192,0.215,0.239,0.262,0.277,0.274,0.306,0.326,0.346,0.375,0.406,0.436,0.463,0.488,0.511,0.531,0.548,0.563,0.575,0.584,0.592,0.598,0.603,0.606,0.609,0.61,0.611,0.612,0.611,0.611,0.609,0.596,0.55,-0.668,-0.728,-0.915,-0.975,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.968,-0.82,-0.353,-0.207,-0.197,-0.198,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.127,-0.095,0.009,0.052,0.062,0.063,0.057,0.049,0.041,0.037,0.038,0.043,0.049,0.054,0.057,0.057,0.053,0.047,0.041,0.037,0.037,0.043,0.055,0.07,0.084,0.095,0.103,0.111,0.12,0.132,0.15,0.174,0.2,0.223,0.241,0.252,0.259,0.261,0.262,0.263,0.259,0.208,0.045,-0.006,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.003,0.079,0.34,0.426,0.437,0.435,0.406,0.317,0.289,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.286,0.274,0.238,0.227,0.232,0.246,0.268,0.296,0.33,0.365,0.396,0.417,0.434,0.43,0.403,0.416,0.445,0.474,0.502,0.527,0.55,0.571,0.59,0.606,0.621,0.633,0.644,0.653,0.66,0.666,0.671,0.674,0.675,0.675,0.673,0.67,0.665,0.638,0.55,-0.612,-0.683,-0.903,-0.974,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.967,-0.816,-0.337,-0.191,-0.193,-0.198,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.127,-0.093,0.016,0.062,0.076,0.078,0.073,0.064,0.055,0.051,0.052,0.058,0.064,0.07,0.073,0.073,0.069,0.061,0.053,0.049,0.05,0.057,0.069,0.086,0.102,0.114,0.12,0.125,0.132,0.143,0.161,0.185,0.21,0.233,0.249,0.258,0.263,0.264,0.264,0.264,0.259,0.208,0.046,-0.006,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.003,0.08,0.342,0.429,0.439,0.437,0.407,0.317,0.289,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.293,0.308,0.311,0.314,0.324,0.344,0.377,0.417,0.457,0.49,0.514,0.525,0.51,0.452,0.456,0.483,0.512,0.539,0.565,0.589,0.61,0.63,0.648,0.664,0.678,0.69,0.701,0.71,0.717,0.722,0.726,0.727,0.727,0.724,0.72,0.712,0.675,0.55,-0.577,-0.656,-0.897,-0.974,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.967,-0.813,-0.327,-0.18,-0.191,-0.198,-0.198,-0.185,-0.144,-0.131,-0.13,-0.13,-0.13,-0.13,-0.127,-0.093,0.017,0.064,0.081,0.086,0.082,0.073,0.065,0.061,0.063,0.069,0.076,0.081,0.084,0.085,0.082,0.074,0.068,0.068,0.072,0.079,0.091,0.108,0.124,0.134,0.138,0.14,0.144,0.153,0.17,0.193,0.218,0.239,0.254,0.261,0.264,0.264,0.264,0.263,0.259,0.207,0.046,-0.006,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.003,0.08,0.342,0.429,0.439,0.437,0.406,0.317,0.289,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.287,0.288,0.306,0.361,0.38,0.384,0.393,0.412,0.446,0.487,0.526,0.555,0.573,0.58,0.56,0.492,0.492,0.519,0.548,0.575,0.6,0.624,0.646,0.666,0.684,0.7,0.715,0.727,0.738,0.747,0.753,0.758,0.761,0.762,0.762,0.759,0.755,0.747,0.702,0.55,-0.559,-0.644,-0.894,-0.973,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.967,-0.812,-0.322,-0.175,-0.189,-0.198,-0.198,-0.185,-0.142,-0.13,-0.128,-0.128,-0.128,-0.128,-0.125,-0.091,0.014,0.061,0.087,0.096,0.093,0.087,0.08,0.078,0.081,0.087,0.092,0.096,0.099,0.1,0.098,0.093,0.09,0.093,0.1,0.106,0.115,0.13,0.142,0.149,0.15,0.149,0.151,0.157,0.171,0.19,0.21,0.227,0.238,0.244,0.245,0.245,0.244,0.244,0.239,0.191,0.047,-0.001,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.005,-0.004,-0.004,-0.004,0.003,0.086,0.341,0.428,0.438,0.435,0.405,0.319,0.29,0.287,0.285,0.284,0.284,0.284,0.285,0.286,0.288,0.289,0.292,0.32,0.403,0.436,0.443,0.45,0.468,0.499,0.537,0.57,0.592,0.604,0.608,0.587,0.523,0.525,0.552,0.58,0.607,0.632,0.655,0.676,0.696,0.713,0.729,0.742,0.754,0.763,0.77,0.776,0.779,0.781,0.782,0.782,0.78,0.777,0.769,0.72,0.55,-0.554,-0.641,-0.893,-0.973,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.967,-0.811,-0.32,-0.172,-0.188,-0.198,-0.198,-0.182,-0.125,-0.109,-0.107,-0.106,-0.104,-0.101,-0.095,-0.065,0.018,0.064,0.1,0.114,0.114,0.112,0.11,0.11,0.113,0.116,0.119,0.121,0.122,0.123,0.123,0.122,0.121,0.124,0.128,0.131,0.136,0.145,0.151,0.153,0.152,0.15,0.15,0.152,0.158,0.168,0.178,0.186,0.191,0.194,0.194,0.194,0.194,0.194,0.191,0.163,0.079,0.051,0.049,0.049,0.049,0.049,0.05,0.051,0.052,0.054,0.058,0.061,0.065,0.069,0.079,0.15,0.35,0.423,0.434,0.432,0.406,0.336,0.305,0.287,0.268,0.254,0.251,0.258,0.27,0.285,0.3,0.314,0.327,0.363,0.449,0.486,0.494,0.498,0.511,0.537,0.569,0.595,0.61,0.618,0.619,0.602,0.549,0.554,0.581,0.608,0.634,0.659,0.681,0.701,0.72,0.736,0.75,0.761,0.771,0.778,0.784,0.788,0.79,0.792,0.792,0.792,0.791,0.788,0.781,0.731,0.55,-0.555,-0.643,-0.894,-0.973,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.967,-0.812,-0.32,-0.172,-0.188,-0.197,-0.195,-0.16,-0.053,-0.018,-0.013,-0.009,-0.003,0.006,0.015,0.029,0.059,0.075,0.091,0.096,0.096,0.094,0.094,0.098,0.103,0.109,0.114,0.116,0.118,0.12,0.122,0.122,0.123,0.128,0.134,0.141,0.152,0.166,0.175,0.177,0.173,0.169,0.167,0.171,0.18,0.195,0.21,0.223,0.231,0.236,0.238,0.238,0.237,0.237,0.236,0.234,0.224,0.222,0.222,0.223,0.223,0.224,0.225,0.228,0.234,0.243,0.255,0.27,0.285,0.299,0.311,0.334,0.403,0.422,0.426,0.425,0.415,0.379,0.342,0.285,0.217,0.167,0.156,0.181,0.23,0.287,0.346,0.399,0.439,0.473,0.518,0.535,0.537,0.536,0.542,0.562,0.588,0.608,0.618,0.623,0.623,0.61,0.572,0.581,0.607,0.633,0.658,0.681,0.702,0.721,0.738,0.752,0.764,0.774,0.782,0.787,0.791,0.794,0.796,0.797,0.797,0.797,0.796,0.794,0.788,0.737,0.55,-0.559,-0.648,-0.896,-0.974,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.967,-0.812,-0.322,-0.173,-0.181,-0.189,-0.181,-0.112,0.03,0.082,0.09,0.093,0.096,0.101,0.105,0.109,0.114,0.117,0.12,0.12,0.12,0.121,0.124,0.128,0.133,0.138,0.14,0.142,0.143,0.144,0.146,0.147,0.149,0.154,0.16,0.165,0.172,0.179,0.182,0.182,0.179,0.176,0.175,0.177,0.184,0.193,0.203,0.211,0.217,0.22,0.221,0.222,0.222,0.221,0.223,0.237,0.27,0.283,0.283,0.282,0.281,0.281,0.282,0.285,0.291,0.301,0.315,0.332,0.349,0.366,0.379,0.391,0.406,0.411,0.414,0.412,0.403,0.379,0.339,0.277,0.208,0.161,0.154,0.187,0.25,0.328,0.403,0.465,0.509,0.539,0.562,0.571,0.569,0.564,0.564,0.577,0.598,0.613,0.622,0.625,0.625,0.616,0.591,0.603,0.629,0.654,0.677,0.699,0.718,0.736,0.751,0.763,0.774,0.782,0.788,0.792,0.795,0.797,0.798,0.799,0.799,0.799,0.798,0.797,0.792,0.741,0.55,-0.563,-0.652,-0.897,-0.974,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.967,-0.813,-0.325,-0.169,-0.162,-0.163,-0.146,-0.078,-0.009,0.006,0.01,0.016,0.023,0.029,0.033,0.038,0.042,0.044,0.044,0.043,0.045,0.051,0.06,0.073,0.086,0.097,0.104,0.107,0.11,0.112,0.116,0.122,0.13,0.143,0.158,0.173,0.185,0.194,0.196,0.192,0.186,0.181,0.18,0.186,0.199,0.218,0.238,0.255,0.267,0.275,0.28,0.282,0.282,0.281,0.28,0.283,0.297,0.298,0.295,0.293,0.291,0.289,0.289,0.291,0.296,0.305,0.317,0.332,0.348,0.363,0.376,0.386,0.394,0.398,0.399,0.394,0.383,0.36,0.324,0.274,0.226,0.197,0.2,0.238,0.305,0.385,0.46,0.515,0.549,0.571,0.586,0.592,0.589,0.581,0.578,0.586,0.602,0.615,0.622,0.625,0.626,0.621,0.607,0.623,0.647,0.671,0.693,0.713,0.731,0.747,0.76,0.771,0.78,0.787,0.792,0.795,0.797,0.798,0.799,0.799,0.799,0.799,0.799,0.799,0.794,0.742,0.55,-0.565,-0.654,-0.897,-0.974,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.967,-0.814,-0.33,-0.173,-0.159,-0.158,-0.136,-0.063,-0.032,-0.027,-0.022,-0.015,-0.008,-0.003,-0.001,0.002,0.004,0.006,0.005,0.006,0.009,0.018,0.03,0.047,0.065,0.079,0.088,0.093,0.096,0.099,0.104,0.111,0.122,0.138,0.157,0.175,0.188,0.196,0.196,0.192,0.186,0.182,0.182,0.189,0.206,0.229,0.253,0.274,0.289,0.299,0.305,0.308,0.308,0.308,0.307,0.307,0.307,0.305,0.301,0.297,0.294,0.292,0.292,0.293,0.297,0.304,0.314,0.327,0.341,0.354,0.366,0.375,0.381,0.383,0.382,0.376,0.364,0.343,0.314,0.28,0.251,0.24,0.255,0.298,0.365,0.441,0.508,0.552,0.576,0.59,0.599,0.603,0.599,0.591,0.585,0.59,0.603,0.615,0.622,0.625,0.626,0.624,0.621,0.639,0.662,0.685,0.706,0.724,0.741,0.755,0.767,0.777,0.784,0.79,0.794,0.797,0.798,0.799,0.799,0.8,0.8,0.8,0.799,0.799,0.795,0.743,0.55,-0.566,-0.655,-0.898,-0.974,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.967,-0.817,-0.338,-0.181,-0.163,-0.161,-0.138,-0.058,-0.032,-0.026,-0.021,-0.015,-0.01,-0.007,-0.007,-0.007,-0.006,-0.006,-0.005,-0.004,0.001,0.01,0.024,0.041,0.06,0.075,0.085,0.091,0.095,0.098,0.103,0.11,0.122,0.137,0.155,0.171,0.182,0.188,0.189,0.186,0.182,0.179,0.182,0.191,0.208,0.231,0.256,0.277,0.292,0.302,0.309,0.312,0.313,0.313,0.313,0.312,0.311,0.308,0.304,0.3,0.297,0.294,0.293,0.294,0.296,0.302,0.311,0.322,0.334,0.346,0.356,0.363,0.368,0.37,0.367,0.361,0.35,0.333,0.312,0.292,0.279,0.282,0.306,0.351,0.415,0.483,0.539,0.574,0.592,0.602,0.607,0.608,0.603,0.595,0.588,0.59,0.6,0.612,0.619,0.623,0.625,0.626,0.633,0.652,0.675,0.696,0.715,0.732,0.748,0.76,0.771,0.78,0.787,0.792,0.795,0.797,0.799,0.799,0.8,0.8,0.8,0.8,0.8,0.799,0.795,0.744,0.55,-0.564,-0.654,-0.897,-0.974,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.968,-0.82,-0.348,-0.19,-0.17,-0.166,-0.141,-0.056,-0.028,-0.022,-0.018,-0.013,-0.009,-0.008,-0.008,-0.009,-0.01,-0.01,-0.009,-0.006,-0.001,0.008,0.022,0.039,0.057,0.072,0.083,0.09,0.095,0.099,0.104,0.111,0.122,0.135,0.149,0.162,0.172,0.177,0.179,0.178,0.176,0.176,0.18,0.191,0.209,0.231,0.255,0.276,0.291,0.302,0.308,0.312,0.314,0.314,0.314,0.314,0.312,0.309,0.306,0.302,0.299,0.296,0.294,0.294,0.296,0.301,0.308,0.318,0.328,0.338,0.346,0.353,0.357,0.357,0.355,0.35,0.34,0.329,0.316,0.307,0.305,0.318,0.346,0.391,0.448,0.506,0.553,0.583,0.599,0.606,0.609,0.607,0.602,0.594,0.587,0.588,0.596,0.606,0.614,0.619,0.622,0.626,0.642,0.663,0.685,0.704,0.722,0.738,0.752,0.764,0.774,0.782,0.788,0.792,0.796,0.798,0.799,0.799,0.8,0.8,0.8,0.8,0.8,0.799,0.795,0.744,0.55,-0.56,-0.65,-0.896,-0.974,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.968,-0.822,-0.356,-0.199,-0.178,-0.173,-0.145,-0.061,-0.03,-0.025,-0.021,-0.017,-0.015,-0.014,-0.014,-0.015,-0.016,-0.016,-0.015,-0.011,-0.006,0.003,0.015,0.031,0.048,0.063,0.074,0.082,0.087,0.092,0.097,0.104,0.113,0.124,0.135,0.146,0.154,0.16,0.163,0.164,0.165,0.168,0.174,0.186,0.204,0.226,0.249,0.269,0.284,0.295,0.302,0.307,0.309,0.31,0.31,0.31,0.308,0.305,0.302,0.299,0.296,0.293,0.291,0.291,0.293,0.296,0.302,0.31,0.319,0.327,0.335,0.34,0.343,0.344,0.343,0.339,0.333,0.326,0.32,0.319,0.325,0.343,0.372,0.414,0.463,0.511,0.551,0.578,0.593,0.601,0.603,0.601,0.596,0.589,0.583,0.583,0.59,0.599,0.607,0.613,0.616,0.624,0.647,0.67,0.691,0.709,0.726,0.74,0.753,0.764,0.773,0.78,0.786,0.79,0.792,0.794,0.795,0.795,0.796,0.796,0.796,0.796,0.796,0.795,0.791,0.74,0.55,-0.553,-0.644,-0.895,-0.973,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.98,-0.968,-0.823,-0.362,-0.207,-0.186,-0.18,-0.161,-0.118,-0.098,-0.094,-0.091,-0.089,-0.087,-0.087,-0.087,-0.087,-0.088,-0.088,-0.086,-0.084,-0.079,-0.072,-0.062,-0.051,-0.038,-0.026,-0.016,-0.009,-0.004,0.002,0.007,0.014,0.022,0.031,0.041,0.051,0.06,0.069,0.076,0.083,0.091,0.101,0.113,0.13,0.15,0.173,0.196,0.216,0.232,0.245,0.253,0.259,0.263,0.265,0.266,0.267,0.266,0.265,0.263,0.26,0.258,0.256,0.255,0.255,0.256,0.258,0.263,0.268,0.275,0.281,0.286,0.291,0.294,0.295,0.295,0.294,0.292,0.291,0.291,0.296,0.308,0.327,0.356,0.392,0.433,0.473,0.509,0.536,0.555,0.567,0.574,0.578,0.578,0.576,0.574,0.576,0.582,0.59,0.597,0.603,0.607,0.616,0.64,0.66,0.676,0.691,0.703,0.715,0.725,0.733,0.74,0.745,0.75,0.751,0.747,0.747,0.748,0.748,0.749,0.749,0.749,0.749,0.749,0.749,0.744,0.701,0.55,-0.543,-0.646,-0.896,-0.977,-0.983,-0.983,-0.983,-0.983,-0.983,-0.983,-0.983,-0.983,-0.983,-0.983,-0.972,-0.828,-0.371,-0.218,-0.198,-0.197,-0.223,-0.29,-0.317,-0.32,-0.319,-0.319,-0.319,-0.319,-0.319,-0.319,-0.319,-0.319,-0.319,-0.318,-0.316,-0.314,-0.312,-0.308,-0.304,-0.3,-0.296,-0.293,-0.289,-0.285,-0.281,-0.274,-0.267,-0.257,-0.245,-0.232,-0.216,-0.198,-0.178,-0.155,-0.13,-0.103,-0.075,-0.046,-0.017,0.011,0.036,0.059,0.078,0.093,0.105,0.115,0.122,0.128,0.132,0.135,0.137,0.138,0.139,0.139,0.139,0.139,0.139,0.14,0.14,0.141,0.142,0.144,0.146,0.148,0.15,0.153,0.155,0.157,0.159,0.162,0.166,0.172,0.179,0.19,0.205,0.225,0.25,0.281,0.315,0.351,0.387,0.421,0.452,0.479,0.503,0.522,0.537,0.55,0.559,0.567,0.575,0.582,0.587,0.591,0.594,0.597,0.606,0.612,0.616,0.62,0.624,0.627,0.63,0.632,0.633,0.635,0.635,0.628,0.608,0.601,0.6,0.601,0.601,0.601,0.601,0.601,0.601,0.601,0.6,0.593,0.55,-0.53,-0.738,-0.949,-1.018,-1.025,-1.025,-1.025,-1.025,-1.025,-1.025,-1.025,-1.025,-1.025,-1.025,-1.013,-0.869,-0.411,-0.26,-0.242,-0.241,-0.271,-0.357,-0.388,-0.392,-0.392,-0.392,-0.392,-0.392,-0.392,-0.392,-0.393,-0.392,-0.392,-0.392,-0.392,-0.391,-0.391,-0.39,-0.389,-0.388,-0.386,-0.384,-0.381,-0.378,-0.373,-0.367,-0.36,-0.35,-0.339,-0.324,-0.306,-0.285,-0.26,-0.232,-0.202,-0.169,-0.135,-0.101,-0.069,-0.039,-0.012,0.011,0.031,0.048,0.061,0.071,0.08,0.086,0.091,0.094,0.097,0.099,0.1,0.101,0.102,0.103,0.103,0.103,0.104,0.104,0.104,0.105,0.105,0.106,0.107,0.108,0.11,0.112,0.115,0.119,0.124,0.131,0.141,0.153,0.169,0.189,0.213,0.242,0.275,0.311,0.349,0.386,0.422,0.455,0.484,0.509,0.529,0.545,0.558,0.568,0.576,0.582,0.586,0.59,0.592,0.594,0.596,0.597,0.598,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.599,0.591,0.563,0.555,0.554,0.554,0.554,0.554,0.554,0.554,0.554,0.554,0.554,0.554,0.55,-1.2,-1.09,-1.141,-1.155,-1.155,-1.155,-1.155,-1.155,-1.155,-1.155,-1.155,-1.155,-1.155,-1.155,-1.143,-0.993,-0.522,-0.371,-0.357,-0.357,-0.363,-0.391,-0.398,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.398,-0.398,-0.398,-0.397,-0.396,-0.395,-0.393,-0.39,-0.387,-0.383,-0.378,-0.37,-0.361,-0.35,-0.335,-0.317,-0.296,-0.27,-0.242,-0.21,-0.176,-0.14,-0.106,-0.072,-0.041,-0.014,0.01,0.03,0.046,0.06,0.07,0.078,0.084,0.089,0.092,0.094,0.096,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.101,0.101,0.101,0.102,0.102,0.103,0.104,0.105,0.107,0.109,0.113,0.118,0.125,0.134,0.146,0.162,0.182,0.206,0.236,0.27,0.307,0.346,0.385,0.423,0.457,0.487,0.512,0.533,0.549,0.562,0.572,0.579,0.585,0.589,0.591,0.594,0.595,0.596,0.597,0.597,0.598,0.598,0.598,0.598,0.598,0.598,0.598,0.597,0.588,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.19,-1.197,-1.197,-1.197,-1.197,-1.197,-1.197,-1.197,-1.197,-1.197,-1.197,-1.197,-1.197,-1.184,-1.033,-0.557,-0.406,-0.394,-0.394,-0.395,-0.398,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.398,-0.398,-0.397,-0.396,-0.394,-0.392,-0.389,-0.385,-0.38,-0.373,-0.364,-0.353,-0.339,-0.321,-0.299,-0.274,-0.245,-0.212,-0.177,-0.141,-0.105,-0.071,-0.039,-0.011,0.013,0.033,0.049,0.062,0.072,0.08,0.086,0.09,0.093,0.095,0.097,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.101,0.101,0.102,0.103,0.104,0.105,0.108,0.111,0.116,0.122,0.131,0.142,0.158,0.178,0.202,0.232,0.267,0.306,0.346,0.387,0.426,0.461,0.492,0.517,0.538,0.554,0.566,0.575,0.582,0.587,0.591,0.593,0.595,0.596,0.597,0.598,0.598,0.598,0.599,0.599,0.599,0.599,0.599,0.599,0.598,0.588,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.409,-0.397,-0.398,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.399,-0.399,-0.399,-0.398,-0.397,-0.397,-0.395,-0.393,-0.391,-0.387,-0.382,-0.376,-0.367,-0.356,-0.342,-0.324,-0.303,-0.277,-0.247,-0.214,-0.178,-0.141,-0.104,-0.069,-0.037,-0.009,0.015,0.035,0.052,0.064,0.074,0.082,0.087,0.091,0.094,0.096,0.097,0.098,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.101,0.101,0.102,0.103,0.104,0.106,0.109,0.113,0.119,0.128,0.139,0.154,0.174,0.199,0.229,0.265,0.304,0.346,0.389,0.429,0.465,0.496,0.521,0.542,0.558,0.57,0.578,0.585,0.589,0.592,0.595,0.596,0.597,0.598,0.598,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.598,0.589,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.41,-0.397,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.399,-0.399,-0.398,-0.397,-0.396,-0.394,-0.392,-0.388,-0.384,-0.378,-0.369,-0.359,-0.345,-0.327,-0.306,-0.28,-0.25,-0.216,-0.18,-0.142,-0.104,-0.068,-0.035,-0.007,0.018,0.038,0.054,0.066,0.076,0.083,0.088,0.092,0.095,0.097,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.101,0.102,0.103,0.105,0.108,0.112,0.117,0.125,0.136,0.151,0.17,0.195,0.226,0.262,0.303,0.347,0.39,0.431,0.468,0.5,0.525,0.546,0.561,0.573,0.581,0.587,0.591,0.594,0.596,0.597,0.598,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.598,0.589,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.41,-0.398,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.399,-0.398,-0.397,-0.395,-0.393,-0.39,-0.385,-0.379,-0.372,-0.361,-0.347,-0.33,-0.309,-0.283,-0.252,-0.218,-0.181,-0.142,-0.104,-0.067,-0.034,-0.005,0.02,0.04,0.056,0.068,0.077,0.084,0.089,0.093,0.095,0.097,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.103,0.104,0.107,0.11,0.115,0.123,0.133,0.148,0.167,0.192,0.223,0.26,0.302,0.347,0.391,0.434,0.471,0.503,0.529,0.549,0.564,0.575,0.583,0.589,0.592,0.595,0.597,0.598,0.599,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.599,0.589,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.41,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.398,-0.397,-0.396,-0.394,-0.391,-0.387,-0.381,-0.373,-0.363,-0.35,-0.332,-0.311,-0.285,-0.254,-0.219,-0.181,-0.142,-0.103,-0.066,-0.032,-0.003,0.022,0.042,0.057,0.07,0.079,0.085,0.09,0.094,0.096,0.097,0.098,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.103,0.106,0.109,0.114,0.121,0.131,0.145,0.164,0.189,0.221,0.258,0.301,0.347,0.393,0.436,0.474,0.506,0.532,0.552,0.567,0.577,0.585,0.59,0.594,0.596,0.597,0.598,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.589,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.41,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.399,-0.398,-0.396,-0.394,-0.392,-0.388,-0.382,-0.375,-0.365,-0.351,-0.334,-0.313,-0.287,-0.256,-0.221,-0.182,-0.142,-0.103,-0.065,-0.031,-0.002,0.023,0.043,0.059,0.071,0.08,0.086,0.091,0.094,0.096,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.103,0.105,0.108,0.112,0.119,0.129,0.143,0.162,0.187,0.219,0.257,0.3,0.347,0.394,0.438,0.476,0.509,0.535,0.554,0.569,0.579,0.586,0.591,0.594,0.597,0.598,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.589,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.41,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.398,-0.397,-0.395,-0.392,-0.388,-0.383,-0.376,-0.366,-0.353,-0.336,-0.314,-0.288,-0.257,-0.222,-0.183,-0.143,-0.102,-0.064,-0.03,0,0.024,0.044,0.06,0.072,0.081,0.087,0.091,0.095,0.097,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.104,0.107,0.111,0.118,0.128,0.141,0.16,0.185,0.217,0.256,0.3,0.347,0.395,0.439,0.478,0.511,0.537,0.556,0.57,0.58,0.587,0.592,0.595,0.597,0.598,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.59,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.561,-0.41,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.398,-0.397,-0.395,-0.393,-0.389,-0.384,-0.376,-0.367,-0.354,-0.337,-0.315,-0.289,-0.258,-0.222,-0.183,-0.143,-0.102,-0.064,-0.029,0,0.025,0.045,0.061,0.072,0.081,0.087,0.092,0.095,0.097,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.104,0.107,0.111,0.117,0.126,0.14,0.159,0.184,0.216,0.255,0.299,0.347,0.395,0.44,0.48,0.512,0.538,0.557,0.572,0.581,0.588,0.593,0.596,0.597,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.59,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.561,-0.41,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.398,-0.397,-0.395,-0.393,-0.389,-0.384,-0.377,-0.367,-0.354,-0.337,-0.316,-0.29,-0.258,-0.223,-0.184,-0.143,-0.102,-0.064,-0.029,0.001,0.026,0.046,0.061,0.073,0.081,0.088,0.092,0.095,0.097,0.098,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.104,0.106,0.11,0.117,0.126,0.139,0.158,0.183,0.215,0.254,0.299,0.347,0.396,0.441,0.481,0.513,0.539,0.558,0.572,0.582,0.589,0.593,0.596,0.598,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.59,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.561,-0.41,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.398,-0.397,-0.395,-0.393,-0.389,-0.384,-0.377,-0.367,-0.355,-0.338,-0.316,-0.29,-0.259,-0.223,-0.184,-0.143,-0.102,-0.064,-0.029,0.001,0.026,0.046,0.061,0.073,0.082,0.088,0.092,0.095,0.097,0.098,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.104,0.106,0.11,0.116,0.126,0.139,0.158,0.183,0.215,0.254,0.299,0.347,0.396,0.441,0.481,0.514,0.539,0.559,0.572,0.582,0.589,0.593,0.596,0.598,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.59,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.561,-0.41,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.398,-0.397,-0.395,-0.393,-0.389,-0.384,-0.377,-0.367,-0.354,-0.337,-0.316,-0.29,-0.258,-0.223,-0.184,-0.143,-0.102,-0.064,-0.029,0.001,0.026,0.046,0.061,0.073,0.081,0.088,0.092,0.095,0.097,0.098,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.104,0.106,0.11,0.117,0.126,0.139,0.158,0.183,0.215,0.254,0.299,0.347,0.396,0.441,0.481,0.513,0.539,0.558,0.572,0.582,0.589,0.593,0.596,0.598,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.59,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.561,-0.41,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.398,-0.397,-0.395,-0.393,-0.389,-0.384,-0.376,-0.367,-0.354,-0.337,-0.315,-0.289,-0.258,-0.222,-0.183,-0.143,-0.102,-0.064,-0.029,0,0.025,0.045,0.061,0.072,0.081,0.087,0.092,0.095,0.097,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.104,0.107,0.111,0.117,0.126,0.14,0.159,0.184,0.216,0.255,0.299,0.347,0.395,0.44,0.48,0.512,0.538,0.557,0.572,0.581,0.588,0.593,0.596,0.597,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.59,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.41,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.398,-0.397,-0.395,-0.392,-0.388,-0.383,-0.376,-0.366,-0.353,-0.336,-0.314,-0.288,-0.257,-0.222,-0.183,-0.143,-0.102,-0.064,-0.03,0,0.024,0.044,0.06,0.072,0.081,0.087,0.091,0.095,0.097,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.104,0.107,0.111,0.118,0.128,0.141,0.16,0.185,0.217,0.256,0.3,0.347,0.395,0.439,0.478,0.511,0.537,0.556,0.57,0.58,0.587,0.592,0.595,0.597,0.598,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.59,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.41,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.399,-0.398,-0.396,-0.394,-0.392,-0.388,-0.382,-0.375,-0.365,-0.351,-0.334,-0.313,-0.287,-0.256,-0.221,-0.182,-0.142,-0.103,-0.065,-0.031,-0.002,0.023,0.043,0.059,0.071,0.08,0.086,0.091,0.094,0.096,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.103,0.105,0.108,0.112,0.119,0.129,0.143,0.162,0.187,0.219,0.257,0.3,0.347,0.394,0.438,0.476,0.509,0.535,0.554,0.569,0.579,0.586,0.591,0.594,0.597,0.598,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.589,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.41,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.398,-0.397,-0.396,-0.394,-0.391,-0.387,-0.381,-0.373,-0.363,-0.35,-0.332,-0.311,-0.285,-0.254,-0.219,-0.181,-0.142,-0.103,-0.066,-0.032,-0.003,0.022,0.042,0.057,0.07,0.079,0.085,0.09,0.094,0.096,0.097,0.098,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.103,0.106,0.109,0.114,0.121,0.131,0.145,0.164,0.189,0.221,0.258,0.301,0.347,0.393,0.436,0.474,0.506,0.532,0.552,0.567,0.577,0.585,0.59,0.594,0.596,0.597,0.598,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.589,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.41,-0.398,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.399,-0.398,-0.397,-0.395,-0.393,-0.39,-0.385,-0.379,-0.372,-0.361,-0.347,-0.33,-0.309,-0.283,-0.252,-0.218,-0.181,-0.142,-0.104,-0.067,-0.034,-0.005,0.02,0.04,0.056,0.068,0.077,0.084,0.089,0.093,0.095,0.097,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.102,0.103,0.104,0.107,0.11,0.115,0.123,0.133,0.148,0.167,0.192,0.223,0.26,0.302,0.347,0.391,0.434,0.471,0.503,0.529,0.549,0.564,0.575,0.583,0.589,0.592,0.595,0.597,0.598,0.599,0.599,0.599,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.599,0.599,0.589,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.41,-0.397,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.399,-0.399,-0.398,-0.397,-0.396,-0.394,-0.392,-0.388,-0.384,-0.378,-0.369,-0.359,-0.345,-0.327,-0.306,-0.28,-0.25,-0.216,-0.18,-0.142,-0.104,-0.068,-0.035,-0.007,0.018,0.038,0.054,0.066,0.076,0.083,0.088,0.092,0.095,0.097,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.101,0.102,0.103,0.105,0.108,0.112,0.117,0.125,0.136,0.151,0.17,0.195,0.226,0.262,0.303,0.347,0.39,0.431,0.468,0.5,0.525,0.546,0.561,0.573,0.581,0.587,0.591,0.594,0.596,0.597,0.598,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.598,0.589,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.56,-0.409,-0.397,-0.398,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.4,-0.4,-0.4,-0.4,-0.399,-0.399,-0.399,-0.399,-0.399,-0.398,-0.397,-0.397,-0.395,-0.393,-0.391,-0.387,-0.382,-0.376,-0.367,-0.356,-0.342,-0.324,-0.303,-0.277,-0.247,-0.214,-0.178,-0.141,-0.104,-0.069,-0.037,-0.009,0.015,0.035,0.052,0.064,0.074,0.082,0.087,0.091,0.094,0.096,0.097,0.098,0.099,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.101,0.101,0.102,0.103,0.104,0.106,0.109,0.113,0.119,0.128,0.139,0.154,0.174,0.199,0.229,0.265,0.304,0.346,0.389,0.429,0.465,0.496,0.521,0.542,0.558,0.57,0.578,0.585,0.589,0.592,0.595,0.596,0.597,0.598,0.598,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.599,0.598,0.589,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.559,-0.409,-0.397,-0.397,-0.398,-0.398,-0.398,-0.398,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.399,-0.398,-0.398,-0.397,-0.396,-0.394,-0.392,-0.389,-0.385,-0.38,-0.373,-0.364,-0.353,-0.339,-0.321,-0.299,-0.274,-0.245,-0.212,-0.177,-0.141,-0.105,-0.071,-0.039,-0.011,0.013,0.033,0.049,0.062,0.072,0.08,0.086,0.09,0.093,0.095,0.097,0.098,0.099,0.099,0.1,0.1,0.1,0.1,0.1,0.101,0.101,0.101,0.101,0.102,0.103,0.104,0.105,0.108,0.111,0.116,0.122,0.131,0.142,0.158,0.178,0.202,0.232,0.267,0.306,0.346,0.387,0.426,0.461,0.492,0.517,0.538,0.554,0.566,0.575,0.582,0.587,0.591,0.593,0.595,0.596,0.597,0.598,0.598,0.598,0.599,0.599,0.599,0.599,0.599,0.599,0.598,0.588,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.037,-0.572,-0.421,-0.409,-0.409,-0.394,-0.395,-0.395,-0.395,-0.395,-0.395,-0.396,-0.396,-0.396,-0.396,-0.396,-0.396,-0.396,-0.396,-0.396,-0.395,-0.395,-0.394,-0.394,-0.393,-0.392,-0.39,-0.388,-0.384,-0.38,-0.375,-0.368,-0.359,-0.347,-0.333,-0.315,-0.294,-0.269,-0.241,-0.21,-0.176,-0.141,-0.107,-0.074,-0.044,-0.017,0.007,0.027,0.043,0.056,0.066,0.074,0.08,0.084,0.087,0.09,0.092,0.093,0.094,0.095,0.095,0.095,0.096,0.096,0.096,0.096,0.096,0.097,0.097,0.098,0.099,0.1,0.102,0.105,0.108,0.113,0.12,0.129,0.141,0.156,0.176,0.2,0.229,0.262,0.299,0.337,0.376,0.413,0.447,0.476,0.501,0.522,0.538,0.55,0.56,0.567,0.572,0.576,0.579,0.581,0.583,0.584,0.584,0.585,0.585,0.586,0.597,0.597,0.597,0.597,0.597,0.596,0.587,0.56,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.187,-1.062,-0.71,-0.584,-0.572,-0.557,-0.369,-0.354,-0.354,-0.355,-0.355,-0.355,-0.355,-0.355,-0.355,-0.355,-0.355,-0.355,-0.355,-0.355,-0.355,-0.355,-0.354,-0.354,-0.353,-0.352,-0.351,-0.35,-0.348,-0.345,-0.342,-0.337,-0.331,-0.324,-0.315,-0.303,-0.289,-0.272,-0.253,-0.231,-0.206,-0.18,-0.153,-0.126,-0.101,-0.077,-0.056,-0.038,-0.022,-0.009,0.001,0.009,0.016,0.021,0.025,0.027,0.03,0.031,0.032,0.033,0.034,0.034,0.034,0.035,0.035,0.035,0.035,0.036,0.036,0.036,0.037,0.038,0.039,0.041,0.043,0.046,0.05,0.056,0.063,0.073,0.085,0.101,0.12,0.143,0.168,0.197,0.226,0.256,0.285,0.311,0.334,0.354,0.37,0.383,0.393,0.401,0.407,0.411,0.415,0.417,0.419,0.42,0.421,0.422,0.422,0.423,0.435,0.576,0.587,0.587,0.587,0.587,0.586,0.579,0.558,0.551,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,-0.2,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55,0.55],"zones":[{"id":"garten","label":"Garten Kellenberger","polygon":[[-20.9,-6.6],[3,-6.6],[3,5.95],[-20.9,5.95]]},{"id":"zufahrt","label":"Feuerwehrzufahrt / Schotterwiese West","polygon":[[-26,-9],[-20.9,-9],[-20.9,6.5],[-26,6.5]]},{"id":"zollet","label":"Nachbargarten Zollet","polygon":[[-24,-14.5],[3,-14.5],[3,-6.6],[-24,-6.6]]},{"id":"sued","label":"Nachbargrundstück Süd","polygon":[[-22,5.95],[6,5.95],[6,12],[-22,12]]},{"id":"ost","label":"Wiese Ost","polygon":[[3,-6.6],[9,-6.6],[9,5.95],[3,5.95]]}],"spots":[{"x":-14.174,"z":-7.683,"y":-1.77,"zone":"zollet"},{"x":-22.166,"z":-7.211,"y":-2.08,"zone":"zufahrt"},{"x":-22.139,"z":5.248,"y":-0.14,"zone":"zufahrt"},{"x":2.948,"z":-7.035,"y":-1.57,"zone":"zollet"},{"x":3.073,"z":-3.825,"y":-0.37,"zone":"ost"},{"x":2.586,"z":-1.113,"y":-0.03,"zone":"garten"},{"x":2.561,"z":0.323,"y":-0.03,"zone":"garten"},{"x":1.7,"z":0.131,"y":-0.03,"zone":"garten"},{"x":2.56,"z":4.31,"y":0.63,"zone":"garten"},{"x":-1.638,"z":3.488,"y":-0.04,"zone":"garten"},{"x":-14.035,"z":3.548,"y":-0.04,"zone":"garten"},{"x":-22.121,"z":1.996,"y":-0.28,"zone":"zufahrt"},{"x":-22.457,"z":0.119,"y":-1.08,"zone":"zufahrt"},{"x":-14.741,"z":-4.532,"y":-0.07,"zone":"garten"},{"x":-14.438,"z":1.353,"y":-0.02,"zone":"garten"},{"x":-14.874,"z":-0.78,"y":-0.02,"zone":"garten"},{"x":-15.545,"z":-1.39,"y":-0.17,"zone":"garten"},{"x":-15.772,"z":-0.831,"y":-0.33,"zone":"garten"},{"x":-17.034,"z":0.729,"y":-0.02,"zone":"garten"},{"x":-22.139,"z":4.442,"y":-0.14,"zone":"zufahrt"},{"x":-14.343,"z":-6.206,"y":-0.28,"zone":"garten"},{"x":-21.559,"z":0.171,"y":-0.28,"zone":"zufahrt"},{"x":-19.494,"z":0.663,"y":-0.3,"zone":"garten"},{"x":-0.92,"z":3.682,"y":0.45,"zone":"garten"},{"x":-3.348,"z":3.191,"y":0.45,"zone":"garten"},{"x":-19.486,"z":-6.3,"y":-0.28,"zone":"garten"},{"x":-21.957,"z":2.65,"y":-0.14,"zone":"zufahrt"},{"x":-9.003,"z":3.661,"y":0.26,"zone":"garten"},{"x":-18.351,"z":3.669,"y":0.14,"zone":"garten"},{"x":-18.918,"z":3.639,"y":-0.04,"zone":"garten"},{"x":-21.417,"z":3.35,"y":-0.13,"zone":"zufahrt"},{"x":-21.041,"z":2.228,"y":-0.32,"zone":"zufahrt"},{"x":-9.137,"z":4.635,"y":0.32,"zone":"garten"},{"x":-14.444,"z":3.887,"y":0.14,"zone":"garten"},{"x":-18.28,"z":4.674,"y":-0.02,"zone":"garten"},{"x":-20.391,"z":3.754,"y":-0.04,"zone":"garten"},{"x":-15.135,"z":4.191,"y":0.09,"zone":"garten"},{"x":-0.282,"z":4.718,"y":0.63,"zone":"garten"},{"x":-14.378,"z":-7.312,"y":-1.35,"zone":"zollet"},{"x":-13.628,"z":4.014,"y":0.26,"zone":"garten"},{"x":-0.455,"z":1.522,"y":0,"zone":"garten"},{"x":1.896,"z":-6.003,"y":-0.72,"zone":"garten"},{"x":4.148,"z":-5.045,"y":-0.72,"zone":"ost"},{"x":1.703,"z":-2.727,"y":-0.02,"zone":"garten"},{"x":0.629,"z":-6.701,"y":-0.72,"zone":"zollet"},{"x":-24.5,"z":-8,"y":-2.6,"zone":"zufahrt"},{"x":-24.5,"z":0,"y":-1.6,"zone":"zufahrt"},{"x":-24.5,"z":5,"y":-0.6,"zone":"zufahrt"},{"x":-22.6,"z":5.5,"y":-0.3,"zone":"zufahrt"},{"x":-20,"z":-12.5,"y":-1.75,"zone":"zollet"},{"x":-8,"z":-12.5,"y":-1.7,"zone":"zollet"},{"x":2,"z":-11.5,"y":-1.65,"zone":"zollet"},{"x":-18,"z":9,"y":-0.4,"zone":"sued"},{"x":-6,"z":9,"y":0.1,"zone":"sued"},{"x":4,"z":9,"y":0.6,"zone":"sued"},{"x":7.5,"z":-5,"y":0.1,"zone":"ost"},{"x":7.5,"z":0,"y":0.45,"zone":"ost"},{"x":7.5,"z":5,"y":0.8,"zone":"ost"}]},"surfaces":[{"type":"tegula","area":53.9,"polygon":[[-25.6,-9],[-22.1,-9],[-22.1,6.4],[-25.6,6.4]],"note":"Feuerwehrzufahrt und Zugangsweg, Tegula-Pflastersteine grau"},{"type":"travertin80","area":33.41,"polygon":[[-16.241,-0.244],[-16.241,0.556],[-18.641,0.555],[-18.642,3.755],[-20.655,3.754],[-20.654,2.154],[-19.441,2.155],[-19.439,-5.845],[-13.839,-5.844],[-13.839,-4.244],[-14.639,-4.244],[-14.64,-3.085],[-15.276,-3.085],[-15.277,-0.244]]},{"type":"travertin4060","area":24.25,"polygon":[[-13.932,0.556],[-13.932,0.506],[-13.821,0.506],[-13.821,0.956],[0.276,0.961],[0.275,0.524],[2.123,0.511],[2.124,1.801],[-4.574,1.799],[-4.575,3.949],[-8.715,3.948],[-8.714,1.798],[-14.725,1.796],[-14.725,0.956],[-14.745,0.956],[-14.745,0.556]]},{"type":"rasen","area":21.35,"polygon":[[-18.242,3.745],[-18.241,0.955],[-14.745,0.956],[-14.745,1.816],[-8.734,1.818],[-8.735,3.748]]},{"type":"keramik","area":20.38,"polygon":[[-13.93,-3.06],[-8.73,-3.06],[-8.73,0.86],[-13.93,0.86]],"note":"Gedeckter Sitzplatz 14.4 m2, Keramikplatten 60x60x2 cm"},{"type":"travertin4060","area":18.35,"polygon":[[-18.642,3.755],[-18.642,3.945],[-20.655,3.944],[-20.655,4.404],[-21.652,4.404],[-21.649,0.611],[-20.618,0.596],[-20.078,-6.622],[-20.078,-6.322],[-13.819,-6.32],[-13.703,-5.844],[-18.639,-5.845],[-18.639,-5.045],[-19.439,-5.045],[-19.441,2.155]]},{"type":"schotterwiese","area":14.14,"polygon":[[-21.74,0.521],[-21.737,-10.773],[-21.737,-10.773],[-20.012,-8.667],[-20.088,-7.67],[-20.287,-7.685],[-20.302,-7.486],[-20.462,-7.498],[-20.523,-6.7],[-20.164,-6.673],[-20.71,0.507]]},{"type":"travertin4060","area":13.9,"polygon":[[-20.013,-7.465],[-18.77,-7.472],[-18.768,-9.575],[-14.568,-9.573],[-14.567,-11.961],[-13.967,-11.973],[-13.968,-7.173],[-18.168,-7.174],[-18.169,-6.872],[-20.059,-6.865]]},{"type":"rasen","area":13.76,"polygon":[[-19.401,-7.468],[-19,-12.703],[-14.567,-11.961],[-14.568,-9.573],[-18.768,-9.575],[-18.77,-7.472]]},{"type":"keramik","area":12.86,"polygon":[[2.115,-4.483],[2.123,0.511],[-0.874,0.51],[-0.873,-2.672],[0.277,-2.672],[0.278,-4.484]]},{"type":"spielsand","area":6.89,"polygon":[[-0.944,1.82],[-0.945,1.9],[-0.845,1.9],[-0.844,1.8],[-0.144,1.801],[-0.145,3.951],[-3.375,3.95],[-3.374,1.82]]},{"type":"travertinstell","area":4.74,"polygon":[[-20.595,4.404],[-20.595,4.004],[-8.735,4.008],[-8.735,4.408]]},{"type":"beet","area":3.51,"polygon":[[0.856,1.861],[2.184,1.861],[2.183,4.501],[0.855,4.501]]},{"type":"verbundstein","area":3.5,"polygon":[[2.518,-4.183],[2.535,-3.343],[2.814,-0.367],[2.861,1.913],[2.183,2.324],[2.183,0.511],[2.123,0.511],[2.119,-1.032],[2.179,-1.032],[2.176,-4.183]]},{"type":"beet","area":2.72,"polygon":[[2.175,-4.543],[0.497,-4.544],[0.494,-5.532],[0.497,-6.278],[1.922,-6.272],[1.919,-5.533],[2.174,-5.533]]},{"type":"travertin80","area":2.63,"polygon":[[-14.74,-2.69],[-13.93,-2.69],[-13.93,0.56],[-14.74,0.56]],"note":"Schwebende Stufe Travertin, obere Tritthoehe -0.17"},{"type":"travertin80","area":2.56,"polygon":[[-15.28,-3.08],[-14.48,-3.08],[-14.48,0.12],[-15.28,0.12]],"note":"Schwebende Stufe Travertin, untere Tritthoehe -0.33"},{"type":"travertin80","area":1.9,"polygon":[[-8.735,3.748],[-8.735,3.948],[-18.242,3.945],[-18.242,3.745]]},{"type":"beet","area":1.89,"polygon":[[-4.554,1.819],[-3.394,1.82],[-3.395,3.45],[-4.555,3.449]]},{"type":"travertin80","area":1.27,"polygon":[[-21.45,2.15],[-20.65,2.15],[-20.66,3.75],[-21.45,3.75]],"note":"Schwebende Stufen beim Gartentor an der Suedwestecke"},{"type":"travertinstell","area":1.25,"polygon":[[-0.145,4.011],[-0.145,4.411],[-3.273,4.41],[-3.273,4.01]]},{"type":"travertin4060","area":1.21,"polygon":[[-13.819,-4.244],[-13.82,-2.694],[-14.477,-2.694],[-14.476,-3.084],[-14.64,-3.085],[-14.639,-4.244]]},{"type":"beet","area":1.07,"polygon":[[-4.515,3.509],[-3.435,3.51],[-3.435,4.01],[-3.273,4.01],[-3.273,4.41],[-4.575,4.409],[-4.575,3.949],[-4.545,3.949]]},{"type":"travertin4060","area":1.01,"polygon":[[-15.277,-0.244],[-15.277,0.115],[-14.745,0.115],[-14.745,0.556],[-16.241,0.556],[-16.241,-0.244]]},{"type":"beet","area":0.98,"polygon":[[2.172,-7.039],[2.173,-6.523],[0.278,-6.521],[0.278,-7.037]]},{"type":"tegula","area":0.69,"polygon":[[-4.575,3.949],[-4.575,4.409],[-6.075,4.409],[-6.075,3.949]]},{"type":"tegula","area":0.69,"polygon":[[-7.235,3.948],[-7.235,4.408],[-8.735,4.408],[-8.735,3.948]]},{"type":"travertinstell","area":0.66,"polygon":[[1.922,-6.272],[0.277,-6.278],[0.278,-6.528],[2.173,-6.523],[2.174,-5.533],[1.924,-5.532]]},{"type":"keramik","area":0.49,"polygon":[[-6.956,2.474],[-6.256,2.474],[-6.256,3.174],[-6.956,3.174]]},{"type":"travertin4060","area":0.47,"polygon":[[-18.169,-6.872],[-18.169,-6.622],[-20.078,-6.622],[-20.059,-6.865]]},{"type":"travertinstell","area":0.46,"polygon":[[-6.075,4.009],[-6.075,4.409],[-7.235,4.408],[-7.235,4.008]]}],"plants":[{"type":"eibe","x":-15.527,"z":-6.098,"r":0.2},{"type":"eibe","x":-15.928,"z":-6.098,"r":0.2},{"type":"eibe","x":-14.727,"z":-6.099,"r":0.2},{"type":"eibe","x":-15.127,"z":-6.099,"r":0.2},{"type":"eibe","x":-13.923,"z":-6.1,"r":0.2},{"type":"eibe","x":-14.324,"z":-6.1,"r":0.2},{"type":"eibe","x":-6.275,"z":4.209,"r":0.2},{"type":"eibe","x":-7.035,"z":4.208,"r":0.2},{"type":"eibe","x":-6.655,"z":4.209,"r":0.2},{"type":"eibe","x":-8.934,"z":4.208,"r":0.2},{"type":"eibe","x":-9.694,"z":4.208,"r":0.2},{"type":"eibe","x":-9.314,"z":4.208,"r":0.2},{"type":"eibe","x":-11.583,"z":4.207,"r":0.2},{"type":"eibe","x":-12.343,"z":4.207,"r":0.2},{"type":"eibe","x":-11.963,"z":4.207,"r":0.2},{"type":"eibe","x":-14.241,"z":4.209,"r":0.2},{"type":"eibe","x":-15.001,"z":4.208,"r":0.2},{"type":"eibe","x":-14.621,"z":4.208,"r":0.2},{"type":"eibe","x":-2.512,"z":4.21,"r":0.2},{"type":"eibe","x":-3.272,"z":4.21,"r":0.2},{"type":"eibe","x":-2.892,"z":4.21,"r":0.2},{"type":"eibe","x":-2.132,"z":4.21,"r":0.2},{"type":"eibe","x":-20.636,"z":0.895,"r":0.2},{"type":"eibe","x":-21.395,"z":0.894,"r":0.2},{"type":"eibe","x":-21.015,"z":0.894,"r":0.2},{"type":"eibe","x":-20.201,"z":-1.769,"r":0.2},{"type":"eibe","x":-20.268,"z":-0.976,"r":0.2},{"type":"eibe","x":-20.235,"z":-1.371,"r":0.2},{"type":"eibe","x":-20.101,"z":-2.961,"r":0.2},{"type":"eibe","x":-20.168,"z":-2.168,"r":0.2},{"type":"eibe","x":-20.134,"z":-2.563,"r":0.2},{"type":"eibe","x":-19.87,"z":-5.693,"r":0.2},{"type":"eibe","x":-19.937,"z":-4.901,"r":0.2},{"type":"eibe","x":-19.903,"z":-5.295,"r":0.2},{"type":"eibe","x":-19.435,"z":-6.093,"r":0.2},{"type":"eibe","x":-19.836,"z":-6.092,"r":0.2},{"type":"eibe","x":-18.635,"z":-6.094,"r":0.2},{"type":"eibe","x":-17.831,"z":-6.095,"r":0.2},{"type":"eibe","x":-18.232,"z":-6.095,"r":0.2},{"type":"eibe","x":-21.296,"z":4.157,"r":0.2},{"type":"eibe","x":-20.892,"z":4.17,"r":0.2},{"type":"eibe","x":-17.6,"z":4.201,"r":0.2},{"type":"eibe","x":-18.36,"z":4.2,"r":0.2},{"type":"eibe","x":-17.98,"z":4.2,"r":0.2},{"type":"eibe","x":-15.355,"z":4.198,"r":0.2},{"type":"eibe","x":-15.736,"z":4.198,"r":0.2},{"type":"eibe","x":-20.325,"z":4.195,"r":0.2},{"type":"kraut","x":-16.571,"z":-5.965,"r":0.11},{"type":"kraut","x":-17.268,"z":-5.96,"r":0.11},{"type":"kraut","x":-19.867,"z":-4.231,"r":0.17},{"type":"kraut","x":-20.221,"z":0.343,"r":0.17},{"type":"kraut","x":-19.2,"z":4.113,"r":0.1},{"type":"kraut","x":-19.857,"z":4.15,"r":0.12},{"type":"kraut","x":-11.021,"z":4.152,"r":0.13},{"type":"kraut","x":-16.336,"z":4.143,"r":0.13},{"type":"kraut","x":-12.957,"z":4.146,"r":0.13},{"type":"kraut","x":-14.97,"z":0.342,"r":0.22},{"type":"kraut","x":-3.526,"z":1.962,"r":0.15},{"type":"kraut","x":-3.961,"z":1.996,"r":0.16},{"type":"kraut","x":-3.639,"z":2.382,"r":0.09},{"type":"kraut","x":-4.159,"z":2.454,"r":0.24},{"type":"kraut","x":-3.619,"z":2.784,"r":0.16},{"type":"kraut","x":-4.388,"z":1.95,"r":0.09},{"type":"kraut","x":-4.299,"z":3.025,"r":0.22},{"type":"kraut","x":-3.852,"z":3.072,"r":0.09},{"type":"solitaer","x":1.521,"z":2.316,"r":0.35},{"type":"strauch","x":1.691,"z":3.207,"r":0.37},{"type":"solitaer","x":0.881,"z":-5.85,"r":0.35},{"type":"strauch","x":1.125,"z":-4.918,"r":0.37},{"type":"solitaer","x":1.578,"z":-5.509,"r":0.35},{"type":"solitaer","x":1.842,"z":-6.789,"r":0.21},{"type":"kraut","x":0.69,"z":-6.797,"r":0.21},{"type":"eibe","x":1.325,"z":-6.793,"r":0.21},{"type":"hecke","kind":"liguster","height":1.7,"width":0.75,"polygon":[[-20.013,-7.465],[-19.401,-7.468],[-19,-12.703],[-19.587,-13.027]]}],"trees":[{"name":"Solitärgehölz Sitzplatz Süd","x":-17.5,"z":1.6,"crown":1.34,"height":4.6,"species":"parrotia"},{"name":"Solitärgehölz Terrasse NW","x":-19.1,"z":-5.5,"crown":0.67,"height":3.2,"species":"parrotia"},{"name":"Solitärgehölz Flammengrill","x":-4,"z":3.9,"crown":0.63,"height":3,"species":"parrotia"},{"name":"Apfelbaum","x":1.69,"z":4.05,"crown":0.9,"height":3.4,"species":"apfel"},{"name":"Solitärgehölz Zollet","x":-17.6,"z":-10.2,"crown":1.34,"height":4.6,"species":"parrotia","neighbour":true}],"lamps":[{"x":4.073,"z":-6.866,"kind":"poller"},{"x":4.079,"z":-0.185,"kind":"poller"},{"x":4.084,"z":4.632,"kind":"poller"},{"x":-3.045,"z":4.6,"kind":"poller"},{"x":-12.371,"z":4.597,"kind":"poller"},{"x":-20.195,"z":-7.578,"kind":"poller"},{"x":-16.865,"z":-6.051,"kind":"boden"},{"x":-18.897,"z":-5.263,"kind":"boden"},{"x":-17.562,"z":1.356,"kind":"boden"},{"x":-20.311,"z":-0.029,"kind":"boden"},{"x":-19.937,"z":-3.924,"kind":"boden"},{"x":-16.682,"z":4.117,"kind":"boden"},{"x":-1.249,"z":4.105,"kind":"boden"},{"x":-19.444,"z":4.101,"kind":"boden"},{"x":-19.137,"z":-6.771,"kind":"boden"},{"x":-16.03,"z":-6.72,"kind":"boden"},{"x":-17.463,"z":-10.093,"kind":"boden"}],"walls":[{"id":"travertinmauer-nord","type":"travertin","note":"Travertinmauer an der Grenze zum Nachbargrundstueck Zollet, stuetzt rund 1.5 m ab","path":[[-18.17,-6.895],[-13.82,-6.895]],"t":0.55,"top":-0.3,"base":-1.9},{"id":"sitzmauer-nord","type":"travertin","note":"Sitzmauer 25 cm ueber der Travertinmauer (gelbe Kote \"25\" im Plan)","path":[[-20.08,-6.47],[-13.82,-6.47]],"t":0.3,"top":-0.03,"base":-0.45},{"id":"winkelelemente-west","type":"beton","note":"Winkelelemente aus Beton entlang der Westgrenze, stuetzen den Garten ueber der Feuerwehrzufahrt","path":[[-20.12,-6.62],[-20.66,0.51],[-21.7,0.52],[-21.7,2.37]],"t":0.22,"top":null,"base":null,"retains":"west","crestProfile":[-0.607,-0.151,-0.091,-0.082,-0.084,-0.089,-0.094,-0.099,-0.104,-0.11,-0.115,-0.12,-0.125,-0.126,-0.128,-0.13,-0.131,-0.133,-0.139,-0.157,-0.16,-0.139,-0.139,-0.135,-0.119],"footProfile":[-2.232,-2.163,-2.189,-2.191,-2.141,-1.982,-1.747,-1.485,-1.261,-1.1,-0.997,-0.932,-0.883,-0.836,-0.786,-0.755,-0.763,-0.745,-0.622,-0.712,-1.229,-1.228,-1.228,-1.228,-1.228]},{"id":"travertinsitzmauer-west","type":"travertin","note":"Travertinsitzmauer als Abschluss des Hauptsitzplatzes gegen den Rasen","path":[[-18.64,0.755],[-14.73,0.755]],"t":0.41,"top":0.4,"base":-0.45,"seating":true},{"id":"travertinsitzmauer-sued","type":"travertin","note":"Schenkel der Travertinsitzmauer laengs des Rasens","path":[[-18.44,0.55],[-18.44,3.94]],"t":0.4,"top":0.4,"base":-0.4,"seating":true},{"id":"betonmauer-nachbar","type":"beton","note":"Bestehende Betonmauer des Suednachbarn entlang der Grenze","path":[[-17.41,6.07],[1.99,6.07]],"t":0.2,"top":1.1,"base":-0.6},{"id":"betonmauer-nachbar-ost","type":"beton","note":"Rueckspringender Schenkel der Nachbarmauer","path":[[2.09,6.07],[2.09,9.05]],"t":0.2,"top":1.1,"base":-0.6},{"id":"holzscheiterwand","type":"holzscheiter","note":"Holzscheiterwand 150 x 200 cm hinter dem Flammengrill","path":[[-5.75,4.35],[-4.25,4.35]],"t":0.45,"top":1.75,"base":0.26},{"id":"lamellenwand","type":"lamellen","note":"Lamellenwand zum Sitzen: senkrechte Holzlamellen am gedeckten Sitzplatz","path":[[-13.26,0.91],[-8.73,0.91]],"t":0.34,"top":2.72,"base":-0.05,"openSlats":true}],"props":[{"id":"gartenhaus","type":"gartenhaus","x":1.2,"z":-3.6,"w":1.8,"d":1.8,"h":2.15,"rot":0,"note":"Standort Gartenhaus 1,8 x 1,8 m"},{"id":"spielhaus","type":"spielhaus","x":0,"z":3.15,"w":1.8,"d":2.7,"h":2.3,"rot":0,"note":"Spielhaus durch Zimmermann, Podest auf 1.47"},{"id":"flammengrill","type":"flammengrill","x":-6.95,"z":2.45,"w":0.7,"d":0.7,"h":0.85,"rot":0,"note":"Flammengrill im Dekorkiesfeld"},{"id":"baenkli","type":"bank","x":-20.62,"z":1.42,"w":0.5,"d":1.5,"h":0.45,"rot":90,"note":"Baenkli aus Travertin"},{"id":"maehroboter","type":"roboter","x":-17.87,"z":3.72,"w":0.75,"d":0.55,"h":0.4,"rot":0,"note":"Garage Maehroboter"},{"id":"wasserhahn","type":"wasserhahn","x":-13.9,"z":-4.6,"w":0.12,"d":0.12,"h":0.85,"rot":0},{"id":"schlaufschacht","type":"schacht","x":-14.35,"z":-3.95,"w":0.3,"d":0.3,"h":0.02,"rot":0,"note":"Schlaufschacht D = 30 cm"},{"id":"gartentor","type":"tor","x":-21.6,"z":3.3,"w":1,"d":0.06,"h":1.1,"rot":0}],"stelen":[{"x":-19.55,"z":-3.1,"h":0.9},{"x":-19.3,"z":-2.3,"h":1.15},{"x":-19.05,"z":-1.55,"h":0.75},{"x":-13.3,"z":4.45,"h":1.05},{"x":-12.6,"z":4.45,"h":0.8},{"x":-10.3,"z":4.6,"h":1.1},{"x":-9.6,"z":4.6,"h":0.85},{"x":-1.3,"z":4.55,"h":1},{"x":-0.6,"z":4.55,"h":0.75}],"building":{"id":"haus-9a","name":"Haus 9a - Jeanine und Damian Kellenberger","floorLevel":0,"upperLevel":2.9,"roofLevel":6.15,"parapet":0.35,"outline":[[-13.82,-6.87],[0.28,-6.87],[0.28,0.96],[-13.82,0.96]],"loggia":{"x0":-13.82,"z0":-3.06,"x1":-9.06,"z1":0.96,"ceiling":2.75},"canopy":{"a":-3.2,"b":0.28,"depth":1.35,"y":2.6,"thickness":0.16},"openings":[{"face":"south","a":-4.4,"b":-0.2,"sill":0,"head":2.45,"kind":"glass"},{"face":"south","a":-12.9,"b":-9.4,"sill":0,"head":2.45,"kind":"glass"},{"face":"south","a":-3.9,"b":-0.6,"sill":3.35,"head":5,"kind":"glass","upper":true},{"face":"south","a":-12.6,"b":-9.7,"sill":3.35,"head":5,"kind":"glass","upper":true},{"face":"west","a":-5.84,"b":-4.24,"sill":0.85,"head":2.35,"kind":"window"},{"face":"west","a":-5.6,"b":-4.5,"sill":3.75,"head":5,"kind":"window","upper":true},{"face":"north","a":-1.9,"b":-0.9,"sill":0,"head":2.35,"kind":"door"},{"face":"north","a":-5.6,"b":-3.9,"sill":0.9,"head":2.35,"kind":"window"},{"face":"north","a":-11.6,"b":-10.2,"sill":0.9,"head":2.35,"kind":"slats"},{"face":"north","a":-5.4,"b":-4.1,"sill":3.75,"head":5.1,"kind":"window","upper":true},{"face":"north","a":-11.4,"b":-10.4,"sill":3.75,"head":5.1,"kind":"slats","upper":true},{"face":"east","a":-5.6,"b":-4.3,"sill":0.9,"head":2.35,"kind":"window"}]},"neighbourBuildings":[{"id":"haus-9","name":"Haus 9 - Sharon und Alan Zollet","outline":[[-15.6,-13.6],[-1.2,-13.6],[-1.2,-6.87],[-15.6,-6.87]],"floorLevel":-1.7,"roofLevel":4.45}],"startViews":[{"id":"sitzplatz","label":"Hauptsitzplatz","x":-18.6,"z":-2.6,"yaw":135,"note":"Travertinplatten 80x80x3 cm, Blick über den Sitzplatz gegen den Rasen"},{"id":"gedeckt","label":"Gedeckter Sitzplatz","x":-11,"z":-1.2,"yaw":215,"note":"Keramikplatten unter dem Obergeschoss, hinter der Lamellenwand"},{"id":"rasen","label":"Rasen Süd","x":-13,"z":2.8,"yaw":75,"note":"Blick der Südfassade entlang zum Spielbereich"},{"id":"grill","label":"Flammengrill","x":-7.8,"z":2.6,"yaw":110,"note":"Dekorkiesfeld mit Flammengrill und Holzscheiterwand"},{"id":"spielplatz","label":"Spielbereich","x":-3.4,"z":1.4,"yaw":150,"note":"Spielsand, Spielhaus und Apfelbaum"},{"id":"eingang","label":"Hauseingang","x":1.3,"z":-1.8,"yaw":250,"note":"Keramikplatten beim Fixpunkt 0.00"}]} \ No newline at end of file diff --git a/src/controls/walker.ts b/src/controls/walker.ts new file mode 100644 index 0000000..e54e503 --- /dev/null +++ b/src/controls/walker.ts @@ -0,0 +1,225 @@ +/** + * Begehen des Gartens: Maus/Tastatur am Desktop, Joystick und Wischen am Handy. + * Der Spieler folgt dem Gelaende, kann Stufen bis 45 cm ersteigen und wird + * von Mauern, Gebaeuden und Objekten aufgehalten. + */ +import { Euler, PerspectiveCamera, Vector3 } from 'three'; + +/** Achsenausgerichtetes oder gedrehtes Hindernis in der Draufsicht. */ +export interface Obstacle { + cx: number; + cz: number; + hw: number; + hd: number; + /** Drehung um Y in Radiant. */ + rot: number; + /** Hindernisse unterhalb dieser Hoehe kann man uebersteigen. */ + climbable?: boolean; +} + +export interface WalkerOptions { + height: (x: number, z: number) => number; + obstacles: Obstacle[]; + bounds: { x0: number; z0: number; x1: number; z1: number }; + eyeHeight?: number; + radius?: number; + maxStep?: number; +} + +const SPEED_WALK = 1.65; +const SPEED_RUN = 3.6; + +export class Walker { + readonly camera: PerspectiveCamera; + private readonly opts: Required; + private readonly euler = new Euler(0, 0, 0, 'YXZ'); + private readonly keys = new Set(); + private readonly pos = new Vector3(); + private velocityY = 0; + private grounded = true; + private joystick = { x: 0, y: 0, active: false }; + private lookDelta = { x: 0, y: 0 }; + private pointerLocked = false; + private enabled = true; + + /** Wird gesetzt, wenn der Nutzer die Maus gefangen hat oder wieder frei ist. */ + onPointerLockChange?: (locked: boolean) => void; + + constructor(camera: PerspectiveCamera, options: WalkerOptions) { + this.camera = camera; + this.opts = { + eyeHeight: 1.68, + radius: 0.32, + maxStep: 0.45, + ...options, + } as Required; + } + + get position(): Vector3 { return this.pos; } + get yawDegrees(): number { return (this.euler.y * 180) / Math.PI; } + + setEnabled(on: boolean) { this.enabled = on; } + + teleport(x: number, z: number, yawDeg: number) { + this.pos.set(x, this.opts.height(x, z) + this.opts.eyeHeight, z); + this.euler.set(0, (-yawDeg * Math.PI) / 180, 0); + this.velocityY = 0; + this.applyCamera(); + } + + // -- Eingaben ------------------------------------------------------------- + + attach(dom: HTMLElement) { + window.addEventListener('keydown', (e) => { + if (e.code === 'Tab') return; + this.keys.add(e.code); + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space'].includes(e.code)) e.preventDefault(); + }); + window.addEventListener('keyup', (e) => this.keys.delete(e.code)); + window.addEventListener('blur', () => this.keys.clear()); + + dom.addEventListener('click', () => { + if (!this.enabled || this.pointerLocked || isTouch()) return; + // In eingebetteten Vorschauen ist Pointer Lock gesperrt - kein Grund abzustuerzen. + Promise.resolve(dom.requestPointerLock()).catch(() => {}); + }); + document.addEventListener('pointerlockchange', () => { + this.pointerLocked = document.pointerLockElement === dom; + this.onPointerLockChange?.(this.pointerLocked); + }); + document.addEventListener('mousemove', (e) => { + if (!this.pointerLocked || !this.enabled) return; + this.lookDelta.x -= e.movementX * 0.0022; + this.lookDelta.y -= e.movementY * 0.0022; + }); + + // Wischen zum Umsehen (rechte Bildschirmhaelfte bleibt fuer den Blick frei) + let touchId: number | null = null; + let last = { x: 0, y: 0 }; + dom.addEventListener('touchstart', (e) => { + for (const t of Array.from(e.changedTouches)) { + if (touchId === null && t.clientX > window.innerWidth * 0.35) { + touchId = t.identifier; + last = { x: t.clientX, y: t.clientY }; + } + } + }, { passive: true }); + dom.addEventListener('touchmove', (e) => { + for (const t of Array.from(e.changedTouches)) { + if (t.identifier !== touchId) continue; + this.lookDelta.x -= (t.clientX - last.x) * 0.005; + this.lookDelta.y -= (t.clientY - last.y) * 0.005; + last = { x: t.clientX, y: t.clientY }; + } + }, { passive: true }); + const end = (e: TouchEvent) => { + for (const t of Array.from(e.changedTouches)) if (t.identifier === touchId) touchId = null; + }; + dom.addEventListener('touchend', end, { passive: true }); + dom.addEventListener('touchcancel', end, { passive: true }); + } + + setJoystick(x: number, y: number, active: boolean) { + this.joystick = { x, y, active }; + } + + // -- Bewegung ------------------------------------------------------------- + + update(dt: number) { + if (!this.enabled) return; + + this.euler.y += this.lookDelta.x; + this.euler.x += this.lookDelta.y; + this.euler.x = Math.max(-1.4, Math.min(1.4, this.euler.x)); + this.lookDelta.x = 0; + this.lookDelta.y = 0; + + let forward = 0, strafe = 0; + if (this.keys.has('KeyW') || this.keys.has('ArrowUp')) forward += 1; + if (this.keys.has('KeyS') || this.keys.has('ArrowDown')) forward -= 1; + if (this.keys.has('KeyD') || this.keys.has('ArrowRight')) strafe += 1; + if (this.keys.has('KeyA') || this.keys.has('ArrowLeft')) strafe -= 1; + if (this.joystick.active) { + forward += -this.joystick.y; + strafe += this.joystick.x; + } + + const len = Math.hypot(forward, strafe); + if (len > 1) { forward /= len; strafe /= len; } + + const running = this.keys.has('ShiftLeft') || this.keys.has('ShiftRight'); + const speed = (running ? SPEED_RUN : SPEED_WALK) * dt; + const sin = Math.sin(this.euler.y), cos = Math.cos(this.euler.y); + const dx = (-sin * forward + cos * strafe) * speed; + const dz = (-cos * forward - sin * strafe) * speed; + + this.move(dx, dz); + + // Vertikal: dem Gelaende folgen, Absaetze weich abfangen + const ground = this.opts.height(this.pos.x, this.pos.z) + this.opts.eyeHeight; + if (this.pos.y > ground + 0.02) { + this.velocityY -= 9.81 * dt; + this.pos.y += this.velocityY * dt; + if (this.pos.y <= ground) { this.pos.y = ground; this.velocityY = 0; this.grounded = true; } + else this.grounded = false; + } else { + // sanftes Nachziehen beim Aufsteigen + this.pos.y += (ground - this.pos.y) * Math.min(1, dt * 12); + this.velocityY = 0; + this.grounded = true; + } + + this.applyCamera(); + } + + /** Bewegt den Spieler und loest Kollisionen achsenweise auf. */ + private move(dx: number, dz: number) { + const tryAxis = (nx: number, nz: number): boolean => { + const y0 = this.opts.height(this.pos.x, this.pos.z); + const y1 = this.opts.height(nx, nz); + if (this.grounded && y1 - y0 > this.opts.maxStep) return false; + if (this.blocked(nx, nz)) return false; + const b = this.opts.bounds; + if (nx < b.x0 || nx > b.x1 || nz < b.z0 || nz > b.z1) return false; + this.pos.x = nx; + this.pos.z = nz; + return true; + }; + if (dx !== 0) tryAxis(this.pos.x + dx, this.pos.z); + if (dz !== 0) tryAxis(this.pos.x, this.pos.z + dz); + } + + private blocked(x: number, z: number): boolean { + const r = this.opts.radius; + for (const o of this.opts.obstacles) { + const dx = x - o.cx, dz = z - o.cz; + const c = Math.cos(-o.rot), s = Math.sin(-o.rot); + const lx = dx * c - dz * s; + const lz = dx * s + dz * c; + if (Math.abs(lx) < o.hw + r && Math.abs(lz) < o.hd + r) return true; + } + return false; + } + + private applyCamera() { + this.camera.position.copy(this.pos); + this.camera.quaternion.setFromEuler(this.euler); + } +} + +export function isTouch(): boolean { + return window.matchMedia('(pointer: coarse)').matches; +} + +/** Baut ein Hindernis aus einer Strecke (Mauer) mit gegebener Dicke. */ +export function obstacleFromSegment(a: [number, number], b: [number, number], thickness: number): Obstacle { + const dx = b[0] - a[0], dz = b[1] - a[1]; + const len = Math.hypot(dx, dz); + return { + cx: (a[0] + b[0]) / 2, + cz: (a[1] + b[1]) / 2, + hw: len / 2, + hd: thickness / 2, + rot: Math.atan2(dz, dx), + }; +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..eadba86 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,241 @@ +/** + * Begehbare 3D-Visualisierung des Gartens Egglihaldenstrasse 9a, Abtwil SG. + * + * Die gesamte Geometrie ist massstaeblich aus dem Gartenplan 1 der + * Garten Design AG (1:100) abgeleitet; das Wohnhaus zusaetzlich aus den + * Projektplaenen von EHLE Architekten. Siehe tools/ fuer die Datenpipeline. + */ +import { + ACESFilmicToneMapping, Group, PCFSoftShadowMap, PerspectiveCamera, Scene, + SRGBColorSpace, Vector3, WebGLRenderer, +} from 'three'; +import './style.css'; +import { loadSite, makeHeightSampler, type SiteData } from './site'; +import { buildTerrain } from './scene/terrain'; +import { buildSurfaces } from './scene/surfaces'; +import { buildWalls, buildStelen, buildProps, buildLamps } from './scene/structures'; +import { buildVegetation } from './scene/vegetation'; +import { buildBuilding, buildNeighbourBuildings } from './scene/building'; +import { buildEnvironment } from './scene/environment'; +import { Walker, isTouch, obstacleFromSegment, type Obstacle } from './controls/walker'; +import { setupHud } from './ui/hud'; + +const CENTER: [number, number] = [-9.5, -1.0]; +const BOUNDS = { x0: -24.0, z0: -12.5, x1: 7.5, z1: 8.5 }; + +async function main() { + const canvas = document.getElementById('view') as HTMLCanvasElement; + const fill = document.getElementById('loader-fill') as HTMLSpanElement; + const note = document.getElementById('loader-note') as HTMLDivElement; + const progress = (pct: number, text: string) => { + fill.style.width = `${pct}%`; + note.textContent = text; + return new Promise((r) => requestAnimationFrame(() => r())); + }; + + const renderer = new WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' }); + renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); + renderer.outputColorSpace = SRGBColorSpace; + renderer.toneMapping = ACESFilmicToneMapping; + renderer.toneMappingExposure = 0.94; + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = PCFSoftShadowMap; + + const scene = new Scene(); + const camera = new PerspectiveCamera(62, 1, 0.08, 1400); + + await progress(8, 'Gartenplan wird geladen …'); + const site: SiteData = await loadSite(); + const height = makeHeightSampler(site.terrain); + + await progress(22, 'Gelände wird modelliert …'); + scene.add(buildTerrain(site.terrain)); + + await progress(38, 'Beläge werden verlegt …'); + scene.add(buildSurfaces(site.surfaces, height)); + + await progress(52, 'Mauern und Bauteile …'); + scene.add(buildWalls(site.walls, height)); + scene.add(buildStelen(site.stelen, height)); + scene.add(buildProps(site.props, height)); + + await progress(66, 'Haus und Nachbarschaft …'); + scene.add(buildBuilding(site.building)); + scene.add(buildNeighbourBuildings(site.neighbourBuildings)); + + await progress(80, 'Bepflanzung wird gesetzt …'); + scene.add(buildVegetation(site.plants, site.trees, height)); + + await progress(90, 'Licht und Himmel …'); + const lamps = buildLamps(site.lamps, height); + scene.add(lamps.group); + const env = buildEnvironment(scene, { + lat: site.meta.location.lat, + lon: site.meta.location.lon, + northBearingOfSiteX: site.meta.location.northBearingOfSiteX, + center: CENTER, + radius: 26, + }); + scene.add(env.group); + + // -- Hindernisse fuer den Rundgang -------------------------------------- + const obstacles: Obstacle[] = []; + const b = site.building; + const [bx0, bz0] = b.outline[0]; + const bx1 = b.outline[1][0]; + const bz1 = b.outline[2][1]; + // Baukoerper ohne die begehbare Loggia: zwei Rechtecke + obstacles.push({ + cx: (bx0 + bx1) / 2, cz: (bz0 + b.loggia.z0) / 2, + hw: (bx1 - bx0) / 2, hd: (b.loggia.z0 - bz0) / 2, rot: 0, + }); + obstacles.push({ + cx: (b.loggia.x1 + bx1) / 2, cz: (b.loggia.z0 + bz1) / 2, + hw: (bx1 - b.loggia.x1) / 2, hd: (bz1 - b.loggia.z0) / 2, rot: 0, + }); + for (const nb of site.neighbourBuildings) { + let x0 = Infinity, z0 = Infinity, x1 = -Infinity, z1 = -Infinity; + for (const [x, z] of nb.outline) { x0 = Math.min(x0, x); x1 = Math.max(x1, x); z0 = Math.min(z0, z); z1 = Math.max(z1, z); } + obstacles.push({ cx: (x0 + x1) / 2, cz: (z0 + z1) / 2, hw: (x1 - x0) / 2, hd: (z1 - z0) / 2, rot: 0 }); + } + for (const w of site.walls) { + for (let i = 1; i < w.path.length; i++) { + obstacles.push(obstacleFromSegment(w.path[i - 1], w.path[i], w.t + 0.1)); + } + } + for (const p of site.props) { + if (p.type === 'schacht' || p.type === 'wasserhahn') continue; + obstacles.push({ cx: p.x, cz: p.z, hw: p.w / 2, hd: p.d / 2, rot: (p.rot * Math.PI) / 180 }); + } + for (const pl of site.plants) { + if (pl.type !== 'hecke' || !pl.polygon) continue; + let x0 = Infinity, z0 = Infinity, x1 = -Infinity, z1 = -Infinity; + for (const [x, z] of pl.polygon) { x0 = Math.min(x0, x); x1 = Math.max(x1, x); z0 = Math.min(z0, z); z1 = Math.max(z1, z); } + obstacles.push({ cx: (x0 + x1) / 2, cz: (z0 + z1) / 2, hw: (x1 - x0) / 2, hd: (z1 - z0) / 2, rot: 0 }); + } + for (const t of site.trees) { + obstacles.push({ cx: t.x, cz: t.z, hw: 0.22, hd: 0.22, rot: 0 }); + } + + const walker = new Walker(camera, { height, obstacles, bounds: BOUNDS }); + walker.attach(canvas); + const start = site.startViews[0]; + walker.teleport(start.x, start.z, start.yaw); + + // -- Uebersichtsmodus ---------------------------------------------------- + const orbit = { + active: false, + yaw: -0.7, + pitch: 0.72, + distance: 42, + target: new Vector3(CENTER[0], 0.5, CENTER[1]), + apply() { + const cp = Math.cos(this.pitch), sp = Math.sin(this.pitch); + camera.position.set( + this.target.x + this.distance * cp * Math.sin(this.yaw), + this.target.y + this.distance * sp, + this.target.z + this.distance * cp * Math.cos(this.yaw), + ); + camera.lookAt(this.target); + }, + }; + + let dragging = false; + let lastPointer = { x: 0, y: 0 }; + canvas.addEventListener('pointerdown', (e) => { + if (!orbit.active) return; + dragging = true; + lastPointer = { x: e.clientX, y: e.clientY }; + canvas.setPointerCapture(e.pointerId); + }); + canvas.addEventListener('pointermove', (e) => { + if (!orbit.active || !dragging) return; + orbit.yaw -= (e.clientX - lastPointer.x) * 0.006; + orbit.pitch = Math.max(0.12, Math.min(1.45, orbit.pitch + (e.clientY - lastPointer.y) * 0.004)); + lastPointer = { x: e.clientX, y: e.clientY }; + orbit.apply(); + }); + canvas.addEventListener('pointerup', () => { dragging = false; }); + canvas.addEventListener('wheel', (e) => { + if (!orbit.active) return; + e.preventDefault(); + orbit.distance = Math.max(14, Math.min(110, orbit.distance * (1 + Math.sign(e.deltaY) * 0.12))); + orbit.apply(); + }, { passive: false }); + + // -- Bedienoberflaeche --------------------------------------------------- + const hud = setupHud({ + site, + onView: (v) => { + if (orbit.active) hud.setMode('walk'); + walker.teleport(v.x, v.z, v.yaw); + }, + onMode: (mode) => { + orbit.active = mode === 'overview'; + walker.setEnabled(!orbit.active); + if (orbit.active) { + if (document.pointerLockElement) document.exitPointerLock(); + orbit.apply(); + } else { + walker.teleport(walker.position.x, walker.position.z, walker.yawDegrees); + } + canvas.style.cursor = orbit.active ? 'grab' : 'default'; + }, + onJoystick: (x, y, active) => walker.setJoystick(x, y, active), + }); + walker.onPointerLockChange = (locked) => hud.setPointerLocked(locked); + + // -- Renderschleife ------------------------------------------------------ + function resize() { + const w = canvas.clientWidth || window.innerWidth; + const h = canvas.clientHeight || window.innerHeight; + renderer.setSize(w, h, false); + camera.aspect = w / h; + camera.updateProjectionMatrix(); + } + window.addEventListener('resize', resize); + resize(); + + let last = performance.now(); + let lastSun = { hours: -1, day: -1 }; + + function frame(now: number) { + const dt = Math.min(0.05, (now - last) / 1000); + last = now; + + const t = hud.time(); + if (t.hours !== lastSun.hours || t.day !== lastSun.day) { + const sun = env.update(t.day, t.hours); + lamps.setOn(env.isDark()); + hud.setSunReadout(sun); + lastSun = t; + } + + if (orbit.active) { + orbit.apply(); + } else { + walker.update(dt); + } + + renderer.render(scene, camera); + requestAnimationFrame(frame); + } + + await progress(100, 'Bereit.'); + document.getElementById('loader')!.classList.add('done'); + document.getElementById('hud')!.hidden = false; + if (isTouch()) document.getElementById('joystick')!.hidden = false; + requestAnimationFrame(frame); + + // Zugriff fuer Entwicklung und automatisierte Bildkontrolle + (window as unknown as { garten: unknown }).garten = { + scene, camera, site, env, walker, orbit, hud, renderer, Group, + setOverview(on: boolean) { hud.setMode(on ? 'overview' : 'walk'); }, + }; +} + +main().catch((err) => { + console.error(err); + const note = document.getElementById('loader-note'); + if (note) note.textContent = `Fehler beim Laden: ${(err as Error).message}`; +}); diff --git a/src/scene/building.ts b/src/scene/building.ts new file mode 100644 index 0000000..cf38666 --- /dev/null +++ b/src/scene/building.ts @@ -0,0 +1,273 @@ +/** + * Wohnhaus 9a und Nachbargebaeude. + * + * Grundriss und Oeffnungen stammen aus dem Gartenplan, die Hoehen aus den + * Fassadenplaenen 1:100 von EHLE Architekten (OK FB EG 651.17 = 0.00, + * OK FB OG +2.90, OK Attika +6.15). Materialisierung nach den Fotos: + * vertikale, offene Holzschalung graubraun auf hellem Betonsockel, + * dunkelbronzene Fensterrahmen, Flachdach mit extensiver Begruenung. + */ +import { + BufferAttribute, BufferGeometry, Group, Mesh, MeshPhysicalMaterial, + MeshStandardMaterial, PlaneGeometry, +} from 'three'; +import type { Building, NeighbourBuilding, Opening, Vec2 } from '../site'; +import { boxGeometry, mergeGeometries } from './geometry'; +import * as tex from './textures'; + +/** Rechteck in Fassadenkoordinaten (u entlang der Wand, y in der Hoehe). */ +interface Rect { u0: number; u1: number; y0: number; y1: number } + +/** + * Zieht Aussparungen von einem Wandfeld ab und liefert die verbleibenden + * Rechtecke. Erst in u-Richtung aufteilen, dann je Streifen in y-Richtung. + */ +function subtract(panel: Rect, voids: Rect[]): Rect[] { + const relevant = voids.filter((v) => v.u1 > panel.u0 && v.u0 < panel.u1 && v.y1 > panel.y0 && v.y0 < panel.y1); + if (!relevant.length) return [panel]; + + const cuts = new Set([panel.u0, panel.u1]); + for (const v of relevant) { + if (v.u0 > panel.u0 && v.u0 < panel.u1) cuts.add(v.u0); + if (v.u1 > panel.u0 && v.u1 < panel.u1) cuts.add(v.u1); + } + const us = [...cuts].sort((a, b) => a - b); + + const out: Rect[] = []; + for (let i = 0; i < us.length - 1; i++) { + const u0 = us[i], u1 = us[i + 1]; + if (u1 - u0 < 1e-4) continue; + const mid = (u0 + u1) / 2; + const bands = relevant + .filter((v) => v.u0 <= mid && v.u1 >= mid) + .map((v) => [Math.max(panel.y0, v.y0), Math.min(panel.y1, v.y1)] as [number, number]) + .sort((a, b) => a[0] - b[0]); + let y = panel.y0; + for (const [b0, b1] of bands) { + if (b0 > y) out.push({ u0, u1, y0: y, y1: b0 }); + y = Math.max(y, b1); + } + if (y < panel.y1) out.push({ u0, u1, y0: y, y1: panel.y1 }); + } + return out.filter((r) => r.u1 - r.u0 > 1e-4 && r.y1 - r.y0 > 1e-4); +} + +/** Erzeugt die Quads einer Fassadenebene aus Rechtecken in (u, y). */ +function faceGeometry( + rects: Rect[], + toWorld: (u: number, y: number) => [number, number, number], + flip: boolean, + tile: number, +): BufferGeometry { + const pos: number[] = []; + const uv: number[] = []; + for (const r of rects) { + const a = toWorld(r.u0, r.y0), b = toWorld(r.u1, r.y0); + const c = toWorld(r.u1, r.y1), d = toWorld(r.u0, r.y1); + const quad = flip ? [a, d, c, a, c, b] : [a, b, c, a, c, d]; + const uvs = flip + ? [[r.u0, r.y0], [r.u0, r.y1], [r.u1, r.y1], [r.u0, r.y0], [r.u1, r.y1], [r.u1, r.y0]] + : [[r.u0, r.y0], [r.u1, r.y0], [r.u1, r.y1], [r.u0, r.y0], [r.u1, r.y1], [r.u0, r.y1]]; + for (const p of quad) pos.push(p[0], p[1], p[2]); + for (const [u, y] of uvs) uv.push(u / tile, y / tile); + } + const g = new BufferGeometry(); + g.setAttribute('position', new BufferAttribute(new Float32Array(pos), 3)); + g.setAttribute('uv', new BufferAttribute(new Float32Array(uv), 2)); + g.computeVertexNormals(); + return g; +} + +function glassMaterial() { + return new MeshPhysicalMaterial({ + color: 0x1d2a2e, roughness: 0.06, metalness: 0.1, + transmission: 0, opacity: 0.72, transparent: true, + reflectivity: 0.85, envMapIntensity: 1.2, side: 2, + }); +} + +const FRAME = () => new MeshStandardMaterial({ color: 0x4a3f34, roughness: 0.45, metalness: 0.35 }); + +export function buildBuilding(b: Building): Group { + const group = new Group(); + group.name = b.name; + + const holz = tex.fassadeHolz(); + const holzMat = new MeshStandardMaterial({ map: holz.map, roughness: holz.roughness, metalness: 0 }); + const sockelTex = tex.sockel(); + const sockelMat = new MeshStandardMaterial({ map: sockelTex.map, roughness: sockelTex.roughness }); + const glasMat = glassMaterial(); + const frameMat = FRAME(); + + const [x0, z0] = b.outline[0]; + const [x1] = b.outline[1]; + const z1 = b.outline[2][1]; + const yTop = b.roofLevel; + const SOCKEL = 0.35; // heller Betonsockel unter der Holzschalung + + /** Definition der vier Fassaden: u laeuft jeweils in Site-X bzw. Site-Z. */ + const faces = { + north: { u0: x0, u1: x1, toWorld: (u: number, y: number) => [u, y, z0] as [number, number, number], flip: true }, + south: { u0: x0, u1: x1, toWorld: (u: number, y: number) => [u, y, z1] as [number, number, number], flip: false }, + west: { u0: z0, u1: z1, toWorld: (u: number, y: number) => [x0, y, u] as [number, number, number], flip: false }, + east: { u0: z0, u1: z1, toWorld: (u: number, y: number) => [x1, y, u] as [number, number, number], flip: true }, + }; + + // Aussparungen je Fassade: Fenster, Tueren und der offene Loggia-Bereich + const voidsFor = (face: Opening['face']): Rect[] => { + const list: Rect[] = b.openings + .filter((o) => o.face === face) + .map((o) => ({ u0: Math.min(o.a, o.b), u1: Math.max(o.a, o.b), y0: o.sill, y1: o.head })); + if (face === 'south') list.push({ u0: b.loggia.x0, u1: b.loggia.x1, y0: b.floorLevel - 0.4, y1: b.loggia.ceiling }); + if (face === 'west') list.push({ u0: b.loggia.z0, u1: b.loggia.z1, y0: b.floorLevel - 0.4, y1: b.loggia.ceiling }); + return list; + }; + + const holzParts: BufferGeometry[] = []; + const sockelParts: BufferGeometry[] = []; + for (const key of ['north', 'south', 'west', 'east'] as const) { + const f = faces[key]; + const voids = voidsFor(key); + const sockelRects = subtract({ u0: f.u0, u1: f.u1, y0: b.floorLevel - 0.55, y1: b.floorLevel + SOCKEL }, voids); + const holzRects = subtract({ u0: f.u0, u1: f.u1, y0: b.floorLevel + SOCKEL, y1: yTop }, voids); + sockelParts.push(faceGeometry(sockelRects, f.toWorld, f.flip, sockelTex.tile)); + holzParts.push(faceGeometry(holzRects, f.toWorld, f.flip, holz.tile)); + } + const wallMesh = new Mesh(mergeGeometries(holzParts), holzMat); + wallMesh.name = 'Fassade'; + wallMesh.castShadow = true; + wallMesh.receiveShadow = true; + group.add(wallMesh); + + const sockelMesh = new Mesh(mergeGeometries(sockelParts), sockelMat); + sockelMesh.name = 'Sockel'; + sockelMesh.castShadow = true; + sockelMesh.receiveShadow = true; + group.add(sockelMesh); + + // --- Loggia: Innenwaende, Decke und Boden ------------------------------- + const lg = b.loggia; + const loggia = new Group(); + loggia.name = 'Gedeckter Sitzplatz'; + const inner = new Mesh( + boxGeometry(lg.x1, (lg.z0 + lg.z1) / 2, 0.16, lg.z1 - lg.z0, b.floorLevel - 0.4, lg.ceiling, 0, holz.tile), + holzMat, + ); + loggia.add(inner); + const back = new Mesh( + boxGeometry((lg.x0 + lg.x1) / 2, lg.z0, lg.x1 - lg.x0, 0.16, b.floorLevel - 0.4, lg.ceiling, 0, holz.tile), + holzMat, + ); + loggia.add(back); + const ceil = new PlaneGeometry(lg.x1 - lg.x0, lg.z1 - lg.z0); + ceil.rotateX(Math.PI / 2); + const ceilMesh = new Mesh(ceil, new MeshStandardMaterial({ color: 0xd8d4cc, roughness: 0.85 })); + ceilMesh.position.set((lg.x0 + lg.x1) / 2, lg.ceiling, (lg.z0 + lg.z1) / 2); + loggia.add(ceilMesh); + loggia.children.forEach((c) => { (c as Mesh).castShadow = true; (c as Mesh).receiveShadow = true; }); + group.add(loggia); + + // --- Verglasungen und Rahmen ------------------------------------------- + const glassGroup = new Group(); + glassGroup.name = 'Verglasungen'; + for (const o of b.openings) { + const f = faces[o.face]; + const u0 = Math.min(o.a, o.b), u1 = Math.max(o.a, o.b); + const inset = 0.10; + const mid = (p: [number, number, number]): [number, number, number] => { + const n: [number, number, number] = [...p]; + if (o.face === 'north') n[2] += inset; + if (o.face === 'south') n[2] -= inset; + if (o.face === 'west') n[0] += inset; + if (o.face === 'east') n[0] -= inset; + return n; + }; + const rect: Rect = { u0, u1, y0: o.sill, y1: o.head }; + const paneMat = o.kind === 'door' + ? new MeshStandardMaterial({ color: 0x6b4a30, roughness: 0.55 }) + : o.kind === 'slats' + ? new MeshStandardMaterial({ color: 0x24211d, roughness: 0.7 }) + : glasMat; + const pane = new Mesh(faceGeometry([rect], (u, y) => mid(f.toWorld(u, y)), f.flip, 1), paneMat); + pane.name = `Öffnung ${o.face} ${u0.toFixed(1)}`; + glassGroup.add(pane); + + // Laibungsrahmen + const border = 0.07; + const frameRects: Rect[] = [ + { u0, u1, y0: o.sill - border, y1: o.sill }, + { u0, u1, y0: o.head, y1: o.head + border }, + { u0: u0 - border, u1: u0, y0: o.sill - border, y1: o.head + border }, + { u0: u1, u1: u1 + border, y0: o.sill - border, y1: o.head + border }, + ]; + const frame = new Mesh(faceGeometry(frameRects, (u, y) => mid(f.toWorld(u, y)), f.flip, 1), frameMat); + glassGroup.add(frame); + } + group.add(glassGroup); + + // --- Flachdach mit Attika und Begruenung -------------------------------- + const roof = new Group(); + roof.name = 'Dach'; + const gruen = tex.gruendach(); + const roofPlane = new PlaneGeometry(x1 - x0 - 0.3, z1 - z0 - 0.3); + roofPlane.rotateX(-Math.PI / 2); + const ru = roofPlane.getAttribute('uv') as BufferAttribute; + for (let i = 0; i < ru.count; i++) ru.setXY(i, ru.getX(i) * ((x1 - x0) / gruen.tile), ru.getY(i) * ((z1 - z0) / gruen.tile)); + const roofMesh = new Mesh(roofPlane, new MeshStandardMaterial({ map: gruen.map, roughness: 1 })); + roofMesh.position.set((x0 + x1) / 2, yTop - 0.05, (z0 + z1) / 2); + roofMesh.receiveShadow = true; + roof.add(roofMesh); + + const attikaMat = new MeshStandardMaterial({ color: 0x8b8377, roughness: 0.7, metalness: 0.15 }); + const attikaPath: Vec2[] = [[x0, z0], [x1, z0], [x1, z1], [x0, z1]]; + for (let i = 0; i < 4; i++) { + const a = attikaPath[i], c = attikaPath[(i + 1) % 4]; + const cx = (a[0] + c[0]) / 2, cz = (a[1] + c[1]) / 2; + const w = Math.abs(c[0] - a[0]) || 0.16; + const d = Math.abs(c[1] - a[1]) || 0.16; + const m = new Mesh(boxGeometry(cx, cz, w + 0.16, d + 0.16, yTop, yTop + b.parapet, 0, 2), attikaMat); + m.castShadow = true; + roof.add(m); + } + group.add(roof); + + // --- Vordach ueber dem Eingang ----------------------------------------- + const cp = b.canopy; + const canopy = new Mesh( + boxGeometry((cp.a + cp.b) / 2, z0 - cp.depth / 2, cp.b - cp.a, cp.depth, cp.y, cp.y + cp.thickness, 0, 2), + new MeshStandardMaterial({ color: 0x5a5148, roughness: 0.6 }), + ); + canopy.name = 'Vordach Eingang'; + canopy.castShadow = true; + group.add(canopy); + + return group; +} + +export function buildNeighbourBuildings(list: NeighbourBuilding[]): Group { + const group = new Group(); + group.name = 'Nachbargebäude'; + const holz = tex.fassadeHolz(); + const mat = new MeshStandardMaterial({ map: holz.map, roughness: holz.roughness }); + const attika = new MeshStandardMaterial({ color: 0x8b8377, roughness: 0.7 }); + + for (const b of list) { + let x0 = Infinity, z0 = Infinity, x1 = -Infinity, z1 = -Infinity; + for (const [x, z] of b.outline) { x0 = Math.min(x0, x); x1 = Math.max(x1, x); z0 = Math.min(z0, z); z1 = Math.max(z1, z); } + const body = new Mesh( + boxGeometry((x0 + x1) / 2, (z0 + z1) / 2, x1 - x0, z1 - z0, b.floorLevel - 0.8, b.roofLevel, 0, holz.tile), + mat, + ); + body.name = b.name; + body.castShadow = true; + body.receiveShadow = true; + group.add(body); + const cap = new Mesh( + boxGeometry((x0 + x1) / 2, (z0 + z1) / 2, x1 - x0 + 0.2, z1 - z0 + 0.2, b.roofLevel, b.roofLevel + 0.3, 0, 2), + attika, + ); + cap.castShadow = true; + group.add(cap); + } + return group; +} diff --git a/src/scene/environment.ts b/src/scene/environment.ts new file mode 100644 index 0000000..9bf3ba6 --- /dev/null +++ b/src/scene/environment.ts @@ -0,0 +1,188 @@ +/** + * Himmel, Sonnenstand und Beleuchtung. + * + * Der Sonnenstand wird fuer den tatsaechlichen Standort gerechnet + * (Abtwil SG, 47.42 N / 9.29 O) und in den Site-Frame gedreht, dessen + * X-Achse mit 61.16 Grad gegen Nord verlaeuft. + */ +import { + AmbientLight, BackSide, Color, DirectionalLight, Fog, Group, HemisphereLight, + Mesh, ShaderMaterial, SphereGeometry, Scene, Vector3, +} from 'three'; + +export interface SunState { + /** Hoehenwinkel in Grad, negativ wenn die Sonne unter dem Horizont steht. */ + altitude: number; + /** Azimut in Grad, von Nord im Uhrzeigersinn. */ + azimuth: number; + /** Richtung zur Sonne im Site-Frame. */ + direction: Vector3; +} + +const RAD = Math.PI / 180; + +/** + * Sonnenstand nach dem vereinfachten NOAA-Verfahren. + * `date` als Tag im Jahr (1..365), `hours` als lokale Uhrzeit in Stunden. + */ +export function sunPosition( + dayOfYear: number, hours: number, lat: number, lon: number, utcOffset: number, northBearingOfSiteX: number, +): SunState { + const gamma = ((2 * Math.PI) / 365) * (dayOfYear - 1 + (hours - 12) / 24); + const eqTime = 229.18 * ( + 0.000075 + 0.001868 * Math.cos(gamma) - 0.032077 * Math.sin(gamma) - + 0.014615 * Math.cos(2 * gamma) - 0.040849 * Math.sin(2 * gamma) + ); + const decl = + 0.006918 - 0.399912 * Math.cos(gamma) + 0.070257 * Math.sin(gamma) - + 0.006758 * Math.cos(2 * gamma) + 0.000907 * Math.sin(2 * gamma) - + 0.002697 * Math.cos(3 * gamma) + 0.00148 * Math.sin(3 * gamma); + + const timeOffset = eqTime + 4 * lon - 60 * utcOffset; + const trueSolarTime = (hours * 60 + timeOffset + 1440) % 1440; + const hourAngle = (trueSolarTime / 4 - 180) * RAD; + const latR = lat * RAD; + + const cosZenith = + Math.sin(latR) * Math.sin(decl) + Math.cos(latR) * Math.cos(decl) * Math.cos(hourAngle); + const zenith = Math.acos(Math.max(-1, Math.min(1, cosZenith))); + const altitude = 90 - zenith / RAD; + + let azimuth = Math.atan2( + -Math.sin(hourAngle), + Math.tan(decl) * Math.cos(latR) - Math.sin(latR) * Math.cos(hourAngle), + ) / RAD; + azimuth = (azimuth + 360) % 360; + + const altR = altitude * RAD; + const bx = (azimuth - northBearingOfSiteX) * RAD; + const bz = (azimuth - (northBearingOfSiteX + 90)) * RAD; + const direction = new Vector3( + Math.cos(altR) * Math.cos(bx), + Math.sin(altR), + Math.cos(altR) * Math.cos(bz), + ).normalize(); + + return { altitude, azimuth, direction }; +} + +const SKY_VERT = /* glsl */ ` + varying vec3 vWorld; + void main() { + vWorld = (modelMatrix * vec4(position, 1.0)).xyz; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } +`; + +const SKY_FRAG = /* glsl */ ` + uniform vec3 uZenith; + uniform vec3 uHorizon; + uniform vec3 uGround; + uniform vec3 uSun; + uniform float uSunIntensity; + varying vec3 vWorld; + + void main() { + vec3 dir = normalize(vWorld); + float h = dir.y; + vec3 sky = mix(uHorizon, uZenith, pow(clamp(h, 0.0, 1.0), 0.55)); + sky = mix(uGround, sky, smoothstep(-0.10, 0.02, h)); + float d = max(dot(dir, normalize(uSun)), 0.0); + sky += uSunIntensity * (pow(d, 900.0) * 6.0 + pow(d, 12.0) * 0.30 + pow(d, 3.0) * 0.06); + gl_FragColor = vec4(sky, 1.0); + } +`; + +export interface Environment { + group: Group; + sun: DirectionalLight; + /** Setzt Tageszeit und Jahreszeit; liefert den berechneten Sonnenstand. */ + update(dayOfYear: number, hours: number): SunState; + /** true, sobald die Gartenbeleuchtung eingeschaltet werden soll. */ + isDark(): boolean; +} + +export function buildEnvironment(scene: Scene, opts: { + lat: number; lon: number; northBearingOfSiteX: number; center: [number, number]; radius: number; +}): Environment { + const group = new Group(); + group.name = 'Umgebung'; + + const skyMat = new ShaderMaterial({ + uniforms: { + uZenith: { value: new Color(0x2f6fc4) }, + uHorizon: { value: new Color(0xbcd6ea) }, + uGround: { value: new Color(0x6f7a63) }, + uSun: { value: new Vector3(0, 1, 0) }, + uSunIntensity: { value: 1 }, + }, + vertexShader: SKY_VERT, + fragmentShader: SKY_FRAG, + side: BackSide, + depthWrite: false, + }); + const sky = new Mesh(new SphereGeometry(900, 32, 20), skyMat); + sky.name = 'Himmel'; + group.add(sky); + + const sun = new DirectionalLight(0xfff3e0, 2.6); + sun.castShadow = true; + sun.shadow.mapSize.set(2048, 2048); + const r = opts.radius; + const cam = sun.shadow.camera; + cam.left = -r; cam.right = r; cam.top = r; cam.bottom = -r; + cam.near = 1; cam.far = r * 6; + sun.shadow.bias = -0.0006; + sun.shadow.normalBias = 0.03; + sun.target.position.set(opts.center[0], 0, opts.center[1]); + group.add(sun, sun.target); + + const hemi = new HemisphereLight(0xcfe3f5, 0x6b6a52, 1.0); + group.add(hemi); + const ambient = new AmbientLight(0xffffff, 0.30); + group.add(ambient); + + scene.fog = new Fog(0xc4d6e6, 90, 620); + + let dark = false; + + function update(dayOfYear: number, hours: number): SunState { + // Sommerzeit grob: Ende Maerz bis Ende Oktober + const utcOffset = dayOfYear > 86 && dayOfYear < 303 ? 2 : 1; + const s = sunPosition(dayOfYear, hours, opts.lat, opts.lon, utcOffset, opts.northBearingOfSiteX); + + const dist = r * 3; + sun.position.set( + opts.center[0] + s.direction.x * dist, + Math.max(2, s.direction.y * dist), + opts.center[1] + s.direction.z * dist, + ); + + // Tageslichtkurve: 0 bei Sonnenuntergang, 1 im Zenit + const day = Math.max(0, Math.min(1, (s.altitude + 4) / 34)); + const dusk = Math.max(0, Math.min(1, (s.altitude + 8) / 14)); + dark = s.altitude < 2; + + const warm = 1 - dusk; + sun.intensity = 3.1 * day; + sun.color.setRGB(1, 0.95 - warm * 0.24, 0.86 - warm * 0.44); + sun.visible = s.altitude > -3; + + hemi.intensity = 0.28 + 0.95 * dusk; + ambient.intensity = 0.10 + 0.30 * dusk; + + const zen = new Color().setHSL(0.60, 0.62 - 0.2 * warm, 0.12 + 0.42 * dusk); + const hor = new Color().setHSL(0.58 - 0.10 * warm, 0.45 + 0.25 * warm, 0.18 + 0.66 * dusk); + skyMat.uniforms.uZenith.value.copy(zen); + skyMat.uniforms.uHorizon.value.copy(hor); + skyMat.uniforms.uGround.value.setHSL(0.25, 0.16, 0.06 + 0.26 * dusk); + skyMat.uniforms.uSun.value.copy(s.direction); + skyMat.uniforms.uSunIntensity.value = Math.max(0, dusk); + + (scene.fog as Fog).color.copy(hor); + + return s; + } + + return { group, sun, update, isDark: () => dark }; +} diff --git a/src/scene/geometry.ts b/src/scene/geometry.ts new file mode 100644 index 0000000..93d6c30 --- /dev/null +++ b/src/scene/geometry.ts @@ -0,0 +1,244 @@ +/** + * Geometrie-Bausteine: Polygone auf das Gelaende legen, Mauern extrudieren. + */ +import { BufferGeometry, BufferAttribute, ShapeUtils, Vector2 } from 'three'; +import type { Vec2 } from '../site'; + +type Tri = [Vec2, Vec2, Vec2]; + +/** Zerlegt ein einfaches Polygon in Dreiecke (Ohrenschneiden aus three.js). */ +export function triangulate(polygon: Vec2[]): Tri[] { + const contour = polygon.map(([x, z]) => new Vector2(x, z)); + const faces = ShapeUtils.triangulateShape(contour, []); + return faces.map(([a, b, c]) => [polygon[a], polygon[b], polygon[c]] as Tri); +} + +/** + * Verfeinert Dreiecke, bis keine Kante laenger als `maxEdge` ist. Damit folgen + * grosse Flaechen dem Gelaende, statt es zu ueberspannen. + */ +export function refine(tris: Tri[], maxEdge: number, maxDepth = 10): Tri[] { + let current = tris; + for (let depth = 0; depth < maxDepth; depth++) { + const next: Tri[] = []; + let split = false; + for (const t of current) { + const lens = [0, 1, 2].map((i) => { + const a = t[i], b = t[(i + 1) % 3]; + return Math.hypot(a[0] - b[0], a[1] - b[1]); + }); + const longest = lens.indexOf(Math.max(...lens)); + if (lens[longest] <= maxEdge) { next.push(t); continue; } + split = true; + const a = t[longest], b = t[(longest + 1) % 3], c = t[(longest + 2) % 3]; + const m: Vec2 = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; + next.push([a, m, c], [m, b, c]); + } + current = next; + if (!split) break; + } + return current; +} + +/** + * Baut ein Flaechenmesh, das dem Gelaende folgt. + * UVs sind weltbezogen, damit Plattenraster ueber Flaechengrenzen durchlaufen. + */ +export function surfaceGeometry( + polygon: Vec2[], + height: (x: number, z: number) => number, + opts: { offset?: number; tile?: number; maxEdge?: number } = {}, +): BufferGeometry { + const { offset = 0.01, tile = 1, maxEdge = 0.3 } = opts; + const tris = refine(triangulate(polygon), maxEdge); + const pos = new Float32Array(tris.length * 9); + const uv = new Float32Array(tris.length * 6); + let p = 0, u = 0; + for (const t of tris) { + for (const [x, z] of t) { + pos[p++] = x; + pos[p++] = height(x, z) + offset; + pos[p++] = z; + uv[u++] = x / tile; + uv[u++] = -z / tile; + } + } + const g = new BufferGeometry(); + g.setAttribute('position', new BufferAttribute(pos, 3)); + g.setAttribute('uv', new BufferAttribute(uv, 2)); + g.computeVertexNormals(); + return g; +} + +/** Fasst mehrere Polygone zu einem Mesh zusammen. */ +export function surfaceGeometryMulti( + polygons: Vec2[][], + height: (x: number, z: number) => number, + opts: { offset?: number; tile?: number; maxEdge?: number } = {}, +): BufferGeometry { + const parts = polygons.map((p) => surfaceGeometry(p, height, opts)); + return mergeGeometries(parts); +} + +/** Einfaches Zusammenfuehren gleichartiger Geometrien (position/uv/normal). */ +export function mergeGeometries(parts: BufferGeometry[]): BufferGeometry { + const total = parts.reduce((n, g) => n + g.getAttribute('position').count, 0); + const pos = new Float32Array(total * 3); + const nor = new Float32Array(total * 3); + const uv = new Float32Array(total * 2); + let o = 0; + for (const g of parts) { + const gp = g.getAttribute('position') as BufferAttribute; + const gn = g.getAttribute('normal') as BufferAttribute; + const gu = g.getAttribute('uv') as BufferAttribute; + pos.set(gp.array as Float32Array, o * 3); + nor.set(gn.array as Float32Array, o * 3); + uv.set(gu.array as Float32Array, o * 2); + o += gp.count; + g.dispose(); + } + const g = new BufferGeometry(); + g.setAttribute('position', new BufferAttribute(pos, 3)); + g.setAttribute('normal', new BufferAttribute(nor, 3)); + g.setAttribute('uv', new BufferAttribute(uv, 2)); + return g; +} + +interface Quad { a: [number, number, number]; b: [number, number, number]; c: [number, number, number]; d: [number, number, number] } + +/** Haengt ein Viereck (gegen den Uhrzeigersinn) an die Puffer an. */ +function pushQuad(pos: number[], uv: number[], q: Quad, uScale: number, vScale: number, uOff = 0) { + const [ax, ay, az] = q.a, [bx, by, bz] = q.b, [cx, cy, cz] = q.c, [dx, dy, dz] = q.d; + const du = Math.hypot(bx - ax, bz - az) * uScale; + const v0 = ay * vScale, v1 = cy * vScale, v2 = dy * vScale, v3 = by * vScale; + pos.push(ax, ay, az, bx, by, bz, cx, cy, cz); + uv.push(uOff, v0, uOff + du, v3, uOff + du, v1); + pos.push(ax, ay, az, cx, cy, cz, dx, dy, dz); + uv.push(uOff, v0, uOff + du, v1, uOff, v2); +} + +/** + * Extrudiert eine Mauer entlang einer Mittellinie. + * `top`/`base` liefern fuer einen Parameter t (0..1) die Kronen- bzw. Fusshoehe. + */ +export function wallGeometry( + path: Vec2[], + thickness: number, + top: (t: number, x: number, z: number) => number, + base: (t: number, x: number, z: number) => number, + opts: { segments?: number; tile?: number } = {}, +): BufferGeometry { + const { segments = 24, tile = 1.6 } = opts; + const pos: number[] = []; + const uv: number[] = []; + const h = thickness / 2; + const s = 1 / tile; + + // Laenge und Richtungen entlang des Pfades + const pts: { x: number; z: number; nx: number; nz: number; t: number }[] = []; + const totalLen = path.slice(1).reduce((L, p, i) => L + Math.hypot(p[0] - path[i][0], p[1] - path[i][1]), 0); + for (let i = 0; i <= segments; i++) { + const t = i / segments; + // Punkt auf dem Polyzug bei Bogenlaenge t * totalLen + let want = t * totalLen; + let x = path[0][0], z = path[0][1], dx = 1, dz = 0; + for (let k = 1; k < path.length; k++) { + const ax = path[k - 1][0], az = path[k - 1][1]; + const bx = path[k][0], bz = path[k][1]; + const L = Math.hypot(bx - ax, bz - az); + if (want <= L || k === path.length - 1) { + const u = L > 0 ? Math.min(1, want / L) : 0; + x = ax + (bx - ax) * u; z = az + (bz - az) * u; + dx = (bx - ax) / (L || 1); dz = (bz - az) / (L || 1); + break; + } + want -= L; + } + pts.push({ x, z, nx: -dz, nz: dx, t }); + } + + let uRun = 0; + for (let i = 0; i < segments; i++) { + const p0 = pts[i], p1 = pts[i + 1]; + const t0 = top(p0.t, p0.x, p0.z), t1 = top(p1.t, p1.x, p1.z); + const b0 = base(p0.t, p0.x, p0.z), b1 = base(p1.t, p1.x, p1.z); + const segLen = Math.hypot(p1.x - p0.x, p1.z - p0.z); + + const l0: Vec2 = [p0.x + p0.nx * h, p0.z + p0.nz * h]; + const r0: Vec2 = [p0.x - p0.nx * h, p0.z - p0.nz * h]; + const l1: Vec2 = [p1.x + p1.nx * h, p1.z + p1.nz * h]; + const r1: Vec2 = [p1.x - p1.nx * h, p1.z - p1.nz * h]; + + // linke Flanke + pushQuad(pos, uv, { + a: [l0[0], b0, l0[1]], b: [l1[0], b1, l1[1]], + c: [l1[0], t1, l1[1]], d: [l0[0], t0, l0[1]], + }, s, s, uRun * s); + // rechte Flanke + pushQuad(pos, uv, { + a: [r1[0], b1, r1[1]], b: [r0[0], b0, r0[1]], + c: [r0[0], t0, r0[1]], d: [r1[0], t1, r1[1]], + }, s, s, uRun * s); + // Krone + pos.push(l0[0], t0, l0[1], r0[0], t0, r0[1], r1[0], t1, r1[1]); + uv.push(uRun * s, 0, uRun * s, thickness * s, (uRun + segLen) * s, thickness * s); + pos.push(l0[0], t0, l0[1], r1[0], t1, r1[1], l1[0], t1, l1[1]); + uv.push(uRun * s, 0, (uRun + segLen) * s, thickness * s, (uRun + segLen) * s, 0); + uRun += segLen; + } + + // Stirnseiten + for (const [p, sign] of [[pts[0], -1], [pts[pts.length - 1], 1]] as const) { + const tp = top(p.t, p.x, p.z), bs = base(p.t, p.x, p.z); + const l: Vec2 = [p.x + p.nx * h, p.z + p.nz * h]; + const r: Vec2 = [p.x - p.nx * h, p.z - p.nz * h]; + const A = sign < 0 ? l : r, B = sign < 0 ? r : l; + pushQuad(pos, uv, { + a: [A[0], bs, A[1]], b: [B[0], bs, B[1]], + c: [B[0], tp, B[1]], d: [A[0], tp, A[1]], + }, s, s, 0); + } + + const g = new BufferGeometry(); + g.setAttribute('position', new BufferAttribute(new Float32Array(pos), 3)); + g.setAttribute('uv', new BufferAttribute(new Float32Array(uv), 2)); + g.computeVertexNormals(); + return g; +} + +/** Quaderfoermiges Volumen mit weltbezogenen UVs, fuer Bauteile und Moebel. */ +export function boxGeometry( + cx: number, cz: number, w: number, d: number, y0: number, y1: number, rotDeg = 0, tile = 1, +): BufferGeometry { + const pos: number[] = []; + const uv: number[] = []; + const a = (rotDeg * Math.PI) / 180; + const ca = Math.cos(a), sa = Math.sin(a); + const P = (u: number, v: number, y: number): [number, number, number] => + [cx + u * ca - v * sa, y, cz + u * sa + v * ca]; + const hw = w / 2, hd = d / 2; + const s = 1 / tile; + const faces: [Vec2, Vec2, number][] = [ + [[-hw, -hd], [hw, -hd], 1], [[hw, -hd], [hw, hd], 1], + [[hw, hd], [-hw, hd], 1], [[-hw, hd], [-hw, -hd], 1], + ]; + let uRun = 0; + for (const [p0, p1] of faces) { + const len = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]); + const A = P(p0[0], p0[1], y0), B = P(p1[0], p1[1], y0); + const C = P(p1[0], p1[1], y1), D = P(p0[0], p0[1], y1); + pos.push(...A, ...B, ...C, ...A, ...C, ...D); + uv.push(uRun * s, y0 * s, (uRun + len) * s, y0 * s, (uRun + len) * s, y1 * s); + uv.push(uRun * s, y0 * s, (uRun + len) * s, y1 * s, uRun * s, y1 * s); + uRun += len; + } + // Deckel + const t0 = P(-hw, -hd, y1), t1 = P(hw, -hd, y1), t2 = P(hw, hd, y1), t3 = P(-hw, hd, y1); + pos.push(...t0, ...t1, ...t2, ...t0, ...t2, ...t3); + uv.push(0, 0, w * s, 0, w * s, d * s, 0, 0, w * s, d * s, 0, d * s); + const g = new BufferGeometry(); + g.setAttribute('position', new BufferAttribute(new Float32Array(pos), 3)); + g.setAttribute('uv', new BufferAttribute(new Float32Array(uv), 2)); + g.computeVertexNormals(); + return g; +} diff --git a/src/scene/structures.ts b/src/scene/structures.ts new file mode 100644 index 0000000..2b6254e --- /dev/null +++ b/src/scene/structures.ts @@ -0,0 +1,302 @@ +/** + * Bauteile: Stuetz- und Sitzmauern, Travertinstelen, Gartenhaus, Spielhaus, + * Flammengrill, Baenkli, Leuchten. + */ +import { + BoxGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial, PointLight, + type MeshStandardMaterialParameters, +} from 'three'; +import type { Lamp, Prop, Stele, Wall } from '../site'; +import { boxGeometry, wallGeometry } from './geometry'; +import * as tex from './textures'; + +function material(belag: tex.Belag, extra: MeshStandardMaterialParameters = {}) { + return new MeshStandardMaterial({ + map: belag.map, roughness: belag.roughness, metalness: 0, ...extra, + }); +} + +const WALL_LOOK: Record tex.Belag> = { + travertin: tex.travertinMauer, + beton: tex.beton, + holzscheiter: tex.holzscheiter, + lamellen: tex.fassadeHolz, +}; + +/** Wertet ein Hoehenprofil (Stuetzelemente) an der Stelle t aus. */ +function profileAt(profile: number[] | undefined, t: number, fallback: number): number { + if (!profile || !profile.length) return fallback; + const f = t * (profile.length - 1); + const i = Math.max(0, Math.min(profile.length - 2, Math.floor(f))); + const u = f - i; + return profile[i] * (1 - u) + profile[i + 1] * u; +} + +export function buildWalls(walls: Wall[], height: (x: number, z: number) => number): Group { + const group = new Group(); + group.name = 'Mauern und Wände'; + + for (const w of walls) { + const belag = WALL_LOOK[w.type](); + const top = (t: number, x: number, z: number) => + w.crestProfile ? profileAt(w.crestProfile, t, 0) : (w.top ?? height(x, z) + 0.4); + const base = (t: number, x: number, z: number) => + w.footProfile ? profileAt(w.footProfile, t, -1) : (w.base ?? height(x, z) - 0.5); + + if (w.openSlats) { + // Lamellenwand: senkrechte Bretter im Plan-Rhythmus (10 cm breit, + // 35 cm tief, rund 90 cm Achsabstand), dazu das schmale Randband. + const slats = new Group(); + const [a, b] = [w.path[0], w.path[w.path.length - 1]]; + const len = Math.hypot(b[0] - a[0], b[1] - a[1]); + const n = Math.max(2, Math.round(len / 0.9)); + const mat = material(belag, { roughness: 0.85 }); + const dirX = (b[0] - a[0]) / len, dirZ = (b[1] - a[1]) / len; + const angle = (Math.atan2(dirZ, dirX) * 180) / Math.PI; + for (let i = 0; i <= n; i++) { + const t = i / n; + const x = a[0] + (b[0] - a[0]) * t; + const z = a[1] + (b[1] - a[1]) * t; + const g = boxGeometry(x, z, 0.10, w.t, base(t, x, z), top(t, x, z), angle, 1.2); + const m = new Mesh(g, mat); + m.castShadow = true; + m.receiveShadow = true; + slats.add(m); + } + // durchlaufendes Randband an der Fassadenkante + const band = new Mesh( + wallGeometry(w.path, 0.10, top, base, { segments: 6, tile: belag.tile }), + mat, + ); + band.castShadow = true; + band.receiveShadow = true; + slats.add(band); + slats.name = w.id; + slats.userData.note = w.note; + group.add(slats); + continue; + } + + const geo = wallGeometry(w.path, w.t, top, base, { + segments: w.crestProfile ? w.crestProfile.length - 1 : 16, + tile: belag.tile, + }); + const mesh = new Mesh(geo, material(belag)); + mesh.name = w.id; + mesh.castShadow = true; + mesh.receiveShadow = true; + mesh.userData.note = w.note; + group.add(mesh); + } + + return group; +} + +export function buildStelen(stelen: Stele[], height: (x: number, z: number) => number): Group { + const group = new Group(); + group.name = 'Travertinstelen'; + const mat = material(tex.travertinMauer(), { roughness: 0.68 }); + for (const s of stelen) { + const y0 = height(s.x, s.z) - 0.3; + const geo = boxGeometry(s.x, s.z, 0.22, 0.22, y0, y0 + 0.3 + s.h, 0, 1.6); + const m = new Mesh(geo, mat); + m.castShadow = true; + m.receiveShadow = true; + group.add(m); + } + return group; +} + +// --------------------------------------------------------------------------- +// Freistehende Objekte +// --------------------------------------------------------------------------- + +function gartenhaus(p: Prop, y: number): Group { + const g = new Group(); + const holz = material(tex.fassadeHolz(), { roughness: 0.8 }); + const body = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y, y + p.h, p.rot, 1.2), holz); + body.castShadow = true; body.receiveShadow = true; + g.add(body); + const roof = new Mesh( + boxGeometry(p.x, p.z, p.w + 0.16, p.d + 0.16, y + p.h, y + p.h + 0.09, p.rot, 2), + material(tex.beton(), { color: 0x6a6a66, roughness: 0.9 }), + ); + roof.castShadow = true; + g.add(roof); + // Tuer + const door = new Mesh( + new BoxGeometry(0.78, 1.95, 0.04), + new MeshStandardMaterial({ color: 0x3b3934, roughness: 0.6 }), + ); + door.position.set(p.x, y + 0.98, p.z - p.d / 2 - 0.02); + g.add(door); + return g; +} + +/** Spielhaus durch Zimmermann: Podest, Bretterwaende, Gelaender, Pultdach. */ +function spielhaus(p: Prop, y: number): Group { + const g = new Group(); + const holz = material(tex.fassadeHolz(), { color: 0xd8b98c, roughness: 0.85 }); + const dunkel = material(tex.fassadeHolz(), { color: 0xa8895f, roughness: 0.88 }); + const podestTop = y + 0.55; + + const podest = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y - 0.1, podestTop, p.rot, 1.2), dunkel); + podest.castShadow = true; podest.receiveShadow = true; + g.add(podest); + + const hw = p.w / 2, hd = p.d / 2; + // Rueckwand und zwei Seitenwaende, Vorderseite offen + const back = new Mesh(boxGeometry(p.x, p.z + hd - 0.05, p.w, 0.09, podestTop, y + p.h, p.rot, 1.2), holz); + const left = new Mesh(boxGeometry(p.x - hw + 0.05, p.z, 0.09, p.d, podestTop, y + p.h, p.rot, 1.2), holz); + const right = new Mesh(boxGeometry(p.x + hw - 0.05, p.z - hd / 3, 0.09, p.d / 3, podestTop, y + p.h, p.rot, 1.2), holz); + for (const m of [back, left, right]) { m.castShadow = true; m.receiveShadow = true; g.add(m); } + + // Bruestung an der offenen Seite + const rail = new Mesh(boxGeometry(p.x, p.z - hd + 0.05, p.w, 0.07, podestTop + 0.32, podestTop + 0.45, p.rot, 1.2), holz); + rail.castShadow = true; + g.add(rail); + + const roof = new Mesh( + boxGeometry(p.x, p.z, p.w + 0.26, p.d + 0.26, y + p.h, y + p.h + 0.09, p.rot, 2), + material(tex.fassadeHolz(), { color: 0x7d6549, roughness: 0.9 }), + ); + roof.castShadow = true; + g.add(roof); + return g; +} + +function flammengrill(p: Prop, y: number): Group { + const g = new Group(); + const steel = new MeshStandardMaterial({ color: 0x3a3b3e, roughness: 0.5, metalness: 0.45 }); + const body = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y + 0.06, y + p.h, p.rot, 1), steel); + body.castShadow = true; body.receiveShadow = true; + g.add(body); + const plate = new Mesh( + boxGeometry(p.x, p.z, p.w + 0.1, p.d + 0.1, y + p.h, y + p.h + 0.03, p.rot, 1), + new MeshStandardMaterial({ color: 0x3a3a3c, roughness: 0.35, metalness: 0.7 }), + ); + plate.castShadow = true; + g.add(plate); + return g; +} + +function bank(p: Prop, y: number): Group { + const g = new Group(); + const stein = material(tex.travertinMauer(), { roughness: 0.65 }); + const seat = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y + p.h - 0.09, y + p.h, p.rot, 1.6), stein); + seat.castShadow = true; seat.receiveShadow = true; + g.add(seat); + for (const s of [-1, 1]) { + const m = new Mesh( + boxGeometry(p.x, p.z + (s * (p.d / 2 - 0.22)), p.w - 0.08, 0.14, y, y + p.h - 0.09, p.rot, 1.6), + stein, + ); + m.castShadow = true; + g.add(m); + } + return g; +} + +function simpleBox(p: Prop, y: number, color: number, rough = 0.7): Mesh { + const m = new Mesh( + boxGeometry(p.x, p.z, p.w, p.d, y, y + p.h, p.rot, 1), + new MeshStandardMaterial({ color, roughness: rough }), + ); + m.castShadow = true; + m.receiveShadow = true; + return m; +} + +function gartentor(p: Prop, y: number): Group { + const g = new Group(); + const mat = new MeshStandardMaterial({ color: 0x4a4a4c, roughness: 0.5, metalness: 0.4 }); + const frame = new Mesh(boxGeometry(p.x, p.z, p.w, 0.05, y, y + p.h, p.rot, 1), mat); + g.add(frame); + for (let i = 1; i < 6; i++) { + const bar = new Mesh( + boxGeometry(p.x - p.w / 2 + (i * p.w) / 6, p.z, 0.03, 0.03, y, y + p.h, p.rot, 1), + mat, + ); + g.add(bar); + } + g.children.forEach((c) => { (c as Mesh).castShadow = true; }); + return g; +} + +export function buildProps(props: Prop[], height: (x: number, z: number) => number): Group { + const group = new Group(); + group.name = 'Objekte'; + for (const p of props) { + const y = height(p.x, p.z); + let obj: Group | Mesh; + switch (p.type) { + case 'gartenhaus': obj = gartenhaus(p, y); break; + case 'spielhaus': obj = spielhaus(p, y); break; + case 'flammengrill': obj = flammengrill(p, y); break; + case 'bank': obj = bank(p, y); break; + case 'tor': obj = gartentor(p, y); break; + case 'roboter': obj = simpleBox(p, y, 0x33413a, 0.6); break; + case 'wasserhahn': obj = simpleBox(p, y, 0x8a8d90, 0.4); break; + case 'schacht': obj = simpleBox(p, y - 0.01, 0x77746e, 0.9); break; + default: continue; + } + obj.name = p.id; + obj.userData.note = p.note; + group.add(obj); + } + return group; +} + +// --------------------------------------------------------------------------- +// Beleuchtung im Garten +// --------------------------------------------------------------------------- + +export interface GardenLights { + group: Group; + /** Schaltet die Leuchten je nach Tageszeit. */ + setOn(on: boolean): void; +} + +export function buildLamps(lamps: Lamp[], height: (x: number, z: number) => number): GardenLights { + const group = new Group(); + group.name = 'Beleuchtung'; + const bodyMat = new MeshStandardMaterial({ color: 0x33332f, roughness: 0.55, metalness: 0.3 }); + const glowMat = new MeshStandardMaterial({ + color: 0xffd9a0, emissive: 0xffc26e, emissiveIntensity: 0, roughness: 0.4, + }); + const pointLights: PointLight[] = []; + + for (const l of lamps) { + const y = height(l.x, l.z); + const g = new Group(); + if (l.kind === 'poller') { + const post = new Mesh(new BoxGeometry(0.09, 0.62, 0.09), bodyMat); + post.position.set(l.x, y + 0.31, l.z); + post.castShadow = true; + g.add(post); + const head = new Mesh(new BoxGeometry(0.11, 0.06, 0.11), glowMat); + head.position.set(l.x, y + 0.6, l.z); + g.add(head); + } else { + const ring = new Mesh(new CylinderGeometry(0.07, 0.07, 0.03, 16), bodyMat); + ring.position.set(l.x, y + 0.015, l.z); + g.add(ring); + const lens = new Mesh(new CylinderGeometry(0.05, 0.05, 0.035, 16), glowMat); + lens.position.set(l.x, y + 0.025, l.z); + g.add(lens); + } + const pl = new PointLight(0xffc98a, 0, 6, 2); + pl.position.set(l.x, y + (l.kind === 'poller' ? 0.6 : 0.1), l.z); + pointLights.push(pl); + g.add(pl); + group.add(g); + } + + return { + group, + setOn(on: boolean) { + glowMat.emissiveIntensity = on ? 1.6 : 0; + for (const pl of pointLights) pl.intensity = on ? 2.2 : 0; + }, + }; +} diff --git a/src/scene/surfaces.ts b/src/scene/surfaces.ts new file mode 100644 index 0000000..8790ef0 --- /dev/null +++ b/src/scene/surfaces.ts @@ -0,0 +1,62 @@ +/** + * Belagsflaechen aus dem Gartenplan: Travertin, Keramik, Tegula, Rasen, + * Schotterwiese, Beete, Spielsand. + */ +import { Group, Mesh, MeshStandardMaterial, DoubleSide } from 'three'; +import type { Surface, SurfaceType } from '../site'; +import { surfaceGeometryMulti } from './geometry'; +import * as tex from './textures'; + +const BELAG: Record tex.Belag; offset: number; label: string }> = { + travertin80: { belag: tex.travertin80, offset: 0.042, label: 'Travertinplatten 80×80×3 cm' }, + travertin4060: { belag: tex.travertin4060, offset: 0.042, label: 'Travertinplatten 40×60×3 cm mit Dekorkies' }, + travertinstell: { belag: tex.travertinstell, offset: 0.038, label: 'Travertinstellplatten' }, + keramik: { belag: tex.keramik, offset: 0.032, label: 'Keramikplatten 60×60×2 cm' }, + verbundstein: { belag: tex.verbundstein, offset: 0.030, label: 'Verbundsteine (Anpassung Nachbar)' }, + tegula: { belag: tex.tegula, offset: 0.034, label: 'Tegula-Pflastersteine grau' }, + rasen: { belag: tex.rasen, offset: 0.022, label: 'Rasen' }, + schotterwiese: { belag: tex.schotterwiese, offset: 0.020, label: 'Schotterwiese' }, + beet: { belag: tex.beet, offset: 0.018, label: 'Pflanzbeet' }, + spielsand: { belag: tex.spielsand, offset: 0.016, label: 'Spielsand' }, +}; + +export function buildSurfaces( + surfaces: Surface[], + height: (x: number, z: number) => number, +): Group { + const group = new Group(); + group.name = 'Beläge'; + + const byType = new Map(); + for (const s of surfaces) { + const list = byType.get(s.type) ?? []; + list.push(s); + byType.set(s.type, list); + } + + for (const [type, list] of byType) { + const def = BELAG[type]; + if (!def) continue; + const belag = def.belag(); + const geo = surfaceGeometryMulti( + list.map((s) => s.polygon), + height, + { offset: def.offset, tile: belag.tile, maxEdge: 0.28 }, + ); + const mesh = new Mesh(geo, new MeshStandardMaterial({ + map: belag.map, + roughness: belag.roughness, + metalness: 0, + side: DoubleSide, + polygonOffset: true, + polygonOffsetFactor: -1, + polygonOffsetUnits: -1, + })); + mesh.name = def.label; + mesh.receiveShadow = true; + mesh.userData.surfaceType = type; + group.add(mesh); + } + + return group; +} diff --git a/src/scene/terrain.ts b/src/scene/terrain.ts new file mode 100644 index 0000000..13abca7 --- /dev/null +++ b/src/scene/terrain.ts @@ -0,0 +1,116 @@ +/** + * Gelaende: Basismesh aus dem Hoehenraster, plus eine weite, flache Umgebung, + * damit der Garten nicht im Nichts steht. + */ +import { + BufferAttribute, BufferGeometry, DoubleSide, Mesh, MeshStandardMaterial, Group, +} from 'three'; +import type { Terrain } from '../site'; +import { rasen, schotterwiese } from './textures'; + +export function buildTerrain(terrain: Terrain): Group { + const group = new Group(); + group.name = 'Gelände'; + const { x0, z0, cell, nx, nz } = terrain.grid; + const h = terrain.heights; + + // --- Hauptgelaende ------------------------------------------------------- + const geo = new BufferGeometry(); + const verts = new Float32Array(nx * nz * 3); + const uvs = new Float32Array(nx * nz * 2); + const belag = rasen(); + for (let j = 0; j < nz; j++) { + for (let i = 0; i < nx; i++) { + const k = j * nx + i; + const x = x0 + i * cell, z = z0 + j * cell; + verts[k * 3] = x; + verts[k * 3 + 1] = h[k]; + verts[k * 3 + 2] = z; + uvs[k * 2] = x / belag.tile; + uvs[k * 2 + 1] = -z / belag.tile; + } + } + const idx: number[] = []; + for (let j = 0; j < nz - 1; j++) { + for (let i = 0; i < nx - 1; i++) { + const a = j * nx + i, b = a + 1, c = a + nx, d = c + 1; + idx.push(a, c, b, b, c, d); + } + } + geo.setAttribute('position', new BufferAttribute(verts, 3)); + geo.setAttribute('uv', new BufferAttribute(uvs, 2)); + geo.setIndex(idx); + geo.computeVertexNormals(); + + const ground = new Mesh(geo, new MeshStandardMaterial({ + map: belag.map, roughness: belag.roughness, metalness: 0, + })); + ground.name = 'Terrain'; + ground.receiveShadow = true; + group.add(ground); + + // --- Umland --------------------------------------------------------------- + // Das Grundstueck liegt an einem Hang ueber dem Tal. Das Umland wird als + // Kranz direkt an die Randpunkte des Rasters gehaengt - so entsteht keine + // sichtbare Kante - und faellt gegen aussen ab, vor allem nach Suedwesten. + const wiese = schotterwiese(); + const cx = x0 + ((nx - 1) * cell) / 2; + const cz = z0 + ((nz - 1) * cell) / 2; + + /** + * Randpunkte des Rasters im Umlauf, bewusst grob abgetastet: bei jedem + * Rasterpunkt entstuenden extrem schlanke Dreiecke, deren Normalen + * unbrauchbar werden und sich als dunkle Streifen abzeichnen. + */ + const STEP = 10; + const border: { x: number; z: number; y: number }[] = []; + const at = (i: number, j: number) => ({ + x: x0 + i * cell, z: z0 + j * cell, y: h[j * nx + i] - 0.05, + }); + for (let i = 0; i < nx - 1; i += STEP) border.push(at(i, 0)); + for (let j = 0; j < nz - 1; j += STEP) border.push(at(nx - 1, j)); + for (let i = nx - 1; i > 0; i -= STEP) border.push(at(i, nz - 1)); + for (let j = nz - 1; j > 0; j -= STEP) border.push(at(0, j)); + + // Der Kranz entsteht durch reine Skalierung des Randpolygons um den + // Mittelpunkt. Das kann sich nicht selbst durchdringen, egal wie weit + // hinausskaliert wird, und schliesst luecklos an das Raster an. + const RINGS = 26; + const SCALE_MAX = 18; + const ringPos: number[] = []; + const ringUv: number[] = []; + const ringIdx: number[] = []; + const n = border.length; + for (let k = 0; k <= RINGS; k++) { + const t = (k / RINGS) ** 2.4; + const s = 1 + (SCALE_MAX - 1) * t; + for (const p of border) { + const dx = p.x - cx, dz = p.z - cz; + const x = cx + dx * s; + const z = cz + dz * s; + // Nach Suedwesten faellt das Gelaende deutlich staerker ab (Blick ins Tal). + const sw = (1 - Math.cos(Math.atan2(dz, dx) - 2.4)) / 2; + ringPos.push(x, p.y - t * (6 + 26 * sw * sw), z); + ringUv.push(x / (wiese.tile * 5), -z / (wiese.tile * 5)); + } + } + for (let k = 0; k < RINGS; k++) { + for (let i = 0; i < n; i++) { + const a = k * n + i, b = k * n + ((i + 1) % n); + const c = (k + 1) * n + i, d = (k + 1) * n + ((i + 1) % n); + ringIdx.push(a, c, b, b, c, d); + } + } + const far = new BufferGeometry(); + far.setAttribute('position', new BufferAttribute(new Float32Array(ringPos), 3)); + far.setAttribute('uv', new BufferAttribute(new Float32Array(ringUv), 2)); + far.setIndex(ringIdx); + far.computeVertexNormals(); + const surroundings = new Mesh(far, new MeshStandardMaterial({ + map: wiese.map, roughness: 1, metalness: 0, color: 0x86935f, side: DoubleSide, + })); + surroundings.name = 'Umland'; + group.add(surroundings); + + return group; +} diff --git a/src/scene/textures.ts b/src/scene/textures.ts new file mode 100644 index 0000000..1761af2 --- /dev/null +++ b/src/scene/textures.ts @@ -0,0 +1,401 @@ +/** + * Prozedurale Texturen. Die Anwendung soll ohne externe Assets auskommen, + * deshalb werden alle Belaege zur Laufzeit auf ein Canvas gezeichnet. + * + * Konvention: Jede Textur bildet eine quadratische Weltflaeche von `tile` + * Metern ab. Die Meshes bekommen weltbezogene UVs (x/tile, z/tile), damit + * Plattenraster ueber Flaechengrenzen hinweg durchlaufen - so wie im Plan. + */ +import { + CanvasTexture, RepeatWrapping, SRGBColorSpace, LinearMipmapLinearFilter, +} from 'three'; + +const SIZE = 512; + +/** Deterministischer Zufall, damit jeder Aufbau gleich aussieht. */ +function mulberry32(seed: number) { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +function canvas(): [HTMLCanvasElement, CanvasRenderingContext2D] { + const c = document.createElement('canvas'); + c.width = SIZE; + c.height = SIZE; + const ctx = c.getContext('2d', { willReadFrequently: true })!; + return [c, ctx]; +} + +/** Kachelbares Wertrauschen, wird fuer Koernung und Farbschwankung genutzt. */ +function noiseTile(ctx: CanvasRenderingContext2D, seed: number, cells: number, alpha: number, dark = true) { + const rnd = mulberry32(seed); + const g: number[] = []; + for (let i = 0; i < cells * cells; i++) g.push(rnd()); + const at = (i: number, j: number) => g[((j + cells) % cells) * cells + ((i + cells) % cells)]; + const img = ctx.getImageData(0, 0, SIZE, SIZE); + const step = SIZE / cells; + for (let y = 0; y < SIZE; y++) { + const fy = y / step, j = Math.floor(fy), ty = fy - j; + for (let x = 0; x < SIZE; x++) { + const fx = x / step, i = Math.floor(fx), tx = fx - i; + const sx = tx * tx * (3 - 2 * tx), sy = ty * ty * (3 - 2 * ty); + const v = + at(i, j) * (1 - sx) * (1 - sy) + at(i + 1, j) * sx * (1 - sy) + + at(i, j + 1) * (1 - sx) * sy + at(i + 1, j + 1) * sx * sy; + const k = (y * SIZE + x) * 4; + const d = (dark ? -1 : 1) * (v - 0.5) * 2 * alpha * 255; + img.data[k] = Math.max(0, Math.min(255, img.data[k] + d)); + img.data[k + 1] = Math.max(0, Math.min(255, img.data[k + 1] + d)); + img.data[k + 2] = Math.max(0, Math.min(255, img.data[k + 2] + d)); + } + } + ctx.putImageData(img, 0, 0); +} + +/** Feine Koernung als Ueberlagerung, ohne die Kachelbarkeit zu stoeren. */ +function grain(ctx: CanvasRenderingContext2D, seed: number, amount: number, dotSize = 1) { + const rnd = mulberry32(seed); + const n = Math.floor((SIZE * SIZE) / (24 * dotSize)); + for (let i = 0; i < n; i++) { + const v = Math.floor(rnd() * 255); + ctx.fillStyle = `rgba(${v},${v},${v},${amount})`; + ctx.fillRect(rnd() * SIZE, rnd() * SIZE, dotSize, dotSize); + } +} + +function texture(c: HTMLCanvasElement): CanvasTexture { + const t = new CanvasTexture(c); + t.wrapS = t.wrapT = RepeatWrapping; + t.colorSpace = SRGBColorSpace; + t.anisotropy = 8; + t.minFilter = LinearMipmapLinearFilter; + t.generateMipmaps = true; + return t; +} + +/** Zeichnet ein Plattenraster mit Fuge. slabW/slabH in Metern. */ +function slabs( + ctx: CanvasRenderingContext2D, tile: number, slabW: number, slabH: number, + base: string, jointColor: string, jointM: number, seed: number, variation: number, +) { + const px = SIZE / tile; + ctx.fillStyle = jointColor; + ctx.fillRect(0, 0, SIZE, SIZE); + const rnd = mulberry32(seed); + const cols = Math.round(tile / slabW); + const rows = Math.round(tile / slabH); + const j = Math.max(1, jointM * px); + const [r, g, b] = base.match(/\d+/g)!.map(Number); + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const d = (rnd() - 0.5) * 2 * variation; + ctx.fillStyle = `rgb(${Math.round(r + d)},${Math.round(g + d)},${Math.round(b + d)})`; + ctx.fillRect(col * slabW * px + j / 2, row * slabH * px + j / 2, slabW * px - j, slabH * px - j); + } + } +} + +export interface Belag { + map: CanvasTexture; + tile: number; + roughness: number; + color?: number; +} + +const cache = new Map(); + +function make(key: string, tile: number, roughness: number, draw: (ctx: CanvasRenderingContext2D) => void): Belag { + const hit = cache.get(key); + if (hit) return hit; + const [c, ctx] = canvas(); + draw(ctx); + const belag: Belag = { map: texture(c), tile, roughness }; + cache.set(key, belag); + return belag; +} + +// --------------------------------------------------------------------------- +// Belaege +// --------------------------------------------------------------------------- + +/** Travertinplatten 80 x 80 x 3 cm, warmes Creme mit feiner Poren-Struktur. */ +export const travertin80 = () => make('travertin80', 3.2, 0.72, (ctx) => { + slabs(ctx, 3.2, 0.8, 0.8, 'rgb(209,195,168)', 'rgb(170,157,134)', 0.008, 11, 7); + noiseTile(ctx, 21, 32, 0.05); + grain(ctx, 31, 0.05, 2); +}); + +/** Travertinplatten 40 x 60 x 3 cm, im Plan mit Dekorkies in den Fugen. */ +export const travertin4060 = () => make('travertin4060', 2.4, 0.75, (ctx) => { + slabs(ctx, 2.4, 0.4, 0.6, 'rgb(212,199,172)', 'rgb(158,150,133)', 0.02, 12, 8); + noiseTile(ctx, 22, 32, 0.05); + grain(ctx, 32, 0.06, 2); +}); + +/** Travertinstellplatten als schmaler Randstreifen. */ +export const travertinstell = () => make('travertinstell', 2.4, 0.75, (ctx) => { + slabs(ctx, 2.4, 0.4, 0.4, 'rgb(206,192,166)', 'rgb(164,154,135)', 0.012, 13, 8); + noiseTile(ctx, 23, 32, 0.05); + grain(ctx, 33, 0.05, 2); +}); + +/** Keramikplatten 60 x 60 x 2 cm, grau - bauseits vorhanden (siehe Fotos). */ +export const keramik = () => make('keramik', 2.4, 0.55, (ctx) => { + slabs(ctx, 2.4, 0.6, 0.6, 'rgb(160,158,154)', 'rgb(128,127,124)', 0.006, 14, 5); + noiseTile(ctx, 24, 48, 0.035); + grain(ctx, 34, 0.04, 1); +}); + +/** Tegula-Pflastersteine grau, kleinformatig und leicht unregelmaessig. */ +export const tegula = () => make('tegula', 1.2, 0.85, (ctx) => { + const px = SIZE / 1.2; + ctx.fillStyle = 'rgb(118,116,114)'; + ctx.fillRect(0, 0, SIZE, SIZE); + const rnd = mulberry32(15); + const sw = 0.15, sh = 0.1; + const cols = Math.round(1.2 / sw), rows = Math.round(1.2 / sh); + for (let row = 0; row < rows; row++) { + const off = (row % 2) * 0.5; + for (let col = -1; col <= cols; col++) { + const v = 140 + rnd() * 34; + ctx.fillStyle = `rgb(${v | 0},${(v - 2) | 0},${(v - 5) | 0})`; + const x = (col + off) * sw * px + 1.2; + const y = row * sh * px + 1.2; + ctx.fillRect(x, y, sw * px - 2.4, sh * px - 2.4); + } + } + noiseTile(ctx, 25, 24, 0.06); + grain(ctx, 35, 0.09, 1); +}); + +/** Verbundsteine des Nachbarn, etwas dunkler als Tegula. */ +export const verbundstein = () => make('verbundstein', 1.2, 0.88, (ctx) => { + const px = SIZE / 1.2; + ctx.fillStyle = 'rgb(110,108,106)'; + ctx.fillRect(0, 0, SIZE, SIZE); + const rnd = mulberry32(16); + for (let row = 0; row < 8; row++) { + for (let col = 0; col < 8; col++) { + const v = 126 + rnd() * 30; + ctx.fillStyle = `rgb(${v | 0},${v | 0},${(v - 4) | 0})`; + ctx.fillRect(col * 0.15 * px + 1.5, row * 0.15 * px + 1.5, 0.15 * px - 3, 0.15 * px - 3); + } + } + grain(ctx, 36, 0.08, 1); +}); + +/** Rasen: gemaehte Flaeche mit leichten Bahnen. */ +export const rasen = () => make('rasen', 2.0, 0.95, (ctx) => { + ctx.fillStyle = 'rgb(96,133,62)'; + ctx.fillRect(0, 0, SIZE, SIZE); + noiseTile(ctx, 41, 16, 0.09); + noiseTile(ctx, 42, 64, 0.07); + const rnd = mulberry32(43); + for (let i = 0; i < 5200; i++) { + const x = rnd() * SIZE, y = rnd() * SIZE; + const v = rnd(); + ctx.strokeStyle = `rgba(${(78 + v * 62) | 0},${(118 + v * 66) | 0},${(44 + v * 40) | 0},0.55)`; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + (rnd() - 0.5) * 3, y - 2 - rnd() * 4); + ctx.stroke(); + } +}); + +/** Schotterwiese: magerer Kiesrasen, heller und offener als der Rasen. */ +export const schotterwiese = () => make('schotterwiese', 2.0, 0.98, (ctx) => { + ctx.fillStyle = 'rgb(139,146,104)'; + ctx.fillRect(0, 0, SIZE, SIZE); + noiseTile(ctx, 44, 20, 0.1); + const rnd = mulberry32(45); + for (let i = 0; i < 2600; i++) { + const v = 150 + rnd() * 70; + ctx.fillStyle = `rgba(${v | 0},${(v - 4) | 0},${(v - 18) | 0},0.8)`; + const r = 1 + rnd() * 2.4; + ctx.beginPath(); + ctx.arc(rnd() * SIZE, rnd() * SIZE, r, 0, Math.PI * 2); + ctx.fill(); + } + for (let i = 0; i < 1800; i++) { + const x = rnd() * SIZE, y = rnd() * SIZE; + ctx.strokeStyle = `rgba(112,138,64,0.5)`; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + (rnd() - 0.5) * 3, y - 3 - rnd() * 3); + ctx.stroke(); + } +}); + +/** Dekorkies in den Pflanzflaechen und um den Flammengrill. */ +export const dekorkies = () => make('dekorkies', 1.0, 0.99, (ctx) => { + ctx.fillStyle = 'rgb(150,144,132)'; + ctx.fillRect(0, 0, SIZE, SIZE); + const rnd = mulberry32(46); + for (let i = 0; i < 6500; i++) { + const v = 128 + rnd() * 90; + ctx.fillStyle = `rgb(${v | 0},${(v - 5) | 0},${(v - 16) | 0})`; + ctx.beginPath(); + ctx.ellipse(rnd() * SIZE, rnd() * SIZE, 2 + rnd() * 4, 2 + rnd() * 3, rnd() * 3.14, 0, Math.PI * 2); + ctx.fill(); + } + noiseTile(ctx, 47, 24, 0.07); +}); + +/** Pflanzbeet: dunkler Humus mit Rindenanteil. */ +export const beet = () => make('beet', 1.5, 0.99, (ctx) => { + ctx.fillStyle = 'rgb(74,58,44)'; + ctx.fillRect(0, 0, SIZE, SIZE); + noiseTile(ctx, 48, 20, 0.12); + const rnd = mulberry32(49); + for (let i = 0; i < 2200; i++) { + const v = 60 + rnd() * 60; + ctx.fillStyle = `rgb(${v | 0},${(v - 12) | 0},${(v - 24) | 0})`; + ctx.save(); + ctx.translate(rnd() * SIZE, rnd() * SIZE); + ctx.rotate(rnd() * 3.14); + ctx.fillRect(0, 0, 3 + rnd() * 9, 2 + rnd() * 3); + ctx.restore(); + } +}); + +/** Spielsand. */ +export const spielsand = () => make('spielsand', 1.5, 1.0, (ctx) => { + ctx.fillStyle = 'rgb(211,192,155)'; + ctx.fillRect(0, 0, SIZE, SIZE); + noiseTile(ctx, 50, 28, 0.05); + grain(ctx, 51, 0.16, 1); +}); + +// --------------------------------------------------------------------------- +// Bauteile +// --------------------------------------------------------------------------- + +/** Travertin-Mauerwerk in Schichten, wie auf den Referenzfotos des Plans. */ +export const travertinMauer = () => make('travertinMauer', 1.6, 0.7, (ctx) => { + const px = SIZE / 1.6; + ctx.fillStyle = 'rgb(168,152,126)'; + ctx.fillRect(0, 0, SIZE, SIZE); + const rnd = mulberry32(61); + const h = 0.2; + for (let row = 0; row < Math.round(1.6 / h); row++) { + let x = -((row % 2) * 0.3); + while (x < 1.6) { + const w = 0.4 + rnd() * 0.4; + const v = 202 + rnd() * 26; + ctx.fillStyle = `rgb(${v | 0},${(v - 17) | 0},${(v - 44) | 0})`; + ctx.fillRect(x * px + 1, row * h * px + 1, w * px - 2, h * px - 2); + x += w; + } + } + noiseTile(ctx, 62, 40, 0.045); + grain(ctx, 63, 0.05, 2); +}); + +/** Sichtbeton der Winkelelemente und der Nachbarmauer. */ +export const beton = () => make('beton', 2.0, 0.8, (ctx) => { + ctx.fillStyle = 'rgb(176,174,169)'; + ctx.fillRect(0, 0, SIZE, SIZE); + noiseTile(ctx, 64, 12, 0.05); + noiseTile(ctx, 65, 48, 0.025); + grain(ctx, 66, 0.04, 1); + // Schalungsstoesse + ctx.strokeStyle = 'rgba(120,118,114,0.35)'; + ctx.lineWidth = 1.5; + ctx.beginPath(); + ctx.moveTo(0, SIZE / 2); ctx.lineTo(SIZE, SIZE / 2); + ctx.moveTo(SIZE / 2, 0); ctx.lineTo(SIZE / 2, SIZE); + ctx.stroke(); + ctx.fillStyle = 'rgba(120,118,114,0.4)'; + for (const [x, y] of [[SIZE * 0.25, SIZE * 0.25], [SIZE * 0.75, SIZE * 0.25], [SIZE * 0.25, SIZE * 0.75], [SIZE * 0.75, SIZE * 0.75]]) { + ctx.beginPath(); ctx.arc(x, y, 3.5, 0, Math.PI * 2); ctx.fill(); + } +}); + +/** Gestapelte Holzscheiter der Scheiterwand hinter dem Flammengrill. */ +export const holzscheiter = () => make('holzscheiter', 1.0, 0.9, (ctx) => { + ctx.fillStyle = 'rgb(48,38,30)'; + ctx.fillRect(0, 0, SIZE, SIZE); + const rnd = mulberry32(67); + for (let i = 0; i < 260; i++) { + const r = 12 + rnd() * 16; + const x = rnd() * SIZE, y = rnd() * SIZE; + const v = 128 + rnd() * 74; + ctx.fillStyle = `rgb(${v | 0},${(v - 26) | 0},${(v - 56) | 0})`; + ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); + ctx.strokeStyle = 'rgba(60,44,32,0.75)'; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.strokeStyle = 'rgba(90,66,46,0.5)'; + ctx.lineWidth = 1; + for (let k = 1; k <= 2; k++) { + ctx.beginPath(); ctx.arc(x, y, (r * k) / 3, 0, Math.PI * 2); ctx.stroke(); + } + } +}); + +/** Vertikale Holzschalung der Fassade - graubraun vergraut, wie auf den Fotos. */ +export const fassadeHolz = () => make('fassadeHolz', 1.2, 0.78, (ctx) => { + const px = SIZE / 1.2; + ctx.fillStyle = 'rgb(124,114,100)'; + ctx.fillRect(0, 0, SIZE, SIZE); + const rnd = mulberry32(71); + const w = 0.06; + for (let col = 0; col < Math.round(1.2 / w); col++) { + const v = 152 + rnd() * 32; + ctx.fillStyle = `rgb(${v | 0},${(v - 8) | 0},${(v - 20) | 0})`; + ctx.fillRect(col * w * px + 1.5, 0, w * px - 3, SIZE); + ctx.strokeStyle = 'rgba(84,76,66,0.45)'; + ctx.lineWidth = 1; + for (let i = 0; i < 14; i++) { + const y = rnd() * SIZE; + ctx.beginPath(); + ctx.moveTo(col * w * px + 2, y); + ctx.lineTo(col * w * px + w * px - 3, y + (rnd() - 0.5) * 5); + ctx.stroke(); + } + } + noiseTile(ctx, 72, 32, 0.03); +}); + +/** Heller Betonsockel unter der Holzschalung. */ +export const sockel = () => make('sockel', 2.0, 0.72, (ctx) => { + ctx.fillStyle = 'rgb(214,211,204)'; + ctx.fillRect(0, 0, SIZE, SIZE); + noiseTile(ctx, 73, 16, 0.035); + grain(ctx, 74, 0.03, 1); +}); + +/** Extensive Dachbegruenung der Flachdaecher. */ +export const gruendach = () => make('gruendach', 2.0, 1.0, (ctx) => { + ctx.fillStyle = 'rgb(108,116,72)'; + ctx.fillRect(0, 0, SIZE, SIZE); + noiseTile(ctx, 75, 18, 0.13); + const rnd = mulberry32(76); + for (let i = 0; i < 2800; i++) { + const v = rnd(); + ctx.fillStyle = `rgba(${(96 + v * 80) | 0},${(112 + v * 60) | 0},${(58 + v * 40) | 0},0.75)`; + ctx.beginPath(); + ctx.arc(rnd() * SIZE, rnd() * SIZE, 1.5 + rnd() * 3.5, 0, Math.PI * 2); + ctx.fill(); + } +}); + +/** Dunkler Spritzschutz-Kiesstreifen entlang der Fassade (siehe Fotos). */ +export const spritzschutz = () => make('spritzschutz', 1.0, 0.98, (ctx) => { + ctx.fillStyle = 'rgb(62,62,66)'; + ctx.fillRect(0, 0, SIZE, SIZE); + const rnd = mulberry32(77); + for (let i = 0; i < 4200; i++) { + const v = 52 + rnd() * 78; + ctx.fillStyle = `rgb(${v | 0},${v | 0},${(v + 6) | 0})`; + ctx.beginPath(); + ctx.ellipse(rnd() * SIZE, rnd() * SIZE, 2 + rnd() * 4, 2 + rnd() * 3, rnd() * 3.14, 0, Math.PI * 2); + ctx.fill(); + } +}); diff --git a/src/scene/vegetation.ts b/src/scene/vegetation.ts new file mode 100644 index 0000000..55f4903 --- /dev/null +++ b/src/scene/vegetation.ts @@ -0,0 +1,225 @@ +/** + * Bepflanzung: Solitaergehoelze, Eibenhecke `Hilli`, Beerenstraeucher, + * Graeser, Kraeuter und die Ligusterhecke des Nachbargartens. + * + * Der Stil ist bewusst stilisiert: klare Volumen mit leichter Unregelmaessigkeit + * statt Blattgeometrie - das bleibt auch auf dem Handy fluessig. + */ +import { + BoxGeometry, BufferAttribute, Color, CylinderGeometry, Group, IcosahedronGeometry, + InstancedMesh, Matrix4, Mesh, MeshStandardMaterial, Object3D, Quaternion, Vector3, + type BufferGeometry, +} from 'three'; +import type { Plant, Tree } from '../site'; +import { boxGeometry } from './geometry'; + +function rng(seed: number) { + let a = seed >>> 0; + return () => { + a = (a + 0x9e3779b9) >>> 0; + let t = Math.imul(a ^ (a >>> 16), 0x21f0aaad); + t = Math.imul(t ^ (t >>> 15), 0x735a2d97); + return ((t ^ (t >>> 15)) >>> 0) / 4294967296; + }; +} + +/** + * Kugel mit verrauschter Oberflaeche - Grundform aller Kronen und Polster. + * `stretchY` staucht oder streckt die Form bereits in der Geometrie. Das ist + * wichtig: eine ungleichmaessige Instanz-Skalierung verzerrt die Normalen und + * erzeugt harte, glaenzende Kanten. + */ +function blob(radius: number, detail: number, roughness: number, seed: number, stretchY = 1): BufferGeometry { + const g = new IcosahedronGeometry(radius, detail); + const pos = g.getAttribute('position') as BufferAttribute; + const r = rng(seed); + const v = new Vector3(); + for (let i = 0; i < pos.count; i++) { + v.fromBufferAttribute(pos, i); + const f = 1 + (r() - 0.5) * 2 * roughness; + v.multiplyScalar(f); + pos.setXYZ(i, v.x, v.y * stretchY, v.z); + } + g.computeVertexNormals(); + return g; +} + +const LAUB = { + parrotia: 0x76944f, + apfel: 0x7e9c52, + eibe: 0x466b3a, + strauch: 0x6b8f45, + gras: 0xa3b45c, + kraut: 0x8ba455, + liguster: 0x557a37, +}; + +function foliage(color: number, rough = 0.92, flat = false) { + return new MeshStandardMaterial({ color, roughness: rough, metalness: 0, flatShading: flat }); +} + +const RINDE = new MeshStandardMaterial({ color: 0x7d6d59, roughness: 0.9 }); + +/** Solitaergehoelz oder Baum: mehrstaemmig, mit gestapelten Kronenblasen. */ +function buildTree(t: Tree, y: number): Group { + const g = new Group(); + g.name = t.name; + const r = rng(Math.round((t.x + 100) * 37 + (t.z + 100) * 91)); + const crownR = Math.max(0.9, t.crown); + const trunkH = t.height * 0.42; + + const stems = t.species === 'parrotia' ? 3 : 1; + for (let s = 0; s < stems; s++) { + const lean = stems > 1 ? 0.13 : 0; + const ox = (r() - 0.5) * 2 * lean * trunkH; + const oz = (r() - 0.5) * 2 * lean * trunkH; + const geo = new CylinderGeometry(0.05 + r() * 0.015, 0.075 + r() * 0.02, trunkH, 9, 1); + const m = new Mesh(geo, RINDE); + m.position.set(t.x + ox / 2, y + trunkH / 2, t.z + oz / 2); + m.rotation.z = ox / trunkH; + m.rotation.x = -oz / trunkH; + m.castShadow = true; + g.add(m); + } + + // Krone aus mehreren, leicht unterschiedlich getoenten Blasen + const blobs = 5 + Math.round(r() * 2); + for (let i = 0; i < blobs; i++) { + const rr = crownR * (0.6 + r() * 0.35); + const geo = blob(rr, 2, 0.07, i * 17 + 5); + const shade = 1 - r() * 0.16; + const mat = foliage( + new Color(LAUB[t.species]).multiplyScalar(shade).getHex(), + 0.94, + ); + const m = new Mesh(geo, mat); + const a = (i / blobs) * Math.PI * 2 + r(); + const rad = crownR * 0.3 * (i === 0 ? 0 : 1); + m.position.set( + t.x + Math.cos(a) * rad, + y + trunkH + crownR * (0.4 + r() * 0.45), + t.z + Math.sin(a) * rad, + ); + m.scale.y = 0.86; + m.castShadow = true; + m.receiveShadow = true; + g.add(m); + } + return g; +} + +/** Baut eine InstancedMesh aus Transformationen. */ +function instances( + geo: BufferGeometry, mat: MeshStandardMaterial, + items: { x: number; y: number; z: number; s: number; sy?: number; rot?: number }[], + name: string, +): InstancedMesh | null { + if (!items.length) return null; + const mesh = new InstancedMesh(geo, mat, items.length); + const m = new Matrix4(); + const q = new Quaternion(); + const pos = new Vector3(); + const scale = new Vector3(); + const dummy = new Object3D(); + items.forEach((it, i) => { + pos.set(it.x, it.y, it.z); + dummy.rotation.set(0, it.rot ?? 0, 0); + q.setFromEuler(dummy.rotation); + scale.set(it.s, it.sy ?? it.s, it.s); + m.compose(pos, q, scale); + mesh.setMatrixAt(i, m); + }); + mesh.instanceMatrix.needsUpdate = true; + mesh.castShadow = true; + mesh.receiveShadow = true; + mesh.name = name; + return mesh; +} + +export function buildVegetation( + plants: Plant[], + trees: Tree[], + height: (x: number, z: number) => number, +): Group { + const group = new Group(); + group.name = 'Bepflanzung'; + + for (const t of trees) group.add(buildTree(t, height(t.x, t.z))); + + const r = rng(4242); + + // --- Eiben `Hilli` 80/100 cm: aufrechte, dichte Polster ------------------ + // Plan: Pflanzhoehe 80/100 cm, Breite rund 45 cm. Die Streckung steckt in + // der Geometrie, die Instanzen werden nur gleichmaessig skaliert. + const eiben = plants.filter((p) => p.type === 'eibe' && p.x !== undefined); + const eibeGeo = blob(1, 2, 0.028, 3, 1.9); + const eibeMesh = instances(eibeGeo, foliage(LAUB.eibe, 0.96), eiben.map((p) => { + const s = Math.max(0.22, (p.r ?? 0.2) * 1.1) * (0.92 + r() * 0.18); + return { x: p.x!, y: height(p.x!, p.z!) + s * 1.9, z: p.z!, s, rot: r() * 6.28 }; + }), 'Eiben `Hilli`'); + if (eibeMesh) group.add(eibeMesh); + + // --- Beerenstraeucher und Heidelbeeren ---------------------------------- + const straeucher = plants.filter((p) => p.type === 'strauch' && p.x !== undefined); + const strauchMesh = instances(blob(1, 2, 0.06, 7, 0.8), foliage(LAUB.strauch), straeucher.map((p) => { + const s = (p.r ?? 0.35) * 1.1; + return { x: p.x!, y: height(p.x!, p.z!) + s * 0.75, z: p.z!, s, rot: r() * 6.28 }; + }), 'Beerensträucher'); + if (strauchMesh) group.add(strauchMesh); + + // --- Graeser: Buendel schmaler, leicht geneigter Halme ------------------- + const graeser = plants.filter((p) => p.type === 'gras' && p.x !== undefined); + if (graeser.length) { + // Ein Halm als Einheitsgeometrie mit Fusspunkt im Ursprung; Neigung und + // Position kommen aus der Instanzmatrix, nicht aus einer Mesh-Rotation. + const blade = new BoxGeometry(0.022, 1, 0.022); + blade.translate(0, 0.5, 0); + const mesh = new InstancedMesh(blade, foliage(LAUB.gras, 0.98), graeser.length * 9); + const dummy = new Object3D(); + let k = 0; + for (const p of graeser) { + const y = height(p.x!, p.z!); + for (let i = 0; i < 9; i++) { + const a = r() * Math.PI * 2; + const rad = (p.r ?? 0.12) * r() * 0.9; + dummy.position.set(p.x! + Math.cos(a) * rad, y, p.z! + Math.sin(a) * rad); + dummy.rotation.set((r() - 0.5) * 0.45, r() * Math.PI, (r() - 0.5) * 0.45); + dummy.scale.set(1, 0.42 + r() * 0.4, 1); + dummy.updateMatrix(); + mesh.setMatrixAt(k++, dummy.matrix); + } + } + mesh.count = k; + mesh.instanceMatrix.needsUpdate = true; + mesh.castShadow = true; + mesh.name = 'Gräser'; + group.add(mesh); + } + + // --- Kraeuter und Stauden ----------------------------------------------- + const kraeuter = plants.filter((p) => p.type === 'kraut' && p.x !== undefined); + const krautMesh = instances(blob(1, 2, 0.10, 11, 0.7), foliage(LAUB.kraut, 0.98), kraeuter.map((p) => { + const rr = Math.max(0.10, (p.r ?? 0.14) * 0.85); + return { x: p.x!, y: height(p.x!, p.z!) + rr * 0.5, z: p.z!, s: rr, rot: r() * 6.28 }; + }), 'Kräuter'); + if (krautMesh) group.add(krautMesh); + + // --- Ligusterhecke, Hoehe 170 cm, Breite 75 cm -------------------------- + for (const p of plants) { + if (p.type !== 'hecke' || !p.polygon) continue; + const poly = p.polygon; + let x0 = Infinity, z0 = Infinity, x1 = -Infinity, z1 = -Infinity; + for (const [x, z] of poly) { x0 = Math.min(x0, x); x1 = Math.max(x1, x); z0 = Math.min(z0, z); z1 = Math.max(z1, z); } + const cx = (x0 + x1) / 2, cz = (z0 + z1) / 2; + const y = height(cx, cz); + const h = p.height ?? 1.7; + const geo = boxGeometry(cx, cz, x1 - x0, z1 - z0, y, y + h, 0, 0.8); + const m = new Mesh(geo, foliage(LAUB.liguster, 0.97)); + m.castShadow = true; + m.receiveShadow = true; + m.name = 'Ligusterhecke'; + group.add(m); + } + + return group; +} diff --git a/src/site.ts b/src/site.ts new file mode 100644 index 0000000..22a2f1d --- /dev/null +++ b/src/site.ts @@ -0,0 +1,179 @@ +/** + * Datenmodell der Szene. Entspricht public/data/site.json, das aus dem + * Gartenplan-PDF erzeugt wird (siehe tools/build-site.mjs). + * + * Koordinaten durchwegs in Metern im Site-Frame: + * x = Hausachse ENE (Peilung 61.16 Grad), z = senkrecht SSE, y = Hoehe + * y = 0 entspricht dem Fixpunkt 651.17 m ue. M. (OK Fertigboden EG Haus 9a) + */ + +export type Vec2 = [number, number]; + +export type SurfaceType = + | 'travertin80' | 'travertin4060' | 'travertinstell' | 'keramik' + | 'verbundstein' | 'tegula' | 'rasen' | 'schotterwiese' | 'beet' | 'spielsand'; + +export interface Surface { + type: SurfaceType; + area: number; + polygon: Vec2[]; +} + +export interface Plant { + type: 'eibe' | 'strauch' | 'gras' | 'kraut' | 'hecke'; + x?: number; + z?: number; + r?: number; + kind?: string; + height?: number; + width?: number; + polygon?: Vec2[]; +} + +export interface Tree { + name: string; + x: number; + z: number; + crown: number; + height: number; + species: 'parrotia' | 'apfel'; + neighbour?: boolean; +} + +export interface Lamp { + x: number; + z: number; + kind: 'poller' | 'boden'; +} + +export interface Wall { + id: string; + type: 'travertin' | 'beton' | 'holzscheiter' | 'lamellen'; + note?: string; + path: Vec2[]; + t: number; + top: number | null; + base: number | null; + seating?: boolean; + openSlats?: boolean; + retains?: string; + crestProfile?: number[]; + footProfile?: number[]; +} + +export interface Prop { + id: string; + type: 'gartenhaus' | 'spielhaus' | 'flammengrill' | 'bank' | 'roboter' | 'wasserhahn' | 'schacht' | 'tor'; + x: number; + z: number; + w: number; + d: number; + h: number; + rot: number; + note?: string; +} + +export interface Stele { x: number; z: number; h: number } + +export interface Opening { + face: 'north' | 'south' | 'east' | 'west'; + a: number; + b: number; + sill: number; + head: number; + kind: 'glass' | 'window' | 'door' | 'slats'; + upper?: boolean; +} + +export interface Building { + id: string; + name: string; + floorLevel: number; + upperLevel: number; + roofLevel: number; + parapet: number; + outline: Vec2[]; + loggia: { x0: number; z0: number; x1: number; z1: number; ceiling: number }; + canopy: { a: number; b: number; depth: number; y: number; thickness: number }; + openings: Opening[]; +} + +export interface NeighbourBuilding { + id: string; + name: string; + outline: Vec2[]; + floorLevel: number; + roofLevel: number; +} + +export interface StartView { + id: string; + label: string; + x: number; + z: number; + yaw: number; + note?: string; +} + +export interface Terrain { + grid: { x0: number; z0: number; cell: number; nx: number; nz: number }; + heights: number[]; + zones: { id: string; label: string; polygon: Vec2[] }[]; + spots: { x: number; z: number; y: number; zone: string }[]; +} + +export interface SiteData { + meta: { + plan: string; + architecture: string; + generated: string; + originAbsoluteMasl: number; + location: { + name: string; + lat: number; + lon: number; + northBearingOfSiteX: number; + groundMasl: number; + }; + }; + terrain: Terrain; + surfaces: Surface[]; + plants: Plant[]; + trees: Tree[]; + lamps: Lamp[]; + walls: Wall[]; + props: Prop[]; + stelen: Stele[]; + building: Building; + neighbourBuildings: NeighbourBuilding[]; + startViews: StartView[]; +} + +export async function loadSite(url = './data/site.json'): Promise { + const res = await fetch(url); + if (!res.ok) throw new Error(`site.json konnte nicht geladen werden (${res.status})`); + return (await res.json()) as SiteData; +} + +/** + * Bilineare Hoeheninterpolation im Raster. Ausserhalb des Rasters wird der + * Randwert gehalten, damit die Szene nirgends ins Leere faellt. + */ +export function makeHeightSampler(terrain: Terrain): (x: number, z: number) => number { + const { x0, z0, cell, nx, nz } = terrain.grid; + const h = Float32Array.from(terrain.heights); + return (x: number, z: number) => { + const fi = (x - x0) / cell; + const fj = (z - z0) / cell; + const i = Math.max(0, Math.min(nx - 2, Math.floor(fi))); + const j = Math.max(0, Math.min(nz - 2, Math.floor(fj))); + const tx = Math.max(0, Math.min(1, fi - i)); + const tz = Math.max(0, Math.min(1, fj - j)); + const h00 = h[j * nx + i], h10 = h[j * nx + i + 1]; + const h01 = h[(j + 1) * nx + i], h11 = h[(j + 1) * nx + i + 1]; + return ( + h00 * (1 - tx) * (1 - tz) + h10 * tx * (1 - tz) + + h01 * (1 - tx) * tz + h11 * tx * tz + ); + }; +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..1d282b6 --- /dev/null +++ b/src/style.css @@ -0,0 +1,230 @@ +:root { + --ink: #1d241c; + --paper: rgba(250, 249, 245, 0.92); + --line: rgba(29, 36, 28, 0.14); + --accent: #6f8f3f; + --shadow: 0 6px 24px rgba(20, 28, 18, 0.22); + --radius: 12px; + font-family: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + height: 100%; + overflow: hidden; + background: #10150f; + color: var(--ink); + -webkit-tap-highlight-color: transparent; +} + +#view { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + display: block; + touch-action: none; + cursor: grab; +} + +/* -- Ladeanzeige ---------------------------------------------------------- */ + +#loader { + position: fixed; + inset: 0; + display: grid; + place-items: center; + background: linear-gradient(170deg, #3d5238, #1b2418 70%); + color: #f2f4ec; + z-index: 50; + transition: opacity 0.5s ease; +} + +#loader.done { opacity: 0; pointer-events: none; } + +.loader-inner { width: min(320px, 76vw); text-align: center; } +.loader-title { font-size: 1.05rem; font-weight: 600; letter-spacing: 0.01em; margin-bottom: 1rem; } +.loader-bar { height: 3px; background: rgba(255, 255, 255, 0.18); border-radius: 3px; overflow: hidden; } +.loader-bar span { display: block; height: 100%; width: 0; background: #b9d08a; transition: width 0.3s ease; } +.loader-note { margin-top: 0.7rem; font-size: 0.8rem; opacity: 0.72; } + +/* -- Bedienoberflaeche ---------------------------------------------------- */ + +#hud { position: fixed; inset: 0; pointer-events: none; z-index: 10; } +#hud button, #hud input, #hud a { pointer-events: auto; } + +#topbar { + position: absolute; + top: 0; left: 0; right: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.7rem clamp(0.7rem, 2vw, 1.3rem); + padding-top: max(0.7rem, env(safe-area-inset-top)); + background: linear-gradient(to bottom, rgba(12, 18, 10, 0.62), rgba(12, 18, 10, 0)); + color: #f4f6ee; +} + +.brand { display: flex; flex-direction: column; line-height: 1.32; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); } +.brand strong { font-size: 0.95rem; font-weight: 600; } +.brand span { font-size: 0.72rem; opacity: 0.8; } + +.topbar-actions { display: flex; gap: 0.5rem; } + +#hud button { + font: inherit; + font-size: 0.78rem; + padding: 0.42rem 0.85rem; + border: 1px solid rgba(255, 255, 255, 0.28); + border-radius: 999px; + background: rgba(14, 20, 12, 0.45); + color: #f4f6ee; + cursor: pointer; + backdrop-filter: blur(6px); + transition: background 0.15s ease, border-color 0.15s ease; +} +#hud button:hover { background: rgba(30, 42, 24, 0.7); border-color: rgba(255, 255, 255, 0.5); } +#hud button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; } + +#views { + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: max(0.8rem, env(safe-area-inset-bottom)); + display: flex; + gap: 0.4rem; + padding: 0 0.6rem; + max-width: 96vw; + overflow-x: auto; + scrollbar-width: none; +} +#views::-webkit-scrollbar { display: none; } +#views button { white-space: nowrap; } + +#timebar { + position: absolute; + right: clamp(0.7rem, 2vw, 1.3rem); + top: 4.2rem; + width: min(230px, 46vw); + display: grid; + gap: 0.15rem; + padding: 0.7rem 0.85rem; + border-radius: var(--radius); + background: rgba(14, 20, 12, 0.42); + backdrop-filter: blur(8px); + color: #eef1e6; + font-size: 0.72rem; + box-shadow: var(--shadow); +} +#timebar label { display: flex; justify-content: space-between; opacity: 0.85; } +#timebar output { font-variant-numeric: tabular-nums; font-weight: 600; } +#timebar input { width: 100%; accent-color: var(--accent); margin: 0 0 0.45rem; } + +#crosshair { + position: absolute; + left: 50%; top: 50%; + width: 5px; height: 5px; + margin: -2.5px 0 0 -2.5px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.55); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35); + opacity: 0; + transition: opacity 0.2s ease; +} +#crosshair.on { opacity: 1; } + +#hint { + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: 3.4rem; + padding: 0.4rem 0.9rem; + border-radius: 999px; + background: rgba(14, 20, 12, 0.5); + color: #eef1e6; + font-size: 0.72rem; + white-space: nowrap; + backdrop-filter: blur(6px); + transition: opacity 0.4s ease; +} +#hint.fade { opacity: 0; } +#hint kbd { + font: inherit; + padding: 0.02em 0.34em; + margin: 0 0.06em; + border: 1px solid rgba(255, 255, 255, 0.35); + border-radius: 4px; +} +#hint .touch { display: none; } + +#joystick { + position: absolute; + left: 1.3rem; + bottom: max(4.6rem, calc(env(safe-area-inset-bottom) + 4.4rem)); + width: 116px; + height: 116px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(14, 20, 12, 0.3); + pointer-events: auto; + touch-action: none; +} +#joystick-knob { + position: absolute; + left: 50%; top: 50%; + width: 46px; height: 46px; + margin: -23px 0 0 -23px; + border-radius: 50%; + background: rgba(240, 244, 232, 0.7); + box-shadow: var(--shadow); +} + +#info { + position: absolute; + right: clamp(0.7rem, 2vw, 1.3rem); + top: 4.2rem; + width: min(390px, 88vw); + max-height: min(72vh, 640px); + overflow-y: auto; + padding: 1.1rem 1.25rem 1.3rem; + border-radius: var(--radius); + background: var(--paper); + box-shadow: var(--shadow); + pointer-events: auto; + font-size: 0.82rem; + line-height: 1.55; +} +#info h2 { margin: 0 0 0.6rem; font-size: 1rem; } +#info h3 { margin: 1.1rem 0 0.3rem; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.06em; color: #5d6a55; } +#info p { margin: 0 0 0.5rem; } +#info dl { display: grid; grid-template-columns: auto 1fr; gap: 0.15rem 0.8rem; margin: 0; } +#info dt { color: #5d6a55; } +#info dd { margin: 0; font-variant-numeric: tabular-nums; } +#info ul { margin: 0.2rem 0 0.6rem; padding-left: 1.1rem; } +#info li { margin-bottom: 0.15rem; } +#info-close { + position: absolute; + right: 0.55rem; top: 0.5rem; + width: 28px; height: 28px; + padding: 0 !important; + font-size: 1.1rem !important; + line-height: 1; + border-color: var(--line) !important; + background: transparent !important; + color: var(--ink) !important; +} + +@media (pointer: coarse) { + #hint .desktop { display: none; } + #hint .touch { display: inline; } + #timebar { top: auto; bottom: max(9.5rem, calc(env(safe-area-inset-bottom) + 9rem)); width: min(200px, 44vw); } + #hint { bottom: 8.2rem; font-size: 0.68rem; } + #views { bottom: max(0.6rem, env(safe-area-inset-bottom)); justify-content: flex-start; } +} + +@media (max-width: 560px) { + .brand span { display: none; } +} diff --git a/src/ui/hud.ts b/src/ui/hud.ts new file mode 100644 index 0000000..8c74c6b --- /dev/null +++ b/src/ui/hud.ts @@ -0,0 +1,203 @@ +/** + * Bedienoberflaeche: Standorte, Tageszeit, Modusumschaltung, Joystick, Infobox. + */ +import type { SiteData, StartView } from '../site'; +import type { SunState } from '../scene/environment'; +import { isTouch } from '../controls/walker'; + +export interface HudOptions { + site: SiteData; + onView(view: StartView): void; + onMode(mode: 'walk' | 'overview'): void; + onJoystick(x: number, y: number, active: boolean): void; +} + +export interface Hud { + time(): { hours: number; day: number }; + setMode(mode: 'walk' | 'overview'): void; + setPointerLocked(locked: boolean): void; + setSunReadout(sun: SunState): void; +} + +const MONTHS = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; + +function dayLabel(dayOfYear: number): string { + const d = new Date(2026, 0, 1); + d.setDate(dayOfYear); + return `${d.getDate()}. ${MONTHS[d.getMonth()]}`; +} + +function timeLabel(hours: number): string { + const h = Math.floor(hours); + const m = Math.round((hours - h) * 60); + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; +} + +export function setupHud(opts: HudOptions): Hud { + const { site } = opts; + const $ = (id: string) => document.getElementById(id) as T; + + const timeInput = $('time'); + const timeOut = $('time-out'); + const seasonInput = $('season'); + const seasonOut = $('season-out'); + const btnMode = $('btn-mode'); + const btnInfo = $('btn-info'); + const info = $('info'); + const crosshair = $('crosshair'); + const hint = $('hint'); + + // -- Standorte ----------------------------------------------------------- + const nav = $('views'); + for (const v of site.startViews) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.textContent = v.label; + if (v.note) btn.title = v.note; + btn.addEventListener('click', () => { + for (const other of nav.querySelectorAll('button')) other.removeAttribute('aria-pressed'); + btn.setAttribute('aria-pressed', 'true'); + opts.onView(v); + }); + nav.append(btn); + } + nav.querySelector('button')?.setAttribute('aria-pressed', 'true'); + + // -- Tageszeit ----------------------------------------------------------- + const syncTime = () => { + timeOut.value = timeLabel(+timeInput.value); + seasonOut.value = dayLabel(+seasonInput.value); + }; + timeInput.addEventListener('input', syncTime); + seasonInput.addEventListener('input', syncTime); + syncTime(); + + // -- Modus --------------------------------------------------------------- + let mode: 'walk' | 'overview' = 'walk'; + const setMode = (next: 'walk' | 'overview') => { + mode = next; + btnMode.textContent = mode === 'walk' ? 'Übersicht' : 'Rundgang'; + btnMode.setAttribute('aria-pressed', String(mode === 'overview')); + hint.style.display = mode === 'overview' ? 'none' : ''; + opts.onMode(mode); + }; + btnMode.addEventListener('click', () => setMode(mode === 'walk' ? 'overview' : 'walk')); + + // -- Infobox ------------------------------------------------------------- + const totals = new Map(); + for (const s of site.surfaces) totals.set(s.type, (totals.get(s.type) ?? 0) + s.area); + const NAMES: Record = { + travertin80: 'Travertinplatten 80×80×3 cm', + travertin4060: 'Travertinplatten 40×60×3 cm', + travertinstell: 'Travertinstellplatten', + keramik: 'Keramikplatten 60×60×2 cm', + verbundstein: 'Verbundsteine (Nachbar)', + tegula: 'Tegula-Pflastersteine grau', + rasen: 'Rasen', + schotterwiese: 'Schotterwiese', + beet: 'Pflanzbeete', + spielsand: 'Spielsand', + }; + const heights = site.terrain.spots.filter((s) => s.zone === 'garten').map((s) => s.y); + const eibenCount = site.plants.filter((p) => p.type === 'eibe').length; + + $('info-body').innerHTML = ` +

Alle Flächen, Mauern und Pflanzstandorte sind massstäblich aus dem Gartenplan + übernommen. Die Vektorgeometrie wurde direkt aus dem Plan-PDF (1:100) extrahiert, + das Gelände aus den ${site.terrain.spots.length} Höhenkoten interpoliert.

+ +

Grundlagen

+
+
Gartenplan
${site.meta.plan}
+
Architektur
${site.meta.architecture}
+
Standort
${site.meta.location.name}
+
Fixpunkt 0.00
${site.meta.location.groundMasl.toFixed(2)} m ü. M. (OK FB EG)
+
Höhen im Garten
${Math.min(...heights).toFixed(2)} bis ${Math.max(...heights).toFixed(2)} m
+
+ +

Beläge

+
${[...totals.entries()] + .sort((a, b) => b[1] - a[1]) + .map(([k, v]) => `
${NAMES[k] ?? k}
${v.toFixed(1)} m²
`) + .join('')}
+ +

Bepflanzung und Ausstattung

+
    +
  • ${site.trees.length} Solitärgehölze und Bäume, inkl. Apfelbaum
  • +
  • ${eibenCount} Eiben „Hilli“ 80/100 cm als Heckenpflanzung
  • +
  • Ligusterhecke 170 cm hoch, 75 cm breit
  • +
  • ${site.walls.length} Mauern und Wände, ${site.stelen.length} Travertinstelen
  • +
  • ${site.lamps.length} Leuchten – sie schalten sich abends automatisch ein
  • +
  • Gartenhaus 1,8 × 1,8 m, Spielhaus, Flammengrill mit Holzscheiterwand
  • +
+ +

Bedienung

+
    +
  • Rundgang: Klicken, dann W A S D, Shift zum Rennen
  • +
  • Am Handy: links der Joystick, rechts wischen zum Umsehen
  • +
  • Übersicht: ziehen zum Drehen, Mausrad zum Zoomen
  • +
  • Die Sonne steht für Abtwil SG astronomisch korrekt
  • +
+

+ `; + + const toggleInfo = () => { + info.hidden = !info.hidden; + btnInfo.setAttribute('aria-pressed', String(!info.hidden)); + }; + btnInfo.addEventListener('click', toggleInfo); + $('info-close').addEventListener('click', toggleInfo); + + // -- Joystick ------------------------------------------------------------ + if (isTouch()) { + const pad = $('joystick'); + const knob = $('joystick-knob'); + let id: number | null = null; + const radius = 36; + const reset = () => { + knob.style.transform = ''; + opts.onJoystick(0, 0, false); + id = null; + }; + pad.addEventListener('touchstart', (e) => { + e.preventDefault(); + id = e.changedTouches[0].identifier; + }, { passive: false }); + pad.addEventListener('touchmove', (e) => { + e.preventDefault(); + for (const t of Array.from(e.changedTouches)) { + if (t.identifier !== id) continue; + const r = pad.getBoundingClientRect(); + let dx = t.clientX - (r.left + r.width / 2); + let dy = t.clientY - (r.top + r.height / 2); + const len = Math.hypot(dx, dy); + if (len > radius) { dx = (dx / len) * radius; dy = (dy / len) * radius; } + knob.style.transform = `translate(${dx}px, ${dy}px)`; + opts.onJoystick(dx / radius, dy / radius, true); + } + }, { passive: false }); + pad.addEventListener('touchend', reset, { passive: true }); + pad.addEventListener('touchcancel', reset, { passive: true }); + } + + window.setTimeout(() => hint.classList.add('fade'), 9000); + + return { + time: () => ({ + hours: Math.round(+timeInput.value * 4) / 4, + day: +seasonInput.value, + }), + setMode, + setPointerLocked(locked) { + crosshair.classList.toggle('on', locked); + if (locked) hint.classList.add('fade'); + }, + setSunReadout(sun) { + const el = document.getElementById('sun-readout'); + if (!el) return; + el.textContent = sun.altitude > 0 + ? `Sonnenstand: ${sun.altitude.toFixed(0)}° über dem Horizont, Azimut ${sun.azimuth.toFixed(0)}°.` + : 'Die Sonne steht unter dem Horizont – die Gartenbeleuchtung ist eingeschaltet.'; + }, + }; +} diff --git a/tools/authoring.mjs b/tools/authoring.mjs new file mode 100644 index 0000000..2bc6eb0 --- /dev/null +++ b/tools/authoring.mjs @@ -0,0 +1,316 @@ +/** + * Handgepflegte Ergaenzungen zum Plan. + * + * Der PDF-Plan liefert Flaechen, Pflanzensymbole und Hoehenkoten. Alles, was + * im Plan nur beschriftet ist (Mauerhoehen, Bauteile, Gebaeude), steht hier - + * abgeleitet aus den Planbeschriftungen, den Architekturplaenen von EHLE + * Architekten (Haus 9a, 27.04.2021) und den Fotos des Ist-Zustands. + * + * Alle Koordinaten im Site-Frame in Metern: + * X = Hausachse ENE (Peilung 61.16 deg) + * Z = senkrecht SSE (Peilung 151.16 deg) + * Y = Hoehe relativ zum Fixpunkt 0.00 = 651.17 m ue. M. (= OK FB EG Haus 9a) + */ + +/** Farbe im Plan -> Belagsart. Reihenfolge egal, Zuordnung ueber Hex. */ +export const SURFACE_BY_COLOR = { + '#ffffcc': 'travertin80', // Travertinplatten 80x80x3, Hauptsitzplatz West + '#fcffe7': 'travertin4060', // Travertinplatten 40x60x3 mit Dekorkies + Keramikterrasse Zollet + '#f8ffcc': 'travertinstell',// Travertinstellplatten-Streifen + '#eee8cc': 'travertinmauer',// Mauerkronen, Sitzmauern, schwebende Stufen + '#e5ffe5': 'rasen', + '#ccffcc': 'rasen', + '#ccff99': 'schotterwiese', + '#eeeeee': 'keramik', // Keramikplatten 60x60x2 (bauseits vorhanden) + '#bbbbbb': 'verbundstein', // Anpassung Verbundsteine Nachbar + '#ddba99': 'beet', + '#c0bab4': 'spielsand', + '#ffaa00': 'tegula', // Tegula-Pflastersteine grau (im Plan orange markiert) + '#ddd299': 'travertinstell', + '#73e900': 'ligusterhecke', + '#cccccc': 'winkelelement', + '#aaaaaa': 'spielhaus', + '#b29f3c': 'spielhaus', + '#80712a': 'gartenhaus', + '#335533': 'roboter', +}; + +/** Pflanzensymbole: Planfarbe -> Art. Radius kommt aus dem Plankreis. */ +export const PLANT_BY_COLOR = { + '#00bb00': 'eibe', // Eiben `Hilli` 80/100 cm - Heckenpflanzen und ein Solitaer + '#66ff33': 'solitaer', + '#33ff99': 'solitaer', + '#cc5599': 'solitaer', // Parrotia persica, persischer Eisenholzbaum + '#99ff66': 'strauch', // Heidelbeeren / Beerenstraeucher + '#7fff00': 'gras', + '#99ff00': 'kraut', + '#cbe900': 'kraut', + '#74ff63': 'kraut', + '#e8ff64': 'kraut', + '#59b500': 'kraut', + '#0c8e2d': 'kraut', + '#00e93c': 'kraut', + '#33ff66': 'kraut', + '#339966': 'kraut', + '#00dbdd': 'kraut', + '#ccccff': 'kraut', + '#ff66cc': 'kraut', + '#7863ff': 'kraut', +}; + +/** + * Solitaergehoelze und Baeume. Die Kronenradien stammen aus den Plankreisen, + * die Stammhoehen sind Erfahrungswerte fuer die im Plan gezeigten Groessen. + */ +export const TREES = [ + { name: 'Solitärgehölz Sitzplatz Süd', x: -17.5, z: 1.6, crown: 1.34, height: 4.6, species: 'parrotia' }, + { name: 'Solitärgehölz Terrasse NW', x: -19.1, z: -5.5, crown: 0.67, height: 3.2, species: 'parrotia' }, + { name: 'Solitärgehölz Flammengrill', x: -4.0, z: 3.9, crown: 0.63, height: 3.0, species: 'parrotia' }, + { name: 'Apfelbaum', x: 1.69, z: 4.05, crown: 0.9, height: 3.4, species: 'apfel' }, + // Nachbargarten Zollet, im Plan mitgezeichnet + { name: 'Solitärgehölz Zollet', x: -17.6, z: -10.2, crown: 1.34, height: 4.6, species: 'parrotia', neighbour: true }, +]; + +/** + * Mauern, Stuetzelemente und Waende. + * `path` = Mittellinie, `t` = Dicke, `top`/`base` = Hoehe der Krone bzw. des Fusses + * (null = folgt dem Gelaende). Alle Werte aus den Planbeschriftungen und Koten. + */ +export const WALLS = [ + { + id: 'travertinmauer-nord', type: 'travertin', + note: 'Travertinmauer an der Grenze zum Nachbargrundstueck Zollet, stuetzt rund 1.5 m ab', + path: [[-18.17, -6.895], [-13.82, -6.895]], t: 0.55, top: -0.30, base: -1.90, + }, + { + id: 'sitzmauer-nord', type: 'travertin', + note: 'Sitzmauer 25 cm ueber der Travertinmauer (gelbe Kote "25" im Plan)', + path: [[-20.08, -6.47], [-13.82, -6.47]], t: 0.30, top: -0.03, base: -0.45, + }, + { + id: 'winkelelemente-west', type: 'beton', + note: 'Winkelelemente aus Beton entlang der Westgrenze, stuetzen den Garten ueber der Feuerwehrzufahrt', + path: [[-20.12, -6.62], [-20.66, 0.51], [-21.70, 0.52], [-21.70, 2.37]], + t: 0.22, top: null, base: null, retains: 'west', + }, + { + id: 'travertinsitzmauer-west', type: 'travertin', + note: 'Travertinsitzmauer als Abschluss des Hauptsitzplatzes gegen den Rasen', + path: [[-18.64, 0.755], [-14.73, 0.755]], t: 0.41, top: 0.40, base: -0.45, seating: true, + }, + { + id: 'travertinsitzmauer-sued', type: 'travertin', + note: 'Schenkel der Travertinsitzmauer laengs des Rasens', + path: [[-18.44, 0.55], [-18.44, 3.94]], t: 0.40, top: 0.40, base: -0.40, seating: true, + }, + { + id: 'betonmauer-nachbar', type: 'beton', + note: 'Bestehende Betonmauer des Suednachbarn entlang der Grenze', + path: [[-17.41, 6.07], [1.99, 6.07]], t: 0.20, top: 1.10, base: -0.60, + }, + { + id: 'betonmauer-nachbar-ost', type: 'beton', + note: 'Rueckspringender Schenkel der Nachbarmauer', + path: [[2.09, 6.07], [2.09, 9.05]], t: 0.20, top: 1.10, base: -0.60, + }, + { + id: 'holzscheiterwand', type: 'holzscheiter', + note: 'Holzscheiterwand 150 x 200 cm hinter dem Flammengrill', + path: [[-5.75, 4.35], [-4.25, 4.35]], t: 0.45, top: 1.75, base: 0.26, + }, + { + id: 'lamellenwand', type: 'lamellen', + note: 'Lamellenwand zum Sitzen: senkrechte Holzlamellen am gedeckten Sitzplatz', + path: [[-13.26, 0.91], [-8.73, 0.91]], t: 0.34, top: 2.72, base: -0.05, openSlats: true, + }, +]; + +/** Freistehende Bauteile und Moeblierung. rot = Drehung um Y in Grad. */ +export const PROPS = [ + { id: 'gartenhaus', type: 'gartenhaus', x: 1.20, z: -3.60, w: 1.8, d: 1.8, h: 2.15, rot: 0, + note: 'Standort Gartenhaus 1,8 x 1,8 m' }, + { id: 'spielhaus', type: 'spielhaus', x: 0.0, z: 3.15, w: 1.8, d: 2.7, h: 2.30, rot: 0, + note: 'Spielhaus durch Zimmermann, Podest auf 1.47' }, + { id: 'flammengrill', type: 'flammengrill', x: -6.95, z: 2.45, w: 0.7, d: 0.7, h: 0.85, rot: 0, + note: 'Flammengrill im Dekorkiesfeld' }, + { id: 'baenkli', type: 'bank', x: -20.62, z: 1.42, w: 0.50, d: 1.50, h: 0.45, rot: 90, + note: 'Baenkli aus Travertin' }, + { id: 'maehroboter', type: 'roboter', x: -17.87, z: 3.72, w: 0.75, d: 0.55, h: 0.40, rot: 0, + note: 'Garage Maehroboter' }, + { id: 'wasserhahn', type: 'wasserhahn', x: -13.90, z: -4.60, w: 0.12, d: 0.12, h: 0.85, rot: 0 }, + { id: 'schlaufschacht', type: 'schacht', x: -14.35, z: -3.95, w: 0.30, d: 0.30, h: 0.02, rot: 0, + note: 'Schlaufschacht D = 30 cm' }, + { id: 'gartentor', type: 'tor', x: -21.60, z: 3.30, w: 1.00, d: 0.06, h: 1.10, rot: 0 }, +]; + +/** + * Flaechen, die der Plan nur als weisses Raster zeigt und die deshalb bei der + * Farbklassierung durchfallen - Masse aus den Planlinien. + */ +export const EXTRA_SURFACES = [ + { + type: 'keramik', + note: 'Gedeckter Sitzplatz 14.4 m2, Keramikplatten 60x60x2 cm', + polygon: [[-13.93, -3.06], [-8.73, -3.06], [-8.73, 0.86], [-13.93, 0.86]], + }, + { + type: 'travertin80', + note: 'Schwebende Stufe Travertin, obere Tritthoehe -0.17', + polygon: [[-14.74, -2.69], [-13.93, -2.69], [-13.93, 0.56], [-14.74, 0.56]], + }, + { + type: 'travertin80', + note: 'Schwebende Stufe Travertin, untere Tritthoehe -0.33', + polygon: [[-15.28, -3.08], [-14.48, -3.08], [-14.48, 0.12], [-15.28, 0.12]], + }, + { + type: 'travertin80', + note: 'Schwebende Stufen beim Gartentor an der Suedwestecke', + polygon: [[-21.45, 2.15], [-20.65, 2.15], [-20.66, 3.75], [-21.45, 3.75]], + }, + { + type: 'tegula', + note: 'Feuerwehrzufahrt und Zugangsweg, Tegula-Pflastersteine grau', + polygon: [[-25.60, -9.00], [-22.10, -9.00], [-22.10, 6.40], [-25.60, 6.40]], + }, +]; + +/** + * Travertinstelen: aufrechte Steinstelen als Raumteiler entlang der Beete. + * Positionen aus den Planbeschriftungen "Travertinstelen". + */ +export const STELEN = [ + { x: -19.55, z: -3.10, h: 0.90 }, { x: -19.30, z: -2.30, h: 1.15 }, { x: -19.05, z: -1.55, h: 0.75 }, + { x: -13.30, z: 4.45, h: 1.05 }, { x: -12.60, z: 4.45, h: 0.80 }, { x: -10.30, z: 4.60, h: 1.10 }, + { x: -9.60, z: 4.60, h: 0.85 }, { x: -1.30, z: 4.55, h: 1.00 }, { x: -0.60, z: 4.55, h: 0.75 }, +]; + +/** + * Gebaeude. Das Wohnhaus 9a ist massgetreu aus dem Gartenplan uebernommen, + * die Hoehen stammen aus der Suedwest- und Nordostfassade 1:100: + * OK FB EG 651.17 -> 0.00 OK FB OG 654.07 -> +2.90 + * OK Attika 657.32 -> +6.15 Nachbar Nr. 9 OK FB EG 649.47 -> -1.70 + */ +export const BUILDING = { + id: 'haus-9a', + name: 'Haus 9a - Jeanine und Damian Kellenberger', + floorLevel: 0.0, + upperLevel: 2.9, + roofLevel: 6.15, + parapet: 0.35, + /** + * Aussenkante Fassade. Der Baukoerper ist im Grundriss ein Rechteck von + * 14.10 x 7.83 m; die Masse stammen aus den schwarzen Wandflaechen des + * Gartenplans (Nordostfassade z = -6.87, Suedwestfassade z = +0.96). + */ + outline: [[-13.82, -6.87], [0.28, -6.87], [0.28, 0.96], [-13.82, 0.96]], + /** + * Gedeckter Sitzplatz 14.4 m2: im Erdgeschoss zurueckspringende Nische in + * der Suedwestecke, oben vom Obergeschoss ueberdeckt. + */ + loggia: { x0: -13.82, z0: -3.06, x1: -9.06, z1: 0.96, ceiling: 2.75 }, + /** Vordach ueber dem Hauseingang an der Nordostfassade (siehe Foto). */ + canopy: { a: -3.20, b: 0.28, depth: 1.35, y: 2.60, thickness: 0.16 }, + openings: [ + // Suedwestfassade (z = 0.96 / 0.51) - grosse Fensterfronten zum Garten + { face: 'south', a: -4.40, b: -0.20, sill: 0.0, head: 2.45, kind: 'glass' }, + { face: 'south', a: -12.90, b: -9.40, sill: 0.0, head: 2.45, kind: 'glass' }, + { face: 'south', a: -3.90, b: -0.60, sill: 3.35, head: 5.00, kind: 'glass', upper: true }, + { face: 'south', a: -12.60, b: -9.70, sill: 3.35, head: 5.00, kind: 'glass', upper: true }, + // Westfassade (x = -13.82) - Fenster + { face: 'west', a: -5.84, b: -4.24, sill: 0.85, head: 2.35, kind: 'window' }, + { face: 'west', a: -5.60, b: -4.50, sill: 3.75, head: 5.00, kind: 'window', upper: true }, + // Nordostfassade (z = -6.87) - Eingang und Lamellenfenster + { face: 'north', a: -1.90, b: -0.90, sill: 0.0, head: 2.35, kind: 'door' }, + { face: 'north', a: -5.60, b: -3.90, sill: 0.90, head: 2.35, kind: 'window' }, + { face: 'north', a: -11.60, b: -10.20, sill: 0.90, head: 2.35, kind: 'slats' }, + { face: 'north', a: -5.40, b: -4.10, sill: 3.75, head: 5.10, kind: 'window', upper: true }, + { face: 'north', a: -11.40, b: -10.40, sill: 3.75, head: 5.10, kind: 'slats', upper: true }, + // Ostfassade (x = 0.28) + { face: 'east', a: -5.60, b: -4.30, sill: 0.90, head: 2.35, kind: 'window' }, + ], +}; + +/** + * Nachbargebaeude als Kontext-Volumen. Haus 9 (Zollet) haengt am gleichen + * Baukoerper, liegt aber 1.70 m tiefer und ist gegen Nordwesten versetzt. + */ +export const NEIGHBOUR_BUILDINGS = [ + { + id: 'haus-9', name: 'Haus 9 - Sharon und Alan Zollet', + outline: [[-15.60, -13.60], [-1.20, -13.60], [-1.20, -6.87], [-15.60, -6.87]], + floorLevel: -1.70, roofLevel: 4.45, + }, +]; + +/** + * Gelaendezonen. Innerhalb einer Zone wird zwischen den Hoehenkoten + * interpoliert; ueber Zonengrenzen hinweg nicht - dort stehen die Stuetzmauern. + */ +export const TERRAIN_ZONES = [ + { + id: 'garten', label: 'Garten Kellenberger', + polygon: [[-20.90, -6.60], [3.00, -6.60], [3.00, 5.95], [-20.90, 5.95]], + fallback: -0.05, + }, + { + id: 'zufahrt', label: 'Feuerwehrzufahrt / Schotterwiese West', + polygon: [[-26.00, -9.00], [-20.90, -9.00], [-20.90, 6.50], [-26.00, 6.50]], + fallback: -1.20, + }, + { + id: 'zollet', label: 'Nachbargarten Zollet', + polygon: [[-24.00, -14.50], [3.00, -14.50], [3.00, -6.60], [-24.00, -6.60]], + fallback: -1.70, + }, + { + id: 'sued', label: 'Nachbargrundstück Süd', + polygon: [[-22.00, 5.95], [6.00, 5.95], [6.00, 12.00], [-22.00, 12.00]], + fallback: -0.20, + }, + { + id: 'ost', label: 'Wiese Ost', + polygon: [[3.00, -6.60], [9.00, -6.60], [9.00, 5.95], [3.00, 5.95]], + fallback: 0.55, + }, +]; + +/** + * Zusaetzliche Hoehenpunkte, die im Plan nicht als Kote stehen, fuer ein + * plausibles Gelaende aber noetig sind (Auslauf gegen die Grundstuecksraender). + */ +export const EXTRA_SPOTS = [ + { x: -24.5, z: -8.0, y: -2.60, zone: 'zufahrt' }, + { x: -24.5, z: 0.0, y: -1.60, zone: 'zufahrt' }, + { x: -24.5, z: 5.0, y: -0.60, zone: 'zufahrt' }, + { x: -22.6, z: 5.5, y: -0.30, zone: 'zufahrt' }, + { x: -20.0, z: -12.5, y: -1.75, zone: 'zollet' }, + { x: -8.0, z: -12.5, y: -1.70, zone: 'zollet' }, + { x: 2.0, z: -11.5, y: -1.65, zone: 'zollet' }, + { x: -18.0, z: 9.0, y: -0.40, zone: 'sued' }, + { x: -6.0, z: 9.0, y: 0.10, zone: 'sued' }, + { x: 4.0, z: 9.0, y: 0.60, zone: 'sued' }, + { x: 7.5, z: -5.0, y: 0.10, zone: 'ost' }, + { x: 7.5, z: 0.0, y: 0.45, zone: 'ost' }, + { x: 7.5, z: 5.0, y: 0.80, zone: 'ost' }, +]; + +/** Startposition und Blickrichtung beim Betreten der Szene. yaw = Grad ab Nord. */ +export const START_VIEWS = [ + { id: 'sitzplatz', label: 'Hauptsitzplatz', x: -18.6, z: -2.6, yaw: 135, note: 'Travertinplatten 80x80x3 cm, Blick über den Sitzplatz gegen den Rasen' }, + { id: 'gedeckt', label: 'Gedeckter Sitzplatz', x: -11.0, z: -1.2, yaw: 215, note: 'Keramikplatten unter dem Obergeschoss, hinter der Lamellenwand' }, + { id: 'rasen', label: 'Rasen Süd', x: -13.0, z: 2.8, yaw: 75, note: 'Blick der Südfassade entlang zum Spielbereich' }, + { id: 'grill', label: 'Flammengrill', x: -7.8, z: 2.6, yaw: 110, note: 'Dekorkiesfeld mit Flammengrill und Holzscheiterwand' }, + { id: 'spielplatz', label: 'Spielbereich', x: -3.4, z: 1.4, yaw: 150, note: 'Spielsand, Spielhaus und Apfelbaum' }, + { id: 'eingang', label: 'Hauseingang', x: 1.3, z: -1.8, yaw: 250, note: 'Keramikplatten beim Fixpunkt 0.00' }, +]; + +/** Standort fuer den Sonnenstand: Abtwil SG. */ +export const SITE_LOCATION = { + name: 'Egglihaldenstrasse 9a, 9030 Abtwil SG', + lat: 47.4206, + lon: 9.2903, + /** Peilung der Site-X-Achse gegenueber Nord, in Grad. */ + northBearingOfSiteX: 61.1615, + groundMasl: 651.17, +}; diff --git a/tools/build-site.mjs b/tools/build-site.mjs new file mode 100644 index 0000000..6b5d8a3 --- /dev/null +++ b/tools/build-site.mjs @@ -0,0 +1,351 @@ +/** + * Baut aus der Planextraktion (tools/out/plan-raw.json) und den handgepflegten + * Ergaenzungen (tools/authoring.mjs) das Szenenmodell public/data/site.json. + * + * Aufruf: node tools/build-site.mjs + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + SURFACE_BY_COLOR, PLANT_BY_COLOR, TREES, WALLS, PROPS, STELEN, + BUILDING, NEIGHBOUR_BUILDINGS, TERRAIN_ZONES, EXTRA_SPOTS, EXTRA_SURFACES, + START_VIEWS, SITE_LOCATION, +} from './authoring.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); +const raw = JSON.parse(fs.readFileSync(path.join(__dirname, 'out', 'plan-raw.json'), 'utf8')); + +/** Rasterweite des Hoehenmodells in Metern. */ +const CELL = 0.25; +const GRID = { x0: -26, z0: -14.5, x1: 9, z1: 12 }; + +const round = (v, n = 3) => +v.toFixed(n); + +// --------------------------------------------------------------------------- +// Geometrie-Helfer +// --------------------------------------------------------------------------- + +function pointInPolygon(x, z, poly) { + let inside = false; + for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { + const [xi, zi] = poly[i], [xj, zj] = poly[j]; + if ((zi > z) !== (zj > z) && x < ((xj - xi) * (z - zi)) / (zj - zi) + xi) inside = !inside; + } + return inside; +} + +function polygonArea(poly) { + let a = 0; + for (let i = 0; i < poly.length; i++) { + const [x0, z0] = poly[i], [x1, z1] = poly[(i + 1) % poly.length]; + a += x0 * z1 - x1 * z0; + } + return a / 2; +} + +function centroid(poly) { + let x = 0, z = 0; + for (const p of poly) { x += p[0]; z += p[1]; } + return [x / poly.length, z / poly.length]; +} + +/** Ebene y = a*x + b*z + c durch Punkte, kleinste Quadrate. Null bei Entartung. */ +function fitPlane(points) { + if (points.length < 3) return null; + let sxx = 0, sxz = 0, szz = 0, sx = 0, sz = 0, n = points.length; + let sxy = 0, szy = 0, sy = 0; + for (const p of points) { + sxx += p.x * p.x; sxz += p.x * p.z; szz += p.z * p.z; + sx += p.x; sz += p.z; + sxy += p.x * p.y; szy += p.z * p.y; sy += p.y; + } + const m = [[sxx, sxz, sx], [sxz, szz, sz], [sx, sz, n]]; + const v = [sxy, szy, sy]; + const det = + m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - + m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]); + if (Math.abs(det) < 1e-9) return null; + const solve = (col) => { + const mm = m.map((r) => r.slice()); + for (let i = 0; i < 3; i++) mm[i][col] = v[i]; + return ( + mm[0][0] * (mm[1][1] * mm[2][2] - mm[1][2] * mm[2][1]) - + mm[0][1] * (mm[1][0] * mm[2][2] - mm[1][2] * mm[2][0]) + + mm[0][2] * (mm[1][0] * mm[2][1] - mm[1][1] * mm[2][0]) + ) / det; + }; + return { a: solve(0), b: solve(1), c: solve(2) }; +} + +// --------------------------------------------------------------------------- +// 1. Belagsflaechen +// --------------------------------------------------------------------------- + +/** Bauteile, die als Mauer modelliert werden - nicht als begehbare Flaeche. */ +const NOT_A_SURFACE = new Set(['travertinmauer', 'winkelelement', 'spielhaus', 'gartenhaus', 'roboter', 'ligusterhecke']); + +const surfaces = []; +for (const s of raw.shapes) { + if (s.op !== 'f' || s.circle) continue; + const type = SURFACE_BY_COLOR[s.hex]; + if (!type) continue; + if (s.area < 0.35) continue; + if (NOT_A_SURFACE.has(type)) continue; + const poly = polygonArea(s.pts) < 0 ? [...s.pts].reverse() : s.pts; + surfaces.push({ type, area: round(s.area, 2), polygon: poly.map((p) => [round(p[0]), round(p[1])]) }); +} +for (const s of EXTRA_SURFACES) { + const poly = polygonArea(s.polygon) < 0 ? [...s.polygon].reverse() : s.polygon; + surfaces.push({ type: s.type, area: round(Math.abs(polygonArea(s.polygon)), 2), polygon: poly, note: s.note }); +} +surfaces.sort((a, b) => b.area - a.area); + +// --------------------------------------------------------------------------- +// 2. Hecken und Pflanzen aus den Plankreisen +// --------------------------------------------------------------------------- + +const treeAt = (x, z, tol = 0.6) => TREES.find((t) => Math.hypot(t.x - x, t.z - z) < tol); + +const plants = []; +for (const s of raw.shapes) { + if (s.op !== 'f' || !s.circle) continue; + const type = PLANT_BY_COLOR[s.hex]; + if (!type) continue; + const { cx, cz, r } = s.circle; + if (r < 0.06) continue; // Lampensymbole und Punktraster + if (treeAt(cx, cz)) continue; // Solitaere kommen aus dem Authoring + plants.push({ type, x: round(cx), z: round(cz), r: round(r, 2) }); +} +// Der Ligusterhecken-Streifen ist im Plan eine Flaeche, keine Kreise. +for (const s of raw.shapes) { + if (s.op === 'f' && s.hex === '#73e900' && s.area > 1) { + plants.push({ type: 'hecke', kind: 'liguster', height: 1.7, width: 0.75, polygon: s.pts.map((p) => [round(p[0]), round(p[1])]) }); + } +} + +const trees = TREES.map((t) => ({ ...t, x: round(t.x), z: round(t.z) })); + +// --------------------------------------------------------------------------- +// 3. Leuchten +// --------------------------------------------------------------------------- + +const lamps = []; +const addLamp = (x, z, kind) => { + if (lamps.some((l) => Math.hypot(l.x - x, l.z - z) < 0.35)) return; + lamps.push({ x: round(x), z: round(z), kind }); +}; +for (const s of raw.shapes) { + if (s.op !== 'f') continue; + const [cx, cz] = centroid(s.pts); + // Rote Quadrate = Wand-/Pollerleuchten an Grenze und Mauern + if (s.hex === '#ff0000' && s.area > 0.02 && s.area < 0.08) addLamp(cx, cz, 'poller'); + // Kleine gelbe Marker im Kreissymbol = Bodenstrahler + else if (s.hex === '#ffff00' && s.area < 0.05) addLamp(cx, cz, 'boden'); +} + +// --------------------------------------------------------------------------- +// 4. Hoehenmodell +// --------------------------------------------------------------------------- + +const zoneOf = (x, z) => { + for (const zone of TERRAIN_ZONES) if (pointInPolygon(x, z, zone.polygon)) return zone.id; + return null; +}; + +const spots = [ + ...raw.spots.map((s) => ({ x: s.x, z: s.z, y: s.y, zone: zoneOf(s.x, s.z) })), + ...EXTRA_SPOTS, +]; +// Die Kote 1.47 gehoert zum Spielhauspodest, nicht zum Gelaende. +const terrainSpots = spots.filter((s) => s.zone && s.y < 1.2); + +/** Flaechen, die als Ebene ausgebildet sind - dort wird das Raster begradigt. */ +const FLAT_TYPES = new Set(['travertin80', 'travertin4060', 'travertinstell', 'keramik', 'verbundstein', 'tegula', 'spielsand']); +const flatSurfaces = []; +for (const s of surfaces) { + if (!FLAT_TYPES.has(s.type) || s.area < 1.0) continue; + const inside = terrainSpots.filter((sp) => pointInPolygon(sp.x, sp.z, s.polygon)); + if (!inside.length) continue; + let plane = inside.length >= 3 ? fitPlane(inside) : null; + // Fast kollineare Koten liefern wilde Ebenen. Nur uebernehmen, wenn das + // Gefaelle plausibel ist (< 10 %) und alle Koten getroffen werden. + if (plane) { + const slope = Math.hypot(plane.a, plane.b); + const maxDev = Math.max(...inside.map((p) => Math.abs(plane.a * p.x + plane.b * p.z + plane.c - p.y))); + if (slope > 0.10 || maxDev > 0.06) plane = null; + } + const ys = inside.map((p) => p.y); + const mean = ys.reduce((a, b) => a + b, 0) / ys.length; + flatSurfaces.push({ + polygon: s.polygon, plane, mean, type: s.type, + min: Math.min(...ys) - 0.15, max: Math.max(...ys) + 0.15, + }); +} + +const nx = Math.round((GRID.x1 - GRID.x0) / CELL) + 1; +const nz = Math.round((GRID.z1 - GRID.z0) / CELL) + 1; +const height = new Float32Array(nx * nz); + +/** + * Inverse Distanzwichtung innerhalb einer Zone. Der weiche Kern (1/(d^2+c)) + * vermeidet Spitzen um die einzelnen Koten - ein 1/d^3-Kern erzeugt Beulen, + * die spaeter durch die Belagsflaechen durchstossen. + */ +function interpolate(x, z, zoneId) { + const pool = terrainSpots.filter((s) => s.zone === zoneId); + if (!pool.length) return TERRAIN_ZONES.find((t) => t.id === zoneId)?.fallback ?? 0; + let num = 0, den = 0; + for (const s of pool) { + const d2 = (s.x - x) ** 2 + (s.z - z) ** 2; + const w = 1 / (d2 * d2 + 0.35); + num += w * s.y; den += w; + } + return num / den; +} + +for (let j = 0; j < nz; j++) { + const z = GRID.z0 + j * CELL; + for (let i = 0; i < nx; i++) { + const x = GRID.x0 + i * CELL; + const zone = zoneOf(x, z); + let y; + if (!zone) { + // Ausserhalb aller Zonen: naechstgelegene Zone weiterziehen + let best = null, bd = Infinity; + for (const t of TERRAIN_ZONES) { + const [cx, cz] = centroid(t.polygon); + const d = Math.hypot(cx - x, cz - z); + if (d < bd) { bd = d; best = t; } + } + y = best.fallback; + } else { + y = interpolate(x, z, zone); + for (const f of flatSurfaces) { + if (!pointInPolygon(x, z, f.polygon)) continue; + y = f.plane ? f.plane.a * x + f.plane.b * z + f.plane.c : f.mean; + y = Math.max(f.min, Math.min(f.max, y)); + break; + } + } + height[j * nx + i] = y; + } +} + +// Zwei Glaettungsrunden, damit weder die Kanten der Ebenen schneiden noch +// einzelne Koten als Beulen stehenbleiben. +let smoothed = Float32Array.from(height); +for (let pass = 0; pass < 2; pass++) { + const src = smoothed; + const dst = Float32Array.from(src); + for (let j = 1; j < nz - 1; j++) { + for (let i = 1; i < nx - 1; i++) { + const k = j * nx + i; + dst[k] = (src[k] * 4 + src[k - 1] + src[k + 1] + src[k - nx] + src[k + nx]) / 8; + } + } + smoothed = dst; +} + +// --------------------------------------------------------------------------- +// 5. Mauern: fehlende Hoehen aus dem Gelaende ergaenzen +// --------------------------------------------------------------------------- + +function sampleHeight(x, z) { + const fi = (x - GRID.x0) / CELL, fj = (z - GRID.z0) / CELL; + const i = Math.max(0, Math.min(nx - 2, Math.floor(fi))); + const j = Math.max(0, Math.min(nz - 2, Math.floor(fj))); + const tx = Math.max(0, Math.min(1, fi - i)), tz = Math.max(0, Math.min(1, fj - j)); + const h = (a, b) => smoothed[b * nx + a]; + return ( + h(i, j) * (1 - tx) * (1 - tz) + h(i + 1, j) * tx * (1 - tz) + + h(i, j + 1) * (1 - tx) * tz + h(i + 1, j + 1) * tx * tz + ); +} + +/** Punkt und Normale auf einem Polyzug bei relativer Bogenlaenge t. */ +function alongPath(path, t) { + const segs = path.slice(1).map((p, i) => ({ + a: path[i], b: p, len: Math.hypot(p[0] - path[i][0], p[1] - path[i][1]), + })); + const total = segs.reduce((s, g) => s + g.len, 0); + let want = t * total; + for (const s of segs) { + if (want <= s.len || s === segs[segs.length - 1]) { + const u = s.len > 0 ? Math.min(1, want / s.len) : 0; + const dx = (s.b[0] - s.a[0]) / (s.len || 1); + const dz = (s.b[1] - s.a[1]) / (s.len || 1); + return { x: s.a[0] + (s.b[0] - s.a[0]) * u, z: s.a[1] + (s.b[1] - s.a[1]) * u, nx: -dz, nz: dx }; + } + want -= s.len; + } + return { x: path[0][0], z: path[0][1], nx: 0, nz: 1 }; +} + +const walls = WALLS.map((w) => { + const out = { ...w }; + if (w.retains) { + // Stuetzelement: Krone folgt der hoeheren Seite, Fuss der tieferen. + // Ein Meter beidseits reicht, um die beiden Gelaendeniveaus abzugreifen. + const n = 24; + const crest = [], foot = []; + for (let i = 0; i <= n; i++) { + const { x, z, nx, nz } = alongPath(w.path, i / n); + const hA = sampleHeight(x + nx * 1.0, z + nz * 1.0); + const hB = sampleHeight(x - nx * 1.0, z - nz * 1.0); + crest.push(round(Math.max(hA, hB) + 0.06, 3)); + foot.push(round(Math.min(hA, hB) - 0.25, 3)); + } + out.crestProfile = crest; + out.footProfile = foot; + } + return out; +}); + +// --------------------------------------------------------------------------- +// 6. Ausgabe +// --------------------------------------------------------------------------- + +const site = { + meta: { + ...raw.meta, + generated: new Date().toISOString().slice(0, 10), + location: SITE_LOCATION, + plan: 'Gartenplan 1, Garten Design AG, 14.08.2026, Massstab 1:100', + architecture: 'Haus 9a, EHLE Architekten AG, Projektplaene 27.04.2021', + }, + terrain: { + grid: { x0: GRID.x0, z0: GRID.z0, cell: CELL, nx, nz }, + heights: Array.from(smoothed, (v) => round(v, 3)), + zones: TERRAIN_ZONES.map((z) => ({ id: z.id, label: z.label, polygon: z.polygon })), + spots: terrainSpots.map((s) => ({ x: round(s.x), z: round(s.z), y: s.y, zone: s.zone })), + }, + surfaces, + plants, + trees, + lamps, + walls, + props: PROPS, + stelen: STELEN, + building: BUILDING, + neighbourBuildings: NEIGHBOUR_BUILDINGS, + startViews: START_VIEWS, +}; + +fs.mkdirSync(path.join(ROOT, 'public', 'data'), { recursive: true }); +const file = path.join(ROOT, 'public', 'data', 'site.json'); +fs.writeFileSync(file, JSON.stringify(site)); + +const byType = {}; +for (const s of surfaces) byType[s.type] = (byType[s.type] ?? 0) + s.area; +console.log(`site.json ${(fs.statSync(file).size / 1024).toFixed(0)} kB`); +console.log(`Belagsflaechen ${surfaces.length}:`); +for (const [t, a] of Object.entries(byType).sort((x, y) => y[1] - x[1])) { + console.log(' ' + t.padEnd(16) + a.toFixed(1).padStart(7) + ' m2'); +} +console.log(`Pflanzen ${plants.length}, Baeume ${trees.length}, Leuchten ${lamps.length}, Mauern ${walls.length}, Objekte ${PROPS.length}, Stelen ${STELEN.length}`); +console.log(`Hoehenraster ${nx} x ${nz} (${CELL} m), Koten ${terrainSpots.length}`); +const hs = Array.from(smoothed); +console.log(`Hoehen ${Math.min(...hs).toFixed(2)} .. ${Math.max(...hs).toFixed(2)} m (absolut ${(651.17 + Math.min(...hs)).toFixed(2)} .. ${(651.17 + Math.max(...hs)).toFixed(2)} m ue. M.)`); diff --git a/tools/debug-svg.mjs b/tools/debug-svg.mjs new file mode 100644 index 0000000..13681f0 --- /dev/null +++ b/tools/debug-svg.mjs @@ -0,0 +1,59 @@ +/** + * Zeichnet die extrahierte Geometrie als SVG-Draufsicht - Kontrolle, ob die + * Transformation aus dem PDF stimmt. + * + * Aufruf: node tools/debug-svg.mjs [nur-farbe-hex] + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const OUT = path.join(__dirname, 'out'); +const data = JSON.parse(fs.readFileSync(path.join(OUT, 'plan-raw.json'), 'utf8')); +const only = process.argv[2]?.toLowerCase(); + +const PAD = 1.5; +const PXM = 34; // Pixel pro Meter +let x0 = Infinity, z0 = Infinity, x1 = -Infinity, z1 = -Infinity; +for (const s of data.shapes) for (const [x, z] of s.pts) { + x0 = Math.min(x0, x); x1 = Math.max(x1, x); z0 = Math.min(z0, z); z1 = Math.max(z1, z); +} +x0 -= PAD; z0 -= PAD; x1 += PAD; z1 += PAD; +const W = Math.round((x1 - x0) * PXM), H = Math.round((z1 - z0) * PXM); +const X = (x) => ((x - x0) * PXM).toFixed(1); +const Z = (z) => ((z - z0) * PXM).toFixed(1); + +const out = []; +out.push(``); +out.push(``); + +const fills = data.shapes.filter((s) => s.op === 'f' && s.area > 0.05); +fills.sort((a, b) => b.area - a.area); +for (const s of fills) { + if (only && s.hex.toLowerCase() !== only) continue; + const d = s.pts.map((p, i) => `${i ? 'L' : 'M'}${X(p[0])},${Z(p[1])}`).join('') + 'Z'; + out.push(``); +} +if (!only) { + for (const s of data.shapes) { + if (s.op !== 's' || s.pts.length < 2) continue; + const d = s.pts.map((p, i) => `${i ? 'L' : 'M'}${X(p[0])},${Z(p[1])}`).join(''); + out.push(``); + } + for (const sp of data.spots) { + out.push(``); + out.push(`${sp.y.toFixed(2)}`); + } +} +// Massstab und Achsen +out.push(` + `); +out.push(`+X (ENE)`); +out.push(`+Z (SSE)`); +out.push(`Gartenplan → Site-Frame · ${(x1 - x0).toFixed(1)} × ${(z1 - z0).toFixed(1)} m · 1 Raster = 1 m`); +out.push(''); + +const file = path.join(OUT, only ? `debug-${only.replace('#', '')}.svg` : 'debug-plan.svg'); +fs.writeFileSync(file, out.join('\n')); +console.log('geschrieben:', file, `${W}x${H}px`); diff --git a/tools/extract-plan.mjs b/tools/extract-plan.mjs new file mode 100644 index 0000000..18a1d93 --- /dev/null +++ b/tools/extract-plan.mjs @@ -0,0 +1,264 @@ +/** + * Extrahiert die Vektorgeometrie aus dem Gartenplan-PDF (Garten Design AG, 1:100) + * und transformiert sie in ein georeferenziertes, hausachsen-paralleles + * Koordinatensystem in Metern. + * + * Ausgabe: tools/out/plan-raw.json + * + * Koordinatensystem (Site-Frame, entspricht three.js X/Z): + * X = Hausachse ENE, Peilung 61.16 deg + * Z = senkrecht dazu SSE, Peilung 151.16 deg + * Y = Hoehe (hier nicht enthalten, kommt aus den Hoehenkoten) + * Ursprung = Fixpunkt "0.00" beim Hauseingang, absolut 651.17 m ue. M. + * + * Aufruf: node tools/extract-plan.mjs + */ +import * as mupdf from 'mupdf'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); +const PDF = path.join(ROOT, 'Info Dateien', 'Gartenplan 1 Jasmin und Damian Kellenberger.pdf'); +const OUT_DIR = path.join(__dirname, 'out'); + +/** Massstab 1:100 -> 1 PDF-Punkt entspricht 0.0352778 m in der Realitaet. */ +const PT_TO_M = (25.4 / 72 / 1000) * 100; + +/** Zeichnungsraster gegenueber Blattkante, aus der Winkelanalyse der Linien. */ +const GRID_ANGLE_DEG = -28.8385; + +/** Nur dieser Blattausschnitt enthaelt den Situationsplan (PDF-Punkte). */ +const PLAN_CLIP = { x0: 600, y0: 150, x1: 1800, y1: 1060 }; + +const cos = Math.cos((GRID_ANGLE_DEG * Math.PI) / 180); +const sin = Math.sin((GRID_ANGLE_DEG * Math.PI) / 180); + +/** + * PDF-Punkt -> Site-Meter, noch ohne Ursprungsversatz. + * Auf dem Blatt ist Norden oben, also entspricht (x, y) direkt (Ost, Sued). + * Projektion auf die beiden Rasterachsen des Plans: + * d1 = (cos, sin) bei GRID_ANGLE_DEG -> Site-X (Hausachse ENE) + * d2 = (-sin, cos) -> Site-Z (SSE) + */ +function toSite(x, y) { + return [(x * cos + y * sin) * PT_TO_M, (-x * sin + y * cos) * PT_TO_M]; +} + +function bboxOf(segs) { + let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity; + for (const s of segs) { + for (let i = 1; i + 1 < s.length; i += 2) { + x0 = Math.min(x0, s[i]); x1 = Math.max(x1, s[i]); + y0 = Math.min(y0, s[i + 1]); y1 = Math.max(y1, s[i + 1]); + } + } + return [x0, y0, x1, y1]; +} + +/** Flaeche eines geschlossenen Polygonzugs (Shoelace), in m^2. */ +function areaOf(points) { + let a = 0; + for (let i = 0; i < points.length; i++) { + const [x0, z0] = points[i]; + const [x1, z1] = points[(i + 1) % points.length]; + a += x0 * z1 - x1 * z0; + } + return Math.abs(a) / 2; +} + +/** + * Erkennt, ob ein Pfad ein Kreis ist (Pflanzensymbole sind Bezier-Kreise). + * Liefert {cx, cz, r} oder null. + */ +function asCircle(points) { + if (points.length < 8) return null; + let cx = 0, cz = 0; + for (const [x, z] of points) { cx += x; cz += z; } + cx /= points.length; cz /= points.length; + let rMin = Infinity, rMax = 0, rSum = 0; + for (const [x, z] of points) { + const r = Math.hypot(x - cx, z - cz); + rMin = Math.min(rMin, r); rMax = Math.max(rMax, r); rSum += r; + } + const r = rSum / points.length; + if (r < 0.05) return null; + // Toleranz: echte Kreise streuen unter 12 % um den Mittelwert. + if ((rMax - rMin) / r > 0.12) return null; + return { cx, cz, r }; +} + +function flattenSegments(segs) { + // Beziers werden linear unterteilt; 8 Stuetzstellen reichen bei Plangroessen. + const rings = []; + let cur = null, sx = 0, sy = 0, px = 0, py = 0; + const push = (x, y) => { cur.push([x, y]); px = x; py = y; }; + for (const s of segs) { + switch (s[0]) { + case 'M': + if (cur && cur.length > 1) rings.push(cur); + cur = []; sx = s[1]; sy = s[2]; push(s[1], s[2]); + break; + case 'L': + if (!cur) { cur = []; sx = s[1]; sy = s[2]; } + push(s[1], s[2]); + break; + case 'C': { + if (!cur) { cur = []; sx = px; sy = py; cur.push([px, py]); } + const [x0, y0] = [px, py]; + for (let i = 1; i <= 8; i++) { + const t = i / 8, u = 1 - t; + const x = u * u * u * x0 + 3 * u * u * t * s[1] + 3 * u * t * t * s[3] + t * t * t * s[5]; + const y = u * u * u * y0 + 3 * u * u * t * s[2] + 3 * u * t * t * s[4] + t * t * t * s[6]; + cur.push([x, y]); + } + px = s[5]; py = s[6]; + break; + } + case 'Z': + if (cur && cur.length > 1) { rings.push(cur); } + cur = [[sx, sy]]; px = sx; py = sy; + break; + } + } + if (cur && cur.length > 1) rings.push(cur); + return rings; +} + +/** Entfernt doppelte und kollineare Stuetzpunkte. */ +function simplify(points, eps = 0.004) { + const out = []; + for (const p of points) { + const last = out[out.length - 1]; + if (last && Math.hypot(p[0] - last[0], p[1] - last[1]) < eps) continue; + out.push(p); + } + while (out.length > 1 && Math.hypot(out[0][0] - out[out.length - 1][0], out[0][1] - out[out.length - 1][1]) < eps) { + out.pop(); + } + const res = []; + for (let i = 0; i < out.length; i++) { + const a = out[(i - 1 + out.length) % out.length], b = out[i], c = out[(i + 1) % out.length]; + const cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); + if (Math.abs(cross) < 1e-5 && out.length > 3) continue; + res.push(b); + } + return res.length >= 3 ? res : out; +} + +function main() { + const doc = mupdf.Document.openDocument(fs.readFileSync(PDF), 'application/pdf'); + const page = doc.loadPage(0); + + // --- 1. Vektor-Operationen mitschneiden ------------------------------- + const raw = []; + const apply = (m, x, y) => [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]]; + const walk = (p, m) => { + const segs = []; + p.walk({ + moveTo: (x, y) => segs.push(['M', ...apply(m, x, y)]), + lineTo: (x, y) => segs.push(['L', ...apply(m, x, y)]), + curveTo: (a, b, c, d, e, f) => segs.push(['C', ...apply(m, a, b), ...apply(m, c, d), ...apply(m, e, f)]), + closePath: () => segs.push(['Z']), + }); + return segs; + }; + const noop = () => {}; + const device = new mupdf.Device({ + fillPath: (p, eo, m, cs, color) => raw.push({ op: 'f', segs: walk(p, m), c: Array.from(color || []) }), + strokePath: (p, st, m, cs, color) => raw.push({ op: 's', segs: walk(p, m), c: Array.from(color || []), lw: st?.lineWidth ?? 0 }), + fillShade: noop, fillImage: noop, fillImageMask: noop, + clipPath: noop, clipStrokePath: noop, clipImageMask: noop, popClip: noop, + fillText: noop, strokeText: noop, clipText: noop, clipStrokeText: noop, ignoreText: noop, + beginMask: noop, endMask: noop, beginGroup: noop, endGroup: noop, + beginTile: () => 0, endTile: noop, beginLayer: noop, endLayer: noop, close: noop, + }); + page.run(device, mupdf.Matrix.identity); + device.close(); + + // --- 2. Auf den Planausschnitt beschraenken und transformieren -------- + const shapes = []; + for (const it of raw) { + if (!it.segs.length) continue; + const b = bboxOf(it.segs); + if (!(b[0] >= PLAN_CLIP.x0 && b[2] <= PLAN_CLIP.x1 && b[1] >= PLAN_CLIP.y0 && b[3] <= PLAN_CLIP.y1)) continue; + for (const ring of flattenSegments(it.segs)) { + const pts = simplify(ring.map(([x, y]) => toSite(x, y))); + if (pts.length < 2) continue; + const rgb = it.c.length >= 3 ? it.c.slice(0, 3).map((v) => Math.round(v * 255)) : [0, 0, 0]; + shapes.push({ + op: it.op, + color: rgb, + hex: '#' + rgb.map((v) => v.toString(16).padStart(2, '0')).join(''), + pts, + area: it.op === 'f' ? +areaOf(pts).toFixed(4) : 0, + circle: asCircle(pts), + lw: it.lw ? +(it.lw * PT_TO_M).toFixed(3) : 0, + }); + } + } + + // --- 3. Beschriftungen mit Position ----------------------------------- + const st = JSON.parse(page.toStructuredText('preserve-whitespace').asJSON()); + const labels = []; + for (const block of st.blocks ?? []) { + if (block.type !== 'text') continue; + for (const line of block.lines ?? []) { + const t = (line.text ?? '').trim(); + if (!t) continue; + const { x, y, w, h } = line.bbox; + if (x < PLAN_CLIP.x0 || x > PLAN_CLIP.x1 || y < PLAN_CLIP.y0 || y > PLAN_CLIP.y1) continue; + const [sx, sz] = toSite(x + w / 2, y + h / 2); + labels.push({ text: t, x: +sx.toFixed(3), z: +sz.toFixed(3) }); + } + } + + // --- 4. Ursprung auf den Fixpunkt legen ------------------------------- + const fix = labels.find((l) => l.text.includes('0.00 Fixpunkt')) ?? labels.find((l) => l.text === 'Eingang'); + const ox = fix ? fix.x : 0; + const oz = fix ? fix.z : 0; + const shift = (p) => [+(p[0] - ox).toFixed(4), +(p[1] - oz).toFixed(4)]; + for (const s of shapes) { + s.pts = s.pts.map(shift); + if (s.circle) { + s.circle.cx = +(s.circle.cx - ox).toFixed(4); + s.circle.cz = +(s.circle.cz - oz).toFixed(4); + s.circle.r = +s.circle.r.toFixed(4); + } + } + for (const l of labels) { l.x = +(l.x - ox).toFixed(3); l.z = +(l.z - oz).toFixed(3); } + + // --- 5. Hoehenkoten aus den Beschriftungen -------------------------- + const spots = labels + .filter((l) => /^-?\d\.\d{2}$/.test(l.text)) + .map((l) => ({ x: l.x, z: l.z, y: parseFloat(l.text) })); + + fs.mkdirSync(OUT_DIR, { recursive: true }); + const out = { + meta: { + source: path.basename(PDF), + scale: '1:100', + ptToM: PT_TO_M, + gridAngleDeg: GRID_ANGLE_DEG, + northBearingOfSiteX: 61.1615, + originLabel: fix?.text ?? null, + originAbsoluteMasl: 651.17, + note: 'X = Hausachse ENE, Z = SSE, Ursprung = Fixpunkt 0.00 beim Eingang', + }, + shapes, + labels, + spots, + }; + fs.writeFileSync(path.join(OUT_DIR, 'plan-raw.json'), JSON.stringify(out)); + + const fills = shapes.filter((s) => s.op === 'f'); + console.log(`Formen: ${shapes.length} (Fuellungen ${fills.length}, Linien ${shapes.length - fills.length})`); + console.log(`Kreise: ${shapes.filter((s) => s.circle).length}`); + console.log(`Labels: ${labels.length} Hoehenkoten: ${spots.length}`); + const xs = fills.flatMap((s) => s.pts.map((p) => p[0])); + const zs = fills.flatMap((s) => s.pts.map((p) => p[1])); + console.log(`Ausdehnung: X ${Math.min(...xs).toFixed(1)} .. ${Math.max(...xs).toFixed(1)} m, Z ${Math.min(...zs).toFixed(1)} .. ${Math.max(...zs).toFixed(1)} m`); +} + +main(); diff --git a/tools/inspect-colors.mjs b/tools/inspect-colors.mjs new file mode 100644 index 0000000..c550537 --- /dev/null +++ b/tools/inspect-colors.mjs @@ -0,0 +1,52 @@ +/** + * Listet alle Fuellfarben des Plans mit Flaeche, Anzahl Polygone und den + * naechstgelegenen Beschriftungen - Grundlage fuer die Farb-zu-Material-Zuordnung. + * + * Aufruf: node tools/inspect-colors.mjs [minFlaeche] + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const data = JSON.parse(fs.readFileSync(path.join(__dirname, 'out', 'plan-raw.json'), 'utf8')); +const MIN = parseFloat(process.argv[2] ?? '0.15'); + +const centroid = (pts) => { + let x = 0, z = 0; + for (const p of pts) { x += p[0]; z += p[1]; } + return [x / pts.length, z / pts.length]; +}; + +const groups = new Map(); +for (const s of data.shapes) { + if (s.op !== 'f') continue; + const g = groups.get(s.hex) ?? { hex: s.hex, n: 0, area: 0, big: [], circles: 0 }; + g.n++; g.area += s.area; + if (s.circle) g.circles++; + if (s.area >= MIN) g.big.push(s); + groups.set(s.hex, g); +} + +const rows = [...groups.values()].sort((a, b) => b.area - a.area); +for (const g of rows) { + if (g.area < 0.2 && g.circles === 0) continue; + const near = new Set(); + for (const s of g.big.slice(0, 6)) { + const [cx, cz] = centroid(s.pts); + const cands = data.labels + .map((l) => ({ l, d: Math.hypot(l.x - cx, l.z - cz) })) + .filter((c) => c.d < 3.5 && !/^-?\d\.\d{2}$/.test(c.l.text)) + .sort((a, b) => a.d - b.d) + .slice(0, 3); + for (const c of cands) near.add(c.l.text); + } + console.log( + g.hex.padEnd(9), + ('n=' + g.n).padEnd(7), + (g.area.toFixed(1) + ' m2').padStart(9), + ('gross=' + g.big.length).padEnd(10), + ('kreise=' + g.circles).padEnd(11), + [...near].slice(0, 7).join(' / '), + ); +} diff --git a/tools/inspect-heights.mjs b/tools/inspect-heights.mjs new file mode 100644 index 0000000..32352be --- /dev/null +++ b/tools/inspect-heights.mjs @@ -0,0 +1,16 @@ +import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const d = JSON.parse(fs.readFileSync(path.join(__dirname,'out','plan-raw.json'),'utf8')); +console.log('--- Hoehenkoten (x, z, y) sortiert nach y ---'); +for (const s of [...d.spots].sort((a,b)=>a.y-b.y)) + console.log(String(s.y.toFixed(2)).padStart(6), ' x='+String(s.x.toFixed(2)).padStart(7), ' z='+String(s.z.toFixed(2)).padStart(7)); +function inside(pt, poly){let c=false;for(let i=0,j=poly.length-1;ipt[1])!==(zj>pt[1])&&pt[0]<(xj-xi)*(pt[1]-zi)/(zj-zi)+xi)c=!c;}return c;} +console.log('\n--- Hoehenkoten je grosse Flaeche ---'); +const big = d.shapes.filter(s=>s.op==='f'&&s.area>=1.0&&s.hex!=='#000000').sort((a,b)=>b.area-a.area); +for(const s of big){ + const hits = d.spots.filter(sp=>inside([sp.x,sp.z], s.pts)); + const nearby = d.spots.filter(sp=>!inside([sp.x,sp.z],s.pts)).map(sp=>({sp,dd:Math.min(...s.pts.map(p=>Math.hypot(p[0]-sp.x,p[1]-sp.z)))})).filter(o=>o.dd<0.6).map(o=>o.sp); + console.log(s.hex, String(s.area.toFixed(1)).padStart(6)+'m2', + 'innen=['+hits.map(h=>h.y.toFixed(2)).join(' ')+']', + 'rand=['+nearby.map(h=>h.y.toFixed(2)).join(' ')+']'); +} diff --git a/tools/inspect-polys.mjs b/tools/inspect-polys.mjs new file mode 100644 index 0000000..3f7ef94 --- /dev/null +++ b/tools/inspect-polys.mjs @@ -0,0 +1,14 @@ +import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const data = JSON.parse(fs.readFileSync(path.join(__dirname,'out','plan-raw.json'),'utf8')); +const want = process.argv[2]; +const cent = (p)=>{let x=0,z=0;for(const q of p){x+=q[0];z+=q[1];}return[x/p.length,z/p.length];}; +const list = data.shapes.filter(s=>s.op==='f' && s.area>=parseFloat(process.argv[3]??'0.4') && (!want||s.hex===want)); +list.sort((a,b)=>b.area-a.area); +for(const s of list){ + const [cx,cz]=cent(s.pts); + const near=data.labels.map(l=>({l,d:Math.hypot(l.x-cx,l.z-cz)})).filter(c=>c.d<4&&!/^-?\d\.\d{2}$/.test(c.l.text)).sort((a,b)=>a.d-b.d).slice(0,4).map(c=>c.l.text); + let x0=1e9,z0=1e9,x1=-1e9,z1=-1e9; for(const [x,z] of s.pts){x0=Math.min(x0,x);x1=Math.max(x1,x);z0=Math.min(z0,z);z1=Math.max(z1,z);} + console.log(s.hex, String(s.area.toFixed(2)).padStart(7)+'m2', 'c=('+cx.toFixed(1)+','+cz.toFixed(1)+')', 'bb '+[x0,z0,x1,z1].map(v=>v.toFixed(1)).join(' '), 'n='+s.pts.length, '|', near.join(' / ')); +} +console.log('total', list.length); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d10bae3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "types": [] + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..11aeb83 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', + build: { + target: 'es2022', + chunkSizeWarningLimit: 1200, + rollupOptions: { + output: { + manualChunks: { three: ['three'] }, + }, + }, + }, + server: { host: true, port: 5173 }, +});