Compare commits
No commits in common. "main" and "feature/builder-export" have entirely different histories.
main
...
feature/bu
8
.gitignore
vendored
|
|
@ -2,13 +2,5 @@
|
|||
.godot/
|
||||
*.tmp
|
||||
|
||||
# Mitgelieferte Modelle (Karte t_12cf94fe): werden per tools/sync_models.sh
|
||||
# aus dem Client-Klon gefüllt — NICHT ins Repo (ITHappy-Lizenz erlaubt keine
|
||||
# Weitergabe, siehe README_INSTALL.md §5).
|
||||
models/
|
||||
|
||||
# Export-Builds der eigenständigen App
|
||||
export/
|
||||
|
||||
# System
|
||||
.DS_Store
|
||||
|
|
|
|||
5
.gitmodules
vendored
|
|
@ -1,5 +0,0 @@
|
|||
[submodule "core"]
|
||||
path = core
|
||||
url = ../scopedvr-map-core.git
|
||||
branch = main
|
||||
ignore = untracked
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
addons/modular_builder — "Modular Builder" v0.2.0-alpha von Michel Savadim
|
||||
Quelle: Godot Asset Store (https://store.godotengine.org/asset/michel-savadim/modular-buider/)
|
||||
Lizenz: CC0 1.0 Universal (siehe addons/modular_builder/LICENSE) — gilt für Code,
|
||||
Icons und Dokumentation. Die Demo-Szene/Demo-Texturen des Originals sind NICHT
|
||||
CC0 und wurden beim Import ENTFERNT (addons/modular_builder/demo fehlt bewusst).
|
||||
|
||||
Abgleich 25.10.2026: Der Code der Runtime-Geometrie (modular_piece_geometry.gd,
|
||||
modular_building_detail.gd, modular_piece.gd, modular_pillar_support.gd,
|
||||
modular_roof_support.gd, modular_vertex_simplifier.gd, modular_mesh_baker.gd) ist
|
||||
zeichengleich zu ModularBuilder-English-0.2.0-2.zip (Ordner addons/modular_builder_en,
|
||||
dort CC0 1.0, Copyright (c) 2026 Michel Savadim). Der Bauen-Modus des Map-Editors
|
||||
(src/editor/bau_katalog.gd) nutzt diese Geometrie nur zum Erzeugen der Bauteile
|
||||
(Wandhöhe 3 m, 2-m-Raster); die Teile selbst sind normale Bibliotheks-Objekte
|
||||
(docs/bauen.md). Nicht übernommen: Demo-Szene, Demo-Texturen (nicht CC0).
|
||||
Hinweis der Vorlage: Teile des Codes wurden mit KI-Unterstützung (OpenAI) erstellt —
|
||||
vom Autor geprüft; CC0 gilt unverändert.
|
||||
115
README.md
|
|
@ -48,18 +48,17 @@ UX-Fundament auf dem App-Skelett (t_5badf371), nach dem Chef-Grundsatz gebaut:
|
|||
Neue-Map-Dialog mit 4 Pflicht-Spawns pro Team, Orbit-Viewport mit Gitter/
|
||||
Boden/Wetter-Stimmung — siehe Commit 84bc081 und `svmap_io.gd`.)*
|
||||
|
||||
- **Undo/Redo (`core/editor/action_history.gd`):** zentrale ActionHistory über
|
||||
- **Undo/Redo (`src/editor/action_history.gd`):** zentrale ActionHistory über
|
||||
die svmap-Ober-Blöcke (`objects`/`terrain`/`spawn_zones` — tiefe Kopien vor
|
||||
und nach jeder Änderung, merge_tags gegen Mausrad-Tick-Fluten, Max-Stufe 100).
|
||||
Strg+Z / Strg+Y UND große Buttons „↩ Rückgängig / ↪ Wiederholen" oben.
|
||||
Spätere Werkzeuge melden sich mit `snapshot(keys)` vor und
|
||||
`commit(desc, before, after)` nach der Änderung an — Terrain-Strokes und
|
||||
Spawn-Zonen landen damit automatisch in derselben History.
|
||||
- **Objekt-Platzierung mit Ghost-Preview (`core/editor/interaction.gd`):**
|
||||
- **Objekt-Platzierung mit Ghost-Preview (`src/editor/interaction.gd`):**
|
||||
Bibliothek links (deutsche Namen + Poly-Zahl), Klick startet den
|
||||
Platzier-Modus — eine transparente Vorschau schwebt am Cursor auf dem Boden
|
||||
(Höhe kommt aus dem Terrain), Klick setzt, ESC bricht ab (Rechte
|
||||
Maustaste = Flug, das Objekt bleibt in der Hand — Karte t_2470de89),
|
||||
(Höhe kommt aus dem Terrain), Klick setzt, Rechtsklick/ESC bricht ab,
|
||||
Mausrad dreht die Vorschau VOR dem Setzen (15°-Raster). Modus bleibt an
|
||||
für schnelles Mehrfachsetzen. Poly-Budget wird VOR dem Platzieren geprüft.
|
||||
- **Auswahl-Anfassen:** Klick wählt ein Objekt (orange + Emission), Ziehen
|
||||
|
|
@ -94,26 +93,16 @@ synchron ziehen (IDs sind der Vertrag, Entscheidung 20).
|
|||
|
||||
Auf dem Terrain-Werkzeug (t_9aba04f1) aufgebaut, nach dem Chef-Grundsatz:
|
||||
|
||||
- **Bibliothek-Browser mit Gruppen (`core/editor/library_browser.gd`):**
|
||||
*(Stand 25.10.2026, Military-Pack-Umstellung)* Die Bibliothek ist das
|
||||
komplette Military-Desert-Pack (418 Modelle + 5 Kombis = 423 Objekte aus
|
||||
`library/objects.json`), sortiert in 19 deutsche Kategorien aus dem
|
||||
Manifest-Feld `kategorie` (Panzer, Radfahrzeuge, Flugzeuge & Hubschrauber,
|
||||
Geschütze & Stellungen, Fahrzeugteile, Gebäude & Hangars, Zelte, Türme &
|
||||
Masten, Mauern & Wände, Sperren & Zäune, Kisten/Fässer/Container, Technik &
|
||||
Funk, Ausrüstung, Training, Bäume, Pflanzen/Kakteen/Gras, Felsen & Steine,
|
||||
Boden/Straßen/Dünen, Boden-Decals). Fahrzeuge sind **fertig zusammengebaut**
|
||||
(Panzer mit Turm und Kanone, Hummer mit Rädern, Hubschrauber mit Rotoren);
|
||||
die Einzelteile stehen zusätzlich unter „Fahrzeugteile“. Unbekannte
|
||||
Kategorien (z. B. spätere Bauteil-Module) bekommen automatisch eine eigene
|
||||
Gruppe. Jeder Button zeigt ein 128-px-Thumbnail (`core/thumbs/*.png` —
|
||||
erzeugt von `tools/sync_objects.sh`/`tools/gen_thumbs.gd` und committed;
|
||||
fehlt ein PNG, fällt der Button aufs Text-Layout zurück). Deutsche Namen
|
||||
kommen aus `name_de` im Manifest (ohne UI → headless testbar). Kollision:
|
||||
Gebäude/Zelte/Türme exakter Trimesh (betretbar), Fahrzeuge Verbund aus
|
||||
Boxen, Bäume nur Stamm, Dekor ohne Kollision — erzeugt vom Match-Server-Repo
|
||||
(`tools/pack_registry.py`).
|
||||
- **Snap to Ground (`core/editor/object_ops.gd`):** Ein Klick auf
|
||||
- **Bibliothek-Browser mit Gruppen (`src/editor/library_browser.gd`):** Die
|
||||
11 etablierten Objekte aus `library/objects.json`, sortiert in deutsche
|
||||
Klapp-Gruppen (Kisten & Barrieren / Bauwerke / Natur + automatisch
|
||||
„Sonstiges" für neue, noch ungruppierte IDs). Jeder Button zeigt ein
|
||||
128-px-Thumbnail (`library/thumbs/*.png` — einmalig mit
|
||||
`xvfb-run -a godot4 --rendering-driver opengl3 --path . --script
|
||||
res://tools/gen_thumbs.gd` gerendert und committed; fehlt ein PNG, fällt
|
||||
der Button sauber aufs Text-Layout zurück). Deutsche Namen wohnen jetzt in
|
||||
`LibraryBrowser` (ohne UI → headless testbar).
|
||||
- **Snap to Ground (`src/editor/object_ops.gd`):** Ein Klick auf
|
||||
„⚡ Alles auf den Boden setzen" holt ALLE schwebenden Objekte auf die
|
||||
Terrain-Höhe (z. B. nach dem Terrain-Formen) — EIN Undo-Eintrag für die
|
||||
ganze Aktion. Objekte, die schon auf dem Boden stehen, bleiben unangetastet.
|
||||
|
|
@ -138,14 +127,14 @@ Export-Workflow.
|
|||
|
||||
Auf der Objekt-Bibliothek (t_3a998ae4) aufgebaut, nach dem Chef-Grundsatz:
|
||||
|
||||
- **Spawn-Zonen malen (`core/editor/spawn_tool.gd`):** Werkzeug
|
||||
- **Spawn-Zonen malen (`src/editor/spawn_tool.gd`):** Werkzeug
|
||||
„🚩 Spawn-Zonen malen“ — Team Blau 🔵 / Team Rot 🔴 (TDM-Konvention:
|
||||
IDs A…/B…, mapformat § spawn_zones), Klick setzt eine farbige flache
|
||||
Kreis-Zone auf dem Terrain (Vorschau-Kreis folgt der Maus), Radius
|
||||
2–30 m per Regler. Zone anklicken → auswählen + ziehen (Terrain-Höhe
|
||||
bleibt), Entf löscht, Strg+Z nimmt JEDE Operation zurück (jede Zone =
|
||||
EIN History-Eintrag, Radius-Slider-Ticks sind merge-gekoppelt).
|
||||
- **Live-Validierung (`core/editor/spawn_ops.gd`):** Block „Spawn-Check“
|
||||
- **Live-Validierung (`src/editor/spawn_ops.gd`):** Block „Spawn-Check“
|
||||
rechts bleibt STÄNDIG sichtbar und spiegelt exakt die Loader-Regeln
|
||||
(V11: ≥ 4 Spawn-Punkte pro Team · V7: Objekt-IDs in der Bibliothek) —
|
||||
z. B. „✅ Spawns Team Blau: 4 · ❌ Spawns Team Rot: 2 (mindestens 4
|
||||
|
|
@ -181,11 +170,9 @@ aufgebaut, nach dem Chef-Grundsatz (Klick statt Konsole, deutsche UI,
|
|||
Undo für alles):
|
||||
|
||||
- **Duplizieren (`⧉ Duplizieren`-Button + Strg+D):** Die Kopie des
|
||||
ausgewählten Objekts hängt +2 m in X versetzt daneben (Rasterversatz,
|
||||
Karte t_c46d31c2; Füßpunkt-Höhe unverändert, mit Raster-Snap eingerastet,
|
||||
wenn an) und ist DIREKT angewählt — sie lässt sich gleich
|
||||
ausgewählten Objekts hängt diagonal 1 m versetzt daneben (mit Raster-Snap
|
||||
eingerastet, wenn an) und ist DIREKT angewählt — sie lässt sich gleich
|
||||
weiterziehen/drehen/löschen. Drehung, Größe und Höhe werden übernommen.
|
||||
Bei Mehrfachauswahl werden ALLE Objekte kopiert (EIN Undo-Schritt).
|
||||
Poly-Budget und Objekt-Limit werden VOR dem Kopieren geprüft
|
||||
(Klartext-Meldung, was zu tun ist). EIN Undo-Schritt.
|
||||
- **Anheben/Absenken (`▲ Anheben / ▼ Absenken` + Bild↑/Bild↓):** Objekte
|
||||
|
|
@ -196,12 +183,10 @@ Undo für alles):
|
|||
ist **KEIN Extra-Feld im Dateiformat nötig** und die Loader-Kompatibilität
|
||||
bleibt unangetastet (bewiesen in `tests/test_ux.gd`: Erhöhtes Objekt
|
||||
überlebt gespeichern → laden intakt (Höhe bleibt exakt)).
|
||||
- **Absenken / VERSENKEN (Karte t_e166e656):** Absenken geht FREI unter die
|
||||
Terrain-Höhe — Steine dürfen halb im Boden stecken (Sly-Wunsch).
|
||||
Untergrenze ist die Match-Grenze `bounds.min.y`; die Auswahl-Anzeige und
|
||||
die Statusleiste zeigen die Versenk-Tiefe („1,00 m im Boden versenkt“).
|
||||
„Auf Boden setzen“ holt ein versenktes Objekt wieder exakt auf das
|
||||
Terrain.
|
||||
- **Absenken stoppt an der Terrain-Höhe** am Objektstandort — nichts wird
|
||||
in Hügel begraben. Steckt ein Objekt im Boden (z. B. nach dem
|
||||
Terrain-Formen), bewegt Absenken es NICHT, und die Statusleiste erklärt,
|
||||
dass `▲ Anheben` oder `⚡ Alles auf den Boden setzen` hilft.
|
||||
- **Anheben** hat als Obergrenze die Match-Grenze `bounds.max.y` — höher
|
||||
passt der Server nicht (V9).
|
||||
- **Ziehen behält die Höhe:** Ein angehobenes Objekt schwebt beim
|
||||
|
|
@ -215,11 +200,6 @@ Undo für alles):
|
|||
(Herleitung im Quellcode: gespiegelter Richtungs-Vektor = gespiegelter
|
||||
Winkel; jede Spiegelung ist dadurch selbstinvers — zweimal spiegeln =
|
||||
Ausgangszustand.) Die Rotation wird mitgespiegelt, wie bestellt.
|
||||
KIPPEN wird mitgespiegelt (Karte t_e166e656): Achsenspiegelung =
|
||||
Konjugation der Basis `M·B·M` — X-Kippen bleibt bei X-Spiegelung,
|
||||
Z-Kippen dreht das Vorzeichen (und umgekehrt); Punktspiegelung =
|
||||
`Ry(180°)·B`. Für reines Y-Drehen ergeben beide Regeln exakt die alten
|
||||
Zahlenwerte (Doku: docs/map_format.md im Match-Server-Repo).
|
||||
Bewusst NICHT um die Kartemitte gespiegelt: das würde das Objekt quer
|
||||
über die Map springen lassen — überraschend für Einsteiger. Klartext
|
||||
in Buttons + Tooltip.
|
||||
|
|
@ -238,13 +218,13 @@ Undo für alles):
|
|||
Mehrfachauswahl + Gruppieren + Gruppen-Verschieben, auf dem UX-Fundament
|
||||
(t_40d855d0) aufgebaut, nach dem Chef-Grundsatz:
|
||||
|
||||
- **Mehrfachauswahl (`core/editor/interaction.gd`):** Umschalt+Klick fügt
|
||||
- **Mehrfachauswahl (`src/editor/interaction.gd`):** Umschalt+Klick fügt
|
||||
ein Objekt hinzu/entfernt es, Rahmen-Aufziehen (Rubberband) über leeren
|
||||
Boden wählt ALLE Objekte im Rahmen (Umschalt + Rahmen = zusätzliche zur
|
||||
Auswahl). Loser Klick auf leere Fläche hebt die Auswahl auf. ALLE
|
||||
markierten Objekte färben sich orange, das Auswahl-Panel rechts listet
|
||||
sie namentlich auf (ab 9: „… und N weitere“). Projektions-Mathe liegt
|
||||
in `core/editor/selection_math.gd` (headless getestet).
|
||||
in `src/editor/selection_math.gd` (headless getestet).
|
||||
- **Gruppieren:** Buttons „🔗 Gruppieren / ⛓ Gruppe lösen“ im Auswahl-Panel
|
||||
+ Kürzel Strg+G / Strg+Umschalt+G. Eine Gruppe braucht ≥ 2 Objekte.
|
||||
Gruppen-Mitglieder ziehen und drehen GEMEINSAM — ein Griff an ein
|
||||
|
|
@ -287,7 +267,7 @@ auch ungespeicherte! — sofort als Spiel in einem EIGENEN Fenster. Der Editor
|
|||
bleibt offen. Im Spiel: WASD laufen, Maus umsehen, Shift sprinten, Leertaste
|
||||
springen, ESC beendet das Test-Fenster sauber (kein Prozess-Kill nötig).
|
||||
|
||||
- **Temp-Export (`core/editor/test_run.gd`):** Die Map wird nach
|
||||
- **Temp-Export (`src/editor/test_run.gd`):** Die Map wird nach
|
||||
`user://testrun/<name>.svmap` exportiert (deterministischer Name aus dem
|
||||
Map-Namen, erneuter Testlauf überschreibt) — über dieselbe
|
||||
`SvmapIO.save_to_file`-Validierung wie das echte Speichern. Ist die Map
|
||||
|
|
@ -310,50 +290,3 @@ springen, ESC beendet das Test-Fenster sauber (kein Prozess-Kill nötig).
|
|||
ungültige Map blockiert, Client-Suche inkl. Konfig-Vorrang, Godot-Findung,
|
||||
exakte Argumentliste). Parse-Check umfasst `test_run.gd`.
|
||||
|
||||
## Status (M2, Karte t_c46d31c2 — Tastatur-Shortcuts 1/3: Navigation + Auswahl + Löschen FERTIG)
|
||||
|
||||
Tastenkürzel-Karte 1 von 3 (Sly-Freigabe 16.10., finale Belegung).
|
||||
**`docs/tasten.md` ist jetzt die deutsche Source of truth** für ALLE
|
||||
Shortcut-Karten — Karten 2/3 ergänzen dort.
|
||||
|
||||
- **Konventionen (alle Karten):** Tastaturcode-basiert — InputMap-Actions mit
|
||||
PHYSISCHEN `KEY_*`-Konstanten (`core/editor/shortcut_keys.gd`, registriert in
|
||||
`main._ready`, idempotent). Physische Codes = deutsche (QWERTZ) und
|
||||
Windows-Layouts identisch, keine OS-Hardcodes. Jede Objekt-Operation =
|
||||
EIN ActionHistory-Eintrag (Ausnahme: reine Kamera-Aktionen).
|
||||
- **Freelook-Flug:** Rechte Maustaste GEDACHT halten = Umsehen + Flug
|
||||
(W/S/A/D + Leertaste hoch/Shift runter, Strg = ×3, Alt = ×0,3, Mausrad =
|
||||
Grundgeschwindigkeit DAUERHAFT). Umschalt+F = Flug dauerhaft togglen.
|
||||
Orbit bleibt Default —
|
||||
beim Flug-Exit werden Ziel/Winkel/Abstand aus Flugposition/Blickrichtung
|
||||
zurückgerechnet (kein Kamera-Sprung, getestet). Mathe pur in
|
||||
`core/editor/fly_math.gd`, Kamera dünn (`core/viewport/orbit_camera.gd`).
|
||||
Konflikt-Auflösung: RMB hat ABSOLUTEN Vorrang für den Flug — in JEDEM
|
||||
Werkzeugzustand (Terrain, Bauen, Spawn, Platzieren; Karte t_2470de89,
|
||||
kein `rmb_filter` mehr), Mausrad im Flug gehört dem Tempo (interaction
|
||||
blockt).
|
||||
- **F** = Auswahl ins Bild zentrieren — Orbit-Ziel wandert auf den Auswahl-
|
||||
Mittelpunkt (Anker + halbe Mesh-Höhe, `SelectionMath.focus_center`); im
|
||||
Flug dreht sich nur die Blickrichtung.
|
||||
- **Esc** = Prioritätskette Werkzeug verlassen → Ghost-Preview abbrechen →
|
||||
Auswahl aufheben, als pure, headless-getestete Funktion
|
||||
(`core/editor/shortcut_policy.gd`).
|
||||
- **Entf (DELETE)** = Auswahl SOFORT löschen ohne Rückfrage (Sly-Yolo) —
|
||||
wirkt auf Szene-Auswahl UND Objektbaum-Markierung (Baum und Szene sind
|
||||
synchron), Strg+Z deckt es ab. War schon mehrfach-fähig, jetzt
|
||||
getestet (EIN Undo-Schritt holt ALLE zurück).
|
||||
- **Strg+D** = Auswahl duplizieren: Kopie(n) +2 m in X (Rasterversatz),
|
||||
Füßpunkt unverändert; bei Mehrfachauswahl ALLE Objekte mit EINEM
|
||||
Undo-Schritt, Budget/Limit gesammelt VORHER geprüft
|
||||
(`ObjectOps.duplicate_objects`).
|
||||
- **Strg+LMB** = Mehrfachauswahl umschalten (Objektbaum: SELECT_MULTI +
|
||||
native Strg+Klick-Logik, spiegelt sich in die Viewport-Auswahl und
|
||||
umgekehrt). Bestandsaktionen (Löschen, Duplizieren, Ziehen, Gruppieren)
|
||||
wirken auf die GANZE Mehrfachauswahl.
|
||||
- **Tests:** `tests/test_shortcuts.gd` NEU (70 Checks: InputMap-Actions
|
||||
inkl. Idempotenz, Freelook-Mathe, Esc-Prioritäten, Duplizieren +2 m X
|
||||
mit Undo-Roundtrip, Entf-Mehrfachauswahl gegen die ECHTE interaction.gd,
|
||||
Focus-Mittelpunkt, Kamera Flug-Roundtrip). Alle 8 Suiten grün
|
||||
(571 Checks), Builder-Export 21/0, Parse-Check aller geänderten Scripts
|
||||
(`tools/parse_check_shortcuts.sh`).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
# ScopedVR Map Builder auf einem anderen PC installieren
|
||||
|
||||
Diese Anleitung erklärt Schritt für Schritt, wie du den Map Builder auf einem
|
||||
beliebigen PC zum Laufen bringst und dort eine Map testen kannst — **ohne**
|
||||
Godot-Installation, **ohne** ScopedVR-Client, **ohne** Programmierkenntnisse.
|
||||
|
||||
---
|
||||
|
||||
## 1. Voraussetzungen
|
||||
|
||||
- Ein normaler PC (Linux oder Windows). Kein Godot, keine Kommandozeile nötig,
|
||||
wenn die App einmal exportiert wurde.
|
||||
- Die exportierte App (Ein Ordner oder eine EXE, siehe Schritt 2).
|
||||
- Den Ordner `models/` mit den 3D-Modellen (siehe Schritt 3).
|
||||
|
||||
## 2. Die App bekommen
|
||||
|
||||
Die App ist eine eigenständige Datei (z. B. `MapEditor` unter Linux,
|
||||
`MapEditor.exe` unter Windows). Sie wurde aus dem Map-Editor-Repo exportiert
|
||||
(`export_presets.cfg`, Preset „Linux“). Auf dem Ziel-PC brauchst du sie nur
|
||||
in einen beliebigen Ordner zu kopieren, z. B.:
|
||||
|
||||
~/MapBuilder/ (Linux)
|
||||
C:\MapBuilder\ (Windows)
|
||||
|
||||
Doppelklick drauf — die App startet. Das war's. Keine Installation.
|
||||
|
||||
## 3. Die Modelle danebenlegen (WICHTIG)
|
||||
|
||||
Ohne Modelle zeigt die App nur einfache Platzhalter-Kisten. Damit die echten
|
||||
3D-Modelle erscheinen, muss der Ordner `models/` **direkt neben** der App
|
||||
liegen (genau so wie hier):
|
||||
|
||||
~/MapBuilder/MapEditor ← die App
|
||||
~/MapBuilder/models/ ← die Modelle
|
||||
|
||||
Wie der `models/`-Ordner gefüllt wird: Auf einem PC, auf dem der
|
||||
Client-Klon liegt, im Map-Editor-Repo laufen lassen:
|
||||
|
||||
tools/sync_models.sh
|
||||
|
||||
Das Script kopiert alle Objekte (GLB) und die benötigten Texturen in den
|
||||
`models/`-Ordner — idempotent, du kannst es also gefahrlos mehrmals laufen
|
||||
lassen. Mit `--check` prüft es nur (Exit 0 = alles aktuell, Exit 1 = es fehlt
|
||||
etwas). Die Größe wird am Ende angezeigt.
|
||||
|
||||
Falls die Modelle fehlen, sagt die App beim Start deutlich:
|
||||
> „Für die Anzeige fehlen die Modelle. So bekommst du sie: …“
|
||||
|
||||
Terrain und Bauteile (Wände, Dächer usw.) funktionieren **immer** — auch ganz
|
||||
ohne Modelle (Notfall-Modus).
|
||||
|
||||
## 4. Eine Map testen
|
||||
|
||||
1. App starten.
|
||||
2. Map öffnen: Datei → Map öffnen, dann die gewünschte `.svmap`-Datei wählen.
|
||||
3. Zum Testen: Taste **F5** drücken. Du läufst dann selbst durch die Map
|
||||
(WASD = gehen, Maus = umsehen, Shift = rennen, Leertaste = springen).
|
||||
4. **ESC** zurück in den Editor.
|
||||
|
||||
## 5. Lizenz-Hinweis (bitte wirklich lesen)
|
||||
|
||||
Die 3D-Modelle und Texturen im `models/`-Ordner stammen aus dem
|
||||
**ITHappy-Pack**. Die Lizenz dieses Packs erlaubt es **nicht**, die
|
||||
Roh-Modelle und Texturen weiterzugeben. Das heißt konkret:
|
||||
|
||||
- Die App mit `models/`-Ordner ist für **Slys eigene Rechner** gedacht.
|
||||
- Der Ordner **darf nicht** an andere Leute geschickt oder hochgeladen werden.
|
||||
- Die App selbst (ohne `models/`-Ordner) darf geteilt werden — sie zeigt dann
|
||||
eben nur Platzhalter, bis der Ordner dazukommt.
|
||||
|
||||
## 6. Wenn etwas nicht geht
|
||||
|
||||
| Problem | Lösung |
|
||||
| --- | --- |
|
||||
| Nur Kisten statt echter Modelle | `models/`-Ordner direkt neben die App legen (Schritt 3) |
|
||||
| Meldung „Modelle fehlen“ | `tools/sync_models.sh` ausführen (Schritt 3) |
|
||||
| App startet gar nicht | Auf Linux: Datei per Rechtsklick → „Ausführen“ erlauben, oder im Terminal `chmod +x MapEditor` |
|
||||
| Map lässt sich nicht öffnen | Ist es wirklich eine `.svmap`-Datei? |
|
||||
|
||||
---
|
||||
*Technischer Hintergrund: die Modelle werden zur Laufzeit per GLTFDocument
|
||||
aus dem `models/`-Ordner geladen (siehe `core/viewport/glb_mesh_store.gd`,
|
||||
Quellen-Reihenfolge: Einstellungen → Nachbar-Ordner `../scopedvr-client` →
|
||||
`models/`). Erster Treffer gewinnt.*
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://8ivgqetbm82q"
|
||||
path="res://.godot/imported/create_vertex.svg-935b2fd3add0dedfb62239b6f1e7a4db.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/create_vertex.svg"
|
||||
dest_files=["res://.godot/imported/create_vertex.svg-935b2fd3add0dedfb62239b6f1e7a4db.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
|
|
@ -4,9 +4,9 @@ Build modular structures directly in the Godot 4.7 editor. The panel appears at
|
|||
|
||||
## Install and try the demo
|
||||
|
||||
Open this repository as a Godot project to explore `addons/modular_builder/demo/Demo.tscn`. After Godot finishes importing the assets, enable **Modular Builder (English)** in **Project > Project Settings > Plugins**. The demo and its five textures are contained in `addons/modular_builder/demo/`.
|
||||
Open this repository as a Godot project to explore `addons/modular_builder_en/demo/Demo.tscn`. After Godot finishes importing the assets, enable **Modular Builder (English)** in **Project > Project Settings > Plugins**. The demo and its five textures are contained in `addons/modular_builder_en/demo/`.
|
||||
|
||||
To use the plugin in an existing project, extract the ZIP at the project root so it creates only `addons/modular_builder/`, then enable **Modular Builder (English)** in **Project > Project Settings > Plugins**. Open a 3D scene to use the editor panel. The demo remains inside the addon folder and can be removed if you do not need it.
|
||||
To use the plugin in an existing project, extract the ZIP at the project root so it creates only `addons/modular_builder_en/`, then enable **Modular Builder (English)** in **Project > Project Settings > Plugins**. Open a 3D scene to use the editor panel. The demo remains inside the addon folder and can be removed if you do not need it.
|
||||
|
||||
## Build
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ Save a selected structure as a reusable `.tscn` module, or insert a saved module
|
|||
|
||||
## Files and license
|
||||
|
||||
The English plugin lives in `addons/modular_builder/`. Editor code is split into UI, scene actions, detail placement, vertex editing, and viewport interaction. Geometry generation, mesh baking, roof/pillar placement, and vertex simplification have separate scripts.
|
||||
The English plugin lives in `addons/modular_builder_en/`. Editor code is split into UI, scene actions, detail placement, vertex editing, and viewport interaction. Geometry generation, mesh baking, roof/pillar placement, and vertex simplification have separate scripts.
|
||||
|
||||
The original plugin code, icons, and documentation are dedicated to the public domain under [CC0 1.0](LICENSE). The bundled demo textures are example project assets; the CC0 dedication in this repository's license does not automatically cover them. Check their provenance and permission before redistributing the textures separately.
|
||||
|
||||
|
Before Width: | Height: | Size: 315 B After Width: | Height: | Size: 315 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://kwgsfdxprj4p"
|
||||
path="res://.godot/imported/add_building.svg-02348d81d48c2064f5946a668739ab6e.ctex"
|
||||
uid="uid://dkuvxakhpo11m"
|
||||
path="res://.godot/imported/add_building.svg-c540099545c836d05aaa84aabb9d42a4.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/add_building.svg"
|
||||
dest_files=["res://.godot/imported/add_building.svg-02348d81d48c2064f5946a668739ab6e.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/add_building.svg"
|
||||
dest_files=["res://.godot/imported/add_building.svg-c540099545c836d05aaa84aabb9d42a4.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 195 B After Width: | Height: | Size: 195 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://np6bb2n6ohj3"
|
||||
path="res://.godot/imported/cancel.svg-48b548656752953c76fdbf1fa691b961.ctex"
|
||||
uid="uid://bdfxl3d88c45p"
|
||||
path="res://.godot/imported/cancel.svg-47b87f3e857516f74642fe118e831480.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/cancel.svg"
|
||||
dest_files=["res://.godot/imported/cancel.svg-48b548656752953c76fdbf1fa691b961.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/cancel.svg"
|
||||
dest_files=["res://.godot/imported/cancel.svg-47b87f3e857516f74642fe118e831480.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 315 B After Width: | Height: | Size: 315 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cjc2d332bqgfy"
|
||||
path="res://.godot/imported/collision.svg-c665b3652a42642be8f703d82faa449e.ctex"
|
||||
uid="uid://blcmsiuegfhy"
|
||||
path="res://.godot/imported/collision.svg-e4cf0320ea5b34c663eb051e13179fb1.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/collision.svg"
|
||||
dest_files=["res://.godot/imported/collision.svg-c665b3652a42642be8f703d82faa449e.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/collision.svg"
|
||||
dest_files=["res://.godot/imported/collision.svg-e4cf0320ea5b34c663eb051e13179fb1.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 259 B After Width: | Height: | Size: 259 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://0jckxct26pqs"
|
||||
path="res://.godot/imported/gable_wall_right.svg-3336022dcfa05da5960f0dc8e3ecc6ff.ctex"
|
||||
uid="uid://d4iv2qc3e6k8f"
|
||||
path="res://.godot/imported/create_vertex.svg-b23af865d7a800b0e078b26b2e99a9f7.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/gable_wall_right.svg"
|
||||
dest_files=["res://.godot/imported/gable_wall_right.svg-3336022dcfa05da5960f0dc8e3ecc6ff.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/create_vertex.svg"
|
||||
dest_files=["res://.godot/imported/create_vertex.svg-b23af865d7a800b0e078b26b2e99a9f7.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 265 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://booy2vg0ci6p7"
|
||||
path="res://.godot/imported/door.svg-5067a7f9805041b1e0019db73b7f247e.ctex"
|
||||
uid="uid://shrvgd57wqmy"
|
||||
path="res://.godot/imported/door.svg-abe4c0258238339b0ce092292a14a613.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/door.svg"
|
||||
dest_files=["res://.godot/imported/door.svg-5067a7f9805041b1e0019db73b7f247e.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/door.svg"
|
||||
dest_files=["res://.godot/imported/door.svg-abe4c0258238339b0ce092292a14a613.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 404 B After Width: | Height: | Size: 404 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://62ci7tsy4yeq"
|
||||
path="res://.godot/imported/fence.svg-b115d5a88a7e382552216868149e7186.ctex"
|
||||
uid="uid://bmvaiklprw3ob"
|
||||
path="res://.godot/imported/fence.svg-4eb777531b4c56b60ad89e9e327e738c.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/fence.svg"
|
||||
dest_files=["res://.godot/imported/fence.svg-b115d5a88a7e382552216868149e7186.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/fence.svg"
|
||||
dest_files=["res://.godot/imported/fence.svg-4eb777531b4c56b60ad89e9e327e738c.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 298 B After Width: | Height: | Size: 298 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bey5yf5dc53ms"
|
||||
path="res://.godot/imported/floor_square.svg-c870ce008800866d2fb4f4c19615fe80.ctex"
|
||||
uid="uid://b11j8aumv12dr"
|
||||
path="res://.godot/imported/floor_square.svg-929ec0d7e4700eccd8871b9e7bdc53c8.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/floor_square.svg"
|
||||
dest_files=["res://.godot/imported/floor_square.svg-c870ce008800866d2fb4f4c19615fe80.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/floor_square.svg"
|
||||
dest_files=["res://.godot/imported/floor_square.svg-929ec0d7e4700eccd8871b9e7bdc53c8.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 291 B After Width: | Height: | Size: 291 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://3df01la6lxht"
|
||||
path="res://.godot/imported/foundation_square.svg-23bf2f453c993134ea4ea05145d98488.ctex"
|
||||
uid="uid://blm8vvelfa0ov"
|
||||
path="res://.godot/imported/floor_triangle.svg-dccf4703c638fbb44617d82da92f981f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/foundation_square.svg"
|
||||
dest_files=["res://.godot/imported/foundation_square.svg-23bf2f453c993134ea4ea05145d98488.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/floor_triangle.svg"
|
||||
dest_files=["res://.godot/imported/floor_triangle.svg-dccf4703c638fbb44617d82da92f981f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 286 B |
43
addons/modular_builder_en/icons/foundation_square.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bdg64bvathtkp"
|
||||
path="res://.godot/imported/foundation_square.svg-dc8be81ca55b382cac6eed9723c4ea7c.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/foundation_square.svg"
|
||||
dest_files=["res://.godot/imported/foundation_square.svg-dc8be81ca55b382cac6eed9723c4ea7c.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
|
Before Width: | Height: | Size: 285 B After Width: | Height: | Size: 285 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ln0jh5ibvhe0"
|
||||
path="res://.godot/imported/foundation_triangle.svg-4b3e87f2198d6b6a8427029009457134.ctex"
|
||||
uid="uid://c7sts0wrdier2"
|
||||
path="res://.godot/imported/foundation_triangle.svg-c2747d1fa5be8206a98f02f2c53db66f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/foundation_triangle.svg"
|
||||
dest_files=["res://.godot/imported/foundation_triangle.svg-4b3e87f2198d6b6a8427029009457134.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/foundation_triangle.svg"
|
||||
dest_files=["res://.godot/imported/foundation_triangle.svg-c2747d1fa5be8206a98f02f2c53db66f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 262 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b16auokjgcjuv"
|
||||
path="res://.godot/imported/gable_wall.svg-1d82415321f6b6ad59f51ba7640a6c53.ctex"
|
||||
uid="uid://420a0ay7eep0"
|
||||
path="res://.godot/imported/gable_wall.svg-c2d60f6ba4255837e31c628621486d99.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/gable_wall.svg"
|
||||
dest_files=["res://.godot/imported/gable_wall.svg-1d82415321f6b6ad59f51ba7640a6c53.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/gable_wall.svg"
|
||||
dest_files=["res://.godot/imported/gable_wall.svg-c2d60f6ba4255837e31c628621486d99.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
43
addons/modular_builder_en/icons/gable_wall_right.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cbap2sd7o8fwg"
|
||||
path="res://.godot/imported/gable_wall_right.svg-ad644706ef5497d44f89d17f68ec357c.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/gable_wall_right.svg"
|
||||
dest_files=["res://.godot/imported/gable_wall_right.svg-ad644706ef5497d44f89d17f68ec357c.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
|
Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 276 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cf2okj2f2eq1h"
|
||||
path="res://.godot/imported/hatch.svg-f0ee2bc7f824245d3082af2deb5cb59a.ctex"
|
||||
uid="uid://bhqb8ky2bus1f"
|
||||
path="res://.godot/imported/hatch.svg-0413a819fca720a8bc1e64dc6ad76826.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/hatch.svg"
|
||||
dest_files=["res://.godot/imported/hatch.svg-f0ee2bc7f824245d3082af2deb5cb59a.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/hatch.svg"
|
||||
dest_files=["res://.godot/imported/hatch.svg-0413a819fca720a8bc1e64dc6ad76826.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 293 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dqhgwj72rp5o1"
|
||||
path="res://.godot/imported/load_module.svg-308877482b25606c5b7ed43292d640f3.ctex"
|
||||
uid="uid://yesw2vj8yaah"
|
||||
path="res://.godot/imported/load_module.svg-63e2b3e2ac8c24bc991fcf4e75ace01b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/load_module.svg"
|
||||
dest_files=["res://.godot/imported/load_module.svg-308877482b25606c5b7ed43292d640f3.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/load_module.svg"
|
||||
dest_files=["res://.godot/imported/load_module.svg-63e2b3e2ac8c24bc991fcf4e75ace01b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 460 B After Width: | Height: | Size: 460 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dghymeocq75is"
|
||||
path="res://.godot/imported/floor_triangle.svg-cbffdc894d9762c4cb9806dbfa52405c.ctex"
|
||||
uid="uid://1amy53ump51l"
|
||||
path="res://.godot/imported/merge_vertices.svg-5ece4f02ab55d6738ab8836b942201e1.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/floor_triangle.svg"
|
||||
dest_files=["res://.godot/imported/floor_triangle.svg-cbffdc894d9762c4cb9806dbfa52405c.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/merge_vertices.svg"
|
||||
dest_files=["res://.godot/imported/merge_vertices.svg-5ece4f02ab55d6738ab8836b942201e1.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 351 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://n7jw3p1fa5eu"
|
||||
path="res://.godot/imported/merge_vertices.svg-d9204aed762a783069511285341aaecc.ctex"
|
||||
uid="uid://cfh7b0kxdwl6i"
|
||||
path="res://.godot/imported/move_vertex.svg-ceb416f7c4a6671d49b72b5d7058c829.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/merge_vertices.svg"
|
||||
dest_files=["res://.godot/imported/merge_vertices.svg-d9204aed762a783069511285341aaecc.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/move_vertex.svg"
|
||||
dest_files=["res://.godot/imported/move_vertex.svg-ceb416f7c4a6671d49b72b5d7058c829.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 275 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bkxxyh8ec5oed"
|
||||
path="res://.godot/imported/parapet.svg-f91880fefed49e8dc8e533dcad6bb584.ctex"
|
||||
uid="uid://dl55t56w8agod"
|
||||
path="res://.godot/imported/parapet.svg-ed0a5acaa465eefbbe527d0d92ac41e6.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/parapet.svg"
|
||||
dest_files=["res://.godot/imported/parapet.svg-f91880fefed49e8dc8e533dcad6bb584.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/parapet.svg"
|
||||
dest_files=["res://.godot/imported/parapet.svg-ed0a5acaa465eefbbe527d0d92ac41e6.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dukm4mprtq7l"
|
||||
path="res://.godot/imported/pillar.svg-ccd9ad9da9982abc939a9856fa296417.ctex"
|
||||
uid="uid://bsvap0ukvvtxl"
|
||||
path="res://.godot/imported/pillar.svg-0acb51ea7c090f4767757e19389c7697.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/pillar.svg"
|
||||
dest_files=["res://.godot/imported/pillar.svg-ccd9ad9da9982abc939a9856fa296417.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/pillar.svg"
|
||||
dest_files=["res://.godot/imported/pillar.svg-0acb51ea7c090f4767757e19389c7697.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 250 B After Width: | Height: | Size: 250 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bjvg4halxhoy5"
|
||||
path="res://.godot/imported/remove_vertex.svg-824eacf485182fddca49a06831d65906.ctex"
|
||||
uid="uid://d3smkmuij3tiu"
|
||||
path="res://.godot/imported/remove_vertex.svg-a2650c4a5b88e73778470ad394dd8d24.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/remove_vertex.svg"
|
||||
dest_files=["res://.godot/imported/remove_vertex.svg-824eacf485182fddca49a06831d65906.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/remove_vertex.svg"
|
||||
dest_files=["res://.godot/imported/remove_vertex.svg-a2650c4a5b88e73778470ad394dd8d24.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dvtqw0h5dd5yl"
|
||||
path="res://.godot/imported/roof_gable.svg-8b4da12734ae02486f4e916ee6f2b16d.ctex"
|
||||
uid="uid://7mg7tjvygg82"
|
||||
path="res://.godot/imported/roof_gable.svg-19c7e85e7be7616ba9781f8a0b3ca1c3.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/roof_gable.svg"
|
||||
dest_files=["res://.godot/imported/roof_gable.svg-8b4da12734ae02486f4e916ee6f2b16d.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/roof_gable.svg"
|
||||
dest_files=["res://.godot/imported/roof_gable.svg-19c7e85e7be7616ba9781f8a0b3ca1c3.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 306 B After Width: | Height: | Size: 306 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ccew6wsdsj0kn"
|
||||
path="res://.godot/imported/roof_pyramid.svg-97d144eff93367622d2cd47f9133b57d.ctex"
|
||||
uid="uid://55uqhamplshs"
|
||||
path="res://.godot/imported/roof_pyramid.svg-9e1f692744fe6843f12da4ad40b1e7fd.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/roof_pyramid.svg"
|
||||
dest_files=["res://.godot/imported/roof_pyramid.svg-97d144eff93367622d2cd47f9133b57d.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/roof_pyramid.svg"
|
||||
dest_files=["res://.godot/imported/roof_pyramid.svg-9e1f692744fe6843f12da4ad40b1e7fd.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 236 B After Width: | Height: | Size: 236 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://wyg331tl3rdt"
|
||||
path="res://.godot/imported/roof_shed.svg-47c9ee22596d5277674eb88f312c9478.ctex"
|
||||
uid="uid://dl0xf605xhwdl"
|
||||
path="res://.godot/imported/roof_shed.svg-6a66b1c82d6d6e005d37c43a5b9009df.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/roof_shed.svg"
|
||||
dest_files=["res://.godot/imported/roof_shed.svg-47c9ee22596d5277674eb88f312c9478.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/roof_shed.svg"
|
||||
dest_files=["res://.godot/imported/roof_shed.svg-6a66b1c82d6d6e005d37c43a5b9009df.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 329 B After Width: | Height: | Size: 329 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cxcb3kjn18udy"
|
||||
path="res://.godot/imported/rotate.svg-84e58dd8fc90e4d634e7fbc55f8eda70.ctex"
|
||||
uid="uid://dhi55x4bo0dmp"
|
||||
path="res://.godot/imported/rotate.svg-2f2c1519ec865048747f49f7cbeed3c8.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/rotate.svg"
|
||||
dest_files=["res://.godot/imported/rotate.svg-84e58dd8fc90e4d634e7fbc55f8eda70.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/rotate.svg"
|
||||
dest_files=["res://.godot/imported/rotate.svg-2f2c1519ec865048747f49f7cbeed3c8.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 293 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://pnq0qqniy5hx"
|
||||
path="res://.godot/imported/save_module.svg-eecd37f83ae3c8dc3d40dabc8e8213e4.ctex"
|
||||
uid="uid://di37e5sjuktph"
|
||||
path="res://.godot/imported/save_module.svg-de34a0b11c2e62ba100617a23de20ad2.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/save_module.svg"
|
||||
dest_files=["res://.godot/imported/save_module.svg-eecd37f83ae3c8dc3d40dabc8e8213e4.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/save_module.svg"
|
||||
dest_files=["res://.godot/imported/save_module.svg-de34a0b11c2e62ba100617a23de20ad2.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 355 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b6ibyaujhybao"
|
||||
path="res://.godot/imported/stairs.svg-9b9203b050bb5cf817168a5866b66b2a.ctex"
|
||||
uid="uid://bl85bimtdaffm"
|
||||
path="res://.godot/imported/stairs.svg-8a83477aca2bfaf1ecd92ddcb3d7ad52.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/stairs.svg"
|
||||
dest_files=["res://.godot/imported/stairs.svg-9b9203b050bb5cf817168a5866b66b2a.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/stairs.svg"
|
||||
dest_files=["res://.godot/imported/stairs.svg-8a83477aca2bfaf1ecd92ddcb3d7ad52.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 267 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cmkpq8teurcwt"
|
||||
path="res://.godot/imported/wall.svg-b57c613ae16e9db4887d98c30036dee9.ctex"
|
||||
uid="uid://c61wb4mnf0wls"
|
||||
path="res://.godot/imported/wall.svg-25deeb903c389412115a2f457cdde263.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/wall.svg"
|
||||
dest_files=["res://.godot/imported/wall.svg-b57c613ae16e9db4887d98c30036dee9.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/wall.svg"
|
||||
dest_files=["res://.godot/imported/wall.svg-25deeb903c389412115a2f457cdde263.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 222 B After Width: | Height: | Size: 222 B |
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cu21qr4i7dns1"
|
||||
path="res://.godot/imported/window.svg-b79f89a5f56cfe779422a43f44ba067f.ctex"
|
||||
uid="uid://np7qscb7nf3s"
|
||||
path="res://.godot/imported/window.svg-ac8dc7e4787d27b79bd94a1bd7b1f8f8.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/window.svg"
|
||||
dest_files=["res://.godot/imported/window.svg-b79f89a5f56cfe779422a43f44ba067f.ctex"]
|
||||
source_file="res://addons/modular_builder_en/icons/window.svg"
|
||||
dest_files=["res://.godot/imported/window.svg-ac8dc7e4787d27b79bd94a1bd7b1f8f8.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder/modular_builder_editor_ui.gd"
|
||||
extends "res://addons/modular_builder_en/modular_builder_editor_ui.gd"
|
||||
|
||||
## ScopedVR-Bibliothek-Export (Karte t_82d2a8dd): dritter Export-Modus
|
||||
## „In ScopedVR-Bibliothek" — gebackenes Mesh + Trimesh-Kollision durch die
|
||||
## Objekt-Pipeline (tools/objekt im Match-Server-Repo), Name-Dialog inklusive.
|
||||
const SCOPEDVR_EXPORT_UI := preload("res://addons/modular_builder/scopedvr_export_ui.gd")
|
||||
const SCOPEDVR_EXPORT_UI := preload("res://addons/modular_builder_en/scopedvr_export_ui.gd")
|
||||
var _scopedvr_export: RefCounted = null
|
||||
|
||||
func _add_piece() -> void:
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder/modular_builder_editor_actions.gd"
|
||||
extends "res://addons/modular_builder_en/modular_builder_editor_actions.gd"
|
||||
|
||||
const PILLAR_SUPPORT = preload("res://addons/modular_builder/modular_pillar_support.gd")
|
||||
const VERTEX_SIMPLIFIER = preload("res://addons/modular_builder/modular_vertex_simplifier.gd")
|
||||
const PILLAR_SUPPORT = preload("res://addons/modular_builder_en/modular_pillar_support.gd")
|
||||
const VERTEX_SIMPLIFIER = preload("res://addons/modular_builder_en/modular_vertex_simplifier.gd")
|
||||
|
||||
func _create_detail_from_click(camera: Camera3D, screen_position: Vector2) -> bool:
|
||||
var placement := _find_detail_placement(camera, screen_position)
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const PIECE_SCRIPT = preload("res://addons/modular_builder/modular_piece.gd")
|
||||
const DETAIL_SCRIPT = preload("res://core/vendor/modular_builder/modular_building_detail.gd")
|
||||
const MESH_BAKER = preload("res://addons/modular_builder/modular_mesh_baker.gd")
|
||||
const PIECE_SCRIPT = preload("res://addons/modular_builder_en/modular_piece.gd")
|
||||
const DETAIL_SCRIPT = preload("res://addons/modular_builder_en/modular_building_detail.gd")
|
||||
const MESH_BAKER = preload("res://addons/modular_builder_en/modular_mesh_baker.gd")
|
||||
const TOOL_NONE := ""
|
||||
const TOOL_FOUNDATION := "foundation"
|
||||
const TOOL_FOUNDATION_SQUARE := "foundation_square"
|
||||
|
|
@ -133,19 +133,19 @@ func _enter_tree() -> void:
|
|||
size_spin.suffix = " m"
|
||||
size_spin.custom_minimum_size = Vector2(100, 0)
|
||||
_creation_row.add_child(size_spin)
|
||||
var create_button := _make_icon_button("res://addons/modular_builder/icons/add_building.svg", "Add ModularBuilderPiece", Color(0.22, 0.30, 0.38))
|
||||
var create_button := _make_icon_button("res://addons/modular_builder_en/icons/add_building.svg", "Add ModularBuilderPiece", Color(0.22, 0.30, 0.38))
|
||||
create_button.pressed.connect(Callable(self, "_add_piece"))
|
||||
_creation_row.add_child(create_button)
|
||||
var rotate_button := _make_icon_button("res://addons/modular_builder/icons/rotate.svg", "Rotate selected piece 90°", Color(0.22, 0.30, 0.38))
|
||||
var rotate_button := _make_icon_button("res://addons/modular_builder_en/icons/rotate.svg", "Rotate selected piece 90°", Color(0.22, 0.30, 0.38))
|
||||
rotate_button.pressed.connect(Callable(self, "_rotate_selected_piece"))
|
||||
_creation_row.add_child(rotate_button)
|
||||
var collision_button := _make_icon_button("res://addons/modular_builder/icons/collision.svg", "Add general collision to selected structure", Color(0.22, 0.30, 0.38))
|
||||
var collision_button := _make_icon_button("res://addons/modular_builder_en/icons/collision.svg", "Add general collision to selected structure", Color(0.22, 0.30, 0.38))
|
||||
collision_button.pressed.connect(Callable(self, "_add_general_collision"))
|
||||
_creation_row.add_child(collision_button)
|
||||
var save_module_button := _make_icon_button("res://addons/modular_builder/icons/save_module.svg", "Save selected structure as a .tscn module", Color(0.22, 0.30, 0.38))
|
||||
var save_module_button := _make_icon_button("res://addons/modular_builder_en/icons/save_module.svg", "Save selected structure as a .tscn module", Color(0.22, 0.30, 0.38))
|
||||
save_module_button.pressed.connect(Callable(self, "_show_save_module_dialog"))
|
||||
_creation_row.add_child(save_module_button)
|
||||
var load_module_button := _make_icon_button("res://addons/modular_builder/icons/load_module.svg", "Insert saved module into the scene", Color(0.22, 0.30, 0.38))
|
||||
var load_module_button := _make_icon_button("res://addons/modular_builder_en/icons/load_module.svg", "Insert saved module into the scene", Color(0.22, 0.30, 0.38))
|
||||
load_module_button.pressed.connect(Callable(self, "_show_load_module_dialog"))
|
||||
_creation_row.add_child(load_module_button)
|
||||
_export_row = HFlowContainer.new()
|
||||
|
|
@ -166,33 +166,33 @@ func _enter_tree() -> void:
|
|||
dock.add_child(_tool_row)
|
||||
_tool_button_group = ButtonGroup.new()
|
||||
_tool_button_group.allow_unpress = true
|
||||
_add_tool_button("res://addons/modular_builder/icons/foundation_square.svg", "Square foundation", TOOL_FOUNDATION_SQUARE, Color(0.08, 0.48, 0.22))
|
||||
_add_tool_button("res://addons/modular_builder/icons/foundation_triangle.svg", "Triangular foundation", TOOL_FOUNDATION_TRIANGLE, Color(0.08, 0.48, 0.22))
|
||||
_add_tool_button("res://addons/modular_builder/icons/wall.svg", "Wall", TOOL_WALL, Color(0.65, 0.12, 0.12))
|
||||
_add_tool_button("res://addons/modular_builder/icons/gable_wall.svg", "Gable wall", TOOL_GABLE_WALL, Color(0.65, 0.12, 0.12))
|
||||
_add_tool_button("res://addons/modular_builder/icons/gable_wall_right.svg", "Right triangular gable wall", TOOL_GABLE_WALL_RIGHT, Color(0.65, 0.12, 0.12))
|
||||
_add_tool_button("res://addons/modular_builder/icons/floor_square.svg", "Square floor", TOOL_FLOOR_SQUARE, Color(0.68, 0.48, 0.06))
|
||||
_add_tool_button("res://addons/modular_builder/icons/floor_triangle.svg", "Triangular floor", TOOL_FLOOR_TRIANGLE, Color(0.68, 0.48, 0.06))
|
||||
_add_tool_button("res://addons/modular_builder/icons/roof_gable.svg", "Gable roof", TOOL_ROOF_GABLE, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder/icons/roof_shed.svg", "Shed roof", TOOL_ROOF_SHED, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder/icons/roof_pyramid.svg", "Pyramid roof", TOOL_ROOF_PYRAMID, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder/icons/stairs.svg", "Stairs", TOOL_STAIRS, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder/icons/fence.svg", "Decorative fence", TOOL_FENCE, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder/icons/parapet.svg", "Parapet: single rail and posts", TOOL_PARAPET, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder/icons/window.svg", "Detail: window", TOOL_WINDOW, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder/icons/door.svg", "Detail: door", TOOL_DOOR, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder/icons/hatch.svg", "Detail: hatch", TOOL_HATCH, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder/icons/pillar.svg", "Plain pillar: two vertices or Alt+click for a vertical pillar", TOOL_PILLAR, Color(0.88, 0.22, 0.55))
|
||||
var cancel_button := _make_icon_button("res://addons/modular_builder/icons/cancel.svg", "Deactivate tool", Color(0.34, 0.20, 0.20))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/foundation_square.svg", "Square foundation", TOOL_FOUNDATION_SQUARE, Color(0.08, 0.48, 0.22))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/foundation_triangle.svg", "Triangular foundation", TOOL_FOUNDATION_TRIANGLE, Color(0.08, 0.48, 0.22))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/wall.svg", "Wall", TOOL_WALL, Color(0.65, 0.12, 0.12))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/gable_wall.svg", "Gable wall", TOOL_GABLE_WALL, Color(0.65, 0.12, 0.12))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/gable_wall_right.svg", "Right triangular gable wall", TOOL_GABLE_WALL_RIGHT, Color(0.65, 0.12, 0.12))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/floor_square.svg", "Square floor", TOOL_FLOOR_SQUARE, Color(0.68, 0.48, 0.06))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/floor_triangle.svg", "Triangular floor", TOOL_FLOOR_TRIANGLE, Color(0.68, 0.48, 0.06))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/roof_gable.svg", "Gable roof", TOOL_ROOF_GABLE, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/roof_shed.svg", "Shed roof", TOOL_ROOF_SHED, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/roof_pyramid.svg", "Pyramid roof", TOOL_ROOF_PYRAMID, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/stairs.svg", "Stairs", TOOL_STAIRS, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/fence.svg", "Decorative fence", TOOL_FENCE, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/parapet.svg", "Parapet: single rail and posts", TOOL_PARAPET, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/window.svg", "Detail: window", TOOL_WINDOW, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/door.svg", "Detail: door", TOOL_DOOR, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/hatch.svg", "Detail: hatch", TOOL_HATCH, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/pillar.svg", "Plain pillar: two vertices or Alt+click for a vertical pillar", TOOL_PILLAR, Color(0.88, 0.22, 0.55))
|
||||
var cancel_button := _make_icon_button("res://addons/modular_builder_en/icons/cancel.svg", "Deactivate tool", Color(0.34, 0.20, 0.20))
|
||||
cancel_button.pressed.connect(Callable(self, "_set_tool").bind(TOOL_NONE))
|
||||
_tool_row.add_child(cancel_button)
|
||||
_vertex_edit_row = HFlowContainer.new()
|
||||
dock.add_child(_vertex_edit_row)
|
||||
_vertex_edit_row.add_child(_label("Vertices:"))
|
||||
_add_tool_button("res://addons/modular_builder/icons/merge_vertices.svg", "Merge vertices to reduce markers", TOOL_MERGE_VERTICES, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
_add_tool_button("res://addons/modular_builder/icons/create_vertex.svg", "Create a connection vertex on the clicked surface", TOOL_CREATE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
_add_tool_button("res://addons/modular_builder/icons/move_vertex.svg", "Move a manual connection vertex", TOOL_MOVE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
_add_tool_button("res://addons/modular_builder/icons/remove_vertex.svg", "Remove a manual connection vertex", TOOL_REMOVE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/merge_vertices.svg", "Merge vertices to reduce markers", TOOL_MERGE_VERTICES, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/create_vertex.svg", "Create a connection vertex on the clicked surface", TOOL_CREATE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/move_vertex.svg", "Move a manual connection vertex", TOOL_MOVE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/remove_vertex.svg", "Remove a manual connection vertex", TOOL_REMOVE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
var marker_options_row := HFlowContainer.new()
|
||||
dock.add_child(marker_options_row)
|
||||
_marker_scope_check = CheckBox.new()
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder/modular_builder_editor_details.gd"
|
||||
extends "res://addons/modular_builder_en/modular_builder_editor_details.gd"
|
||||
|
||||
const MESH_PICKER = preload("res://addons/modular_builder/modular_mesh_picker.gd")
|
||||
const MESH_PICKER = preload("res://addons/modular_builder_en/modular_mesh_picker.gd")
|
||||
|
||||
func _sync_vertex_markers() -> void:
|
||||
var root := _get_selected_piece()
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder/modular_builder_editor_vertices.gd"
|
||||
extends "res://addons/modular_builder_en/modular_builder_editor_vertices.gd"
|
||||
|
||||
const ROOF_SUPPORT = preload("res://addons/modular_builder/modular_roof_support.gd")
|
||||
const ROOF_SUPPORT = preload("res://addons/modular_builder_en/modular_roof_support.gd")
|
||||
|
||||
func _forward_3d_gui_input(camera: Camera3D, event: InputEvent) -> int:
|
||||
if active_tool != TOOL_NONE and event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
284
addons/modular_builder_en/modular_building_detail.gd
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
@tool
|
||||
class_name ModularBuilderDetail
|
||||
extends Node3D
|
||||
|
||||
enum DetailType { WINDOW, DOOR, HATCH, PILLAR, FENCE, PARAPET }
|
||||
enum DetailMode { OPENING, SEPARATE_OBJECT }
|
||||
enum WindowStyle { SINGLE_PANE, CROSS_PANE }
|
||||
enum DoorStyle { FLAT, PANELED }
|
||||
|
||||
@export var detail_type: DetailType = DetailType.WINDOW:
|
||||
set(value):
|
||||
detail_type = value
|
||||
notify_property_list_changed()
|
||||
_rebuild()
|
||||
@export var detail_mode: DetailMode = DetailMode.OPENING:
|
||||
set(value):
|
||||
detail_mode = value
|
||||
notify_property_list_changed()
|
||||
_rebuild()
|
||||
@export var center_align: bool = false:
|
||||
set(value):
|
||||
center_align = value
|
||||
_apply_pillar_alignment()
|
||||
notify_property_list_changed()
|
||||
@export_storage var pillar_direction: Vector3 = Vector3.UP
|
||||
@export_storage var pillar_alignment_basis: Basis = Basis.IDENTITY
|
||||
@export var window_style: WindowStyle = WindowStyle.SINGLE_PANE:
|
||||
set(value):
|
||||
window_style = value
|
||||
_rebuild()
|
||||
@export var door_style: DoorStyle = DoorStyle.FLAT:
|
||||
set(value):
|
||||
door_style = value
|
||||
_rebuild()
|
||||
@export_range(0.01, 5.0, 0.001, "suffix:m") var width: float = 1.0:
|
||||
set(value):
|
||||
width = value
|
||||
_rebuild()
|
||||
@export_range(0.2, 5.0, 0.001, "suffix:m") var height: float = 1.0:
|
||||
set(value):
|
||||
height = value
|
||||
_rebuild()
|
||||
@export_range(0.02, 0.5, 0.001, "suffix:m") var frame_depth: float = 0.08:
|
||||
set(value):
|
||||
frame_depth = value
|
||||
_rebuild()
|
||||
@export_range(0.1, 8.0, 0.001, "suffix:m") var fence_height: float = 0.7:
|
||||
set(value):
|
||||
fence_height = value
|
||||
_rebuild()
|
||||
@export_range(0, 100, 1, "suffix: posts") var fence_divisions: int = 1:
|
||||
set(value):
|
||||
fence_divisions = value
|
||||
_rebuild()
|
||||
@export_storage var path_points: PackedVector3Array = PackedVector3Array()
|
||||
@export_range(0.01, 0.5, 0.001, "suffix:m") var frame_width: float = 0.08:
|
||||
set(value):
|
||||
frame_width = value
|
||||
_rebuild()
|
||||
@export var follow_host_thickness: bool = true:
|
||||
set(value):
|
||||
follow_host_thickness = value
|
||||
_rebuild()
|
||||
@export var surface_material: Material:
|
||||
set(value):
|
||||
surface_material = value
|
||||
_rebuild()
|
||||
@export var frame_material: Material:
|
||||
set(value):
|
||||
frame_material = value
|
||||
_rebuild()
|
||||
@export var panel_material: Material:
|
||||
set(value):
|
||||
panel_material = value
|
||||
_rebuild()
|
||||
|
||||
func _validate_property(property: Dictionary) -> void:
|
||||
var separate_object := detail_mode == DetailMode.SEPARATE_OBJECT
|
||||
var is_window := detail_type == DetailType.WINDOW
|
||||
var is_door := detail_type == DetailType.DOOR
|
||||
var has_frame_and_panel := separate_object and detail_type != DetailType.PILLAR and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
|
||||
var show_property := true
|
||||
match StringName(property.name):
|
||||
&"detail_mode":
|
||||
show_property = detail_type != DetailType.PILLAR and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
|
||||
&"center_align":
|
||||
show_property = detail_type == DetailType.PILLAR
|
||||
&"fence_height", &"fence_divisions":
|
||||
show_property = detail_type == DetailType.FENCE or detail_type == DetailType.PARAPET
|
||||
&"path_points":
|
||||
show_property = false
|
||||
&"width":
|
||||
show_property = detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
|
||||
if detail_type == DetailType.PILLAR:
|
||||
property.hint = PROPERTY_HINT_RANGE
|
||||
property.hint_string = "0.01,5.0,0.001,suffix:m"
|
||||
&"height":
|
||||
show_property = detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
|
||||
&"window_style":
|
||||
show_property = is_window and separate_object
|
||||
&"door_style":
|
||||
show_property = is_door and separate_object
|
||||
&"frame_width", &"frame_material", &"panel_material":
|
||||
show_property = has_frame_and_panel
|
||||
&"follow_host_thickness":
|
||||
show_property = has_frame_and_panel
|
||||
&"frame_depth":
|
||||
show_property = has_frame_and_panel and not follow_host_thickness
|
||||
&"surface_material":
|
||||
show_property = separate_object
|
||||
if not show_property:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
|
||||
func _ready() -> void:
|
||||
set_notify_transform(true)
|
||||
_rebuild()
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_TRANSFORM_CHANGED and is_inside_tree():
|
||||
var host := get_parent()
|
||||
if host != null and host.has_method("_rebuild"):
|
||||
host.call("_rebuild")
|
||||
|
||||
func _on_host_thickness_changed() -> void:
|
||||
_rebuild(false)
|
||||
|
||||
func _apply_pillar_alignment() -> void:
|
||||
if detail_type != DetailType.PILLAR:
|
||||
return
|
||||
var direction := pillar_direction.normalized()
|
||||
if direction.is_zero_approx():
|
||||
direction = Vector3.UP
|
||||
if center_align:
|
||||
var object_axes := pillar_alignment_basis.orthonormalized()
|
||||
var best_axis := direction
|
||||
var best_dot := -1.0
|
||||
for axis in [object_axes.x, object_axes.y, object_axes.z]:
|
||||
var normalized_axis: Vector3 = axis.normalized()
|
||||
var alignment := absf(direction.dot(normalized_axis))
|
||||
if alignment > best_dot:
|
||||
best_dot = alignment
|
||||
best_axis = normalized_axis if direction.dot(normalized_axis) >= 0.0 else -normalized_axis
|
||||
direction = best_axis
|
||||
var reference_axis := Vector3.FORWARD if absf(direction.dot(Vector3.UP)) > 0.99 else Vector3.UP
|
||||
var x_axis := reference_axis.cross(direction).normalized()
|
||||
var z_axis := x_axis.cross(direction).normalized()
|
||||
transform.basis = Basis(x_axis, direction, z_axis).orthonormalized()
|
||||
if center_align and pillar_direction.length() > 0.0:
|
||||
height = pillar_direction.length()
|
||||
|
||||
func _rebuild(notify_host: bool = true) -> void:
|
||||
if not is_inside_tree():
|
||||
return
|
||||
for child in get_children():
|
||||
if child.name == "DetailGeometry":
|
||||
remove_child(child)
|
||||
child.queue_free()
|
||||
var host := get_parent()
|
||||
if notify_host and host != null and host.has_method("_rebuild"):
|
||||
host.call("_rebuild")
|
||||
if detail_mode == DetailMode.OPENING and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET:
|
||||
return
|
||||
var holder := Node3D.new()
|
||||
holder.name = "DetailGeometry"
|
||||
add_child(holder)
|
||||
var scene_root: Node = null
|
||||
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
|
||||
scene_root = get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
holder.owner = scene_root
|
||||
var effective_depth := _effective_frame_depth()
|
||||
var frame := minf(maxf(frame_width, 0.01), minf(width, height) * 0.45)
|
||||
if detail_type == DetailType.FENCE or detail_type == DetailType.PARAPET:
|
||||
_build_fence_geometry(holder)
|
||||
return
|
||||
if detail_type == DetailType.PILLAR:
|
||||
_add_box(holder, "PillarBody", Vector3(width, height, width), Vector3.ZERO, surface_material)
|
||||
return
|
||||
if detail_type == DetailType.HATCH:
|
||||
_add_box(holder, "LeftFrame", Vector3(frame, effective_depth, height), Vector3(-width * 0.5 + frame * 0.5, 0.0, 0.0), _frame_material())
|
||||
_add_box(holder, "RightFrame", Vector3(frame, effective_depth, height), Vector3(width * 0.5 - frame * 0.5, 0.0, 0.0), _frame_material())
|
||||
_add_box(holder, "NearFrame", Vector3(width, effective_depth, frame), Vector3(0.0, 0.0, -height * 0.5 + frame * 0.5), _frame_material())
|
||||
_add_box(holder, "FarFrame", Vector3(width, effective_depth, frame), Vector3(0.0, 0.0, height * 0.5 - frame * 0.5), _frame_material())
|
||||
_add_box(holder, "HatchPanel", Vector3(width - frame * 2.0, effective_depth * 0.35, height - frame * 2.0), Vector3(0.0, effective_depth * 0.4, 0.0), _panel_material())
|
||||
return
|
||||
_add_box(holder, "LeftFrame", Vector3(frame, height, effective_depth), Vector3(-width * 0.5 + frame * 0.5, 0.0, 0.0), _frame_material())
|
||||
_add_box(holder, "RightFrame", Vector3(frame, height, effective_depth), Vector3(width * 0.5 - frame * 0.5, 0.0, 0.0), _frame_material())
|
||||
_add_box(holder, "TopFrame", Vector3(width, frame, effective_depth), Vector3(0.0, height * 0.5 - frame * 0.5, 0.0), _frame_material())
|
||||
if detail_type != DetailType.DOOR:
|
||||
_add_box(holder, "BottomFrame", Vector3(width, frame, effective_depth), Vector3(0.0, -height * 0.5 + frame * 0.5, 0.0), _frame_material())
|
||||
if detail_type == DetailType.WINDOW:
|
||||
_add_box(holder, "WindowPanel", Vector3(maxf(width - frame * 2.0, 0.001), maxf(height - frame * 2.0, 0.001), effective_depth * 0.35), Vector3.ZERO, _panel_material())
|
||||
if window_style == WindowStyle.CROSS_PANE:
|
||||
_add_box(holder, "VerticalMullion", Vector3(frame * 0.55, height - frame * 2.0, effective_depth * 0.4), Vector3.ZERO, _frame_material())
|
||||
_add_box(holder, "HorizontalMullion", Vector3(width - frame * 2.0, frame * 0.55, effective_depth * 0.4), Vector3.ZERO, _frame_material())
|
||||
elif detail_type == DetailType.DOOR:
|
||||
_add_box(holder, "DoorPanel", Vector3(width - frame * 2.0, height - frame, effective_depth * 0.45), Vector3(0.0, -frame * 0.5, 0.0), _panel_material())
|
||||
if door_style == DoorStyle.PANELED:
|
||||
var inset_depth := maxf(effective_depth * 0.04, 0.005)
|
||||
var panel_width := (width - frame * 2.0) * 0.82
|
||||
var panel_height := maxf((height - frame) * 0.28, 0.05)
|
||||
for panel_index in range(2):
|
||||
var panel_y := -frame * 0.5 + (panel_index - 0.5) * (panel_height + frame * 0.25)
|
||||
_add_box(holder, "DoorInset_%d" % panel_index, Vector3(panel_width, panel_height, inset_depth), Vector3(0.0, panel_y, effective_depth * 0.24), _frame_material())
|
||||
|
||||
func _effective_frame_depth() -> float:
|
||||
if follow_host_thickness:
|
||||
var host := get_parent()
|
||||
if host != null:
|
||||
var host_thickness = host.get("thickness")
|
||||
if host_thickness is float or host_thickness is int:
|
||||
var depth := float(host_thickness)
|
||||
if detail_type == DetailType.WINDOW or detail_type == DetailType.DOOR:
|
||||
depth *= 1.25
|
||||
return maxf(depth, 0.02)
|
||||
return frame_depth
|
||||
|
||||
func _build_fence_geometry(holder: Node3D) -> void:
|
||||
if path_points.size() < 2 or fence_height <= 0.0:
|
||||
return
|
||||
var start := path_points[0]
|
||||
var finish := path_points[1]
|
||||
var path := finish - start
|
||||
var path_length := path.length()
|
||||
if path_length < 0.05:
|
||||
return
|
||||
var path_axis := path / path_length
|
||||
var up_axis := Vector3.UP - path_axis * path_axis.dot(Vector3.UP)
|
||||
if up_axis.length_squared() < 0.001:
|
||||
up_axis = Vector3.FORWARD - path_axis * path_axis.dot(Vector3.FORWARD)
|
||||
up_axis = up_axis.normalized()
|
||||
var side_axis := path_axis.cross(up_axis).normalized()
|
||||
var post_width := 0.16 if detail_type == DetailType.PARAPET else 0.08
|
||||
var post_count := fence_divisions + 2
|
||||
var posts: Array[Vector3] = []
|
||||
for i in range(post_count):
|
||||
var base := start.lerp(finish, float(i) / float(post_count - 1))
|
||||
posts.append(base)
|
||||
_add_oriented_box(holder, "Post_%d" % i, base + up_axis * fence_height * 0.5, path_axis, up_axis, side_axis, Vector3(post_width, fence_height, post_width))
|
||||
for i in range(posts.size() - 1):
|
||||
var a := posts[i]
|
||||
var b := posts[i + 1]
|
||||
var segment := b - a
|
||||
var segment_length := segment.length()
|
||||
if detail_type == DetailType.PARAPET:
|
||||
_add_oriented_box(holder, "Rail_%d" % i, (a + b) * 0.5 + up_axis * fence_height * 0.78, segment.normalized(), up_axis, side_axis, Vector3(segment_length, post_width * 0.8, post_width * 0.8))
|
||||
else:
|
||||
for height_ratio in [0.28, 0.78]:
|
||||
_add_oriented_box(holder, "Rail_%d_%d" % [i, int(height_ratio * 100.0)], (a + b) * 0.5 + up_axis * fence_height * height_ratio, segment.normalized(), up_axis, side_axis, Vector3(segment_length, post_width, post_width))
|
||||
|
||||
func _add_oriented_box(holder: Node3D, node_name: String, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3, dimensions: Vector3) -> void:
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = node_name
|
||||
var box := BoxMesh.new()
|
||||
box.size = dimensions
|
||||
mesh_instance.mesh = box
|
||||
var basis := Basis(axis_x.normalized(), axis_y.normalized(), axis_z.normalized()).orthonormalized()
|
||||
mesh_instance.transform = Transform3D(basis, center)
|
||||
mesh_instance.material_override = surface_material
|
||||
holder.add_child(mesh_instance)
|
||||
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
|
||||
var scene_root := get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
mesh_instance.owner = scene_root
|
||||
|
||||
func _frame_material() -> Material:
|
||||
return frame_material if frame_material != null else surface_material
|
||||
|
||||
func _panel_material() -> Material:
|
||||
return panel_material if panel_material != null else surface_material
|
||||
|
||||
func _add_box(holder: Node3D, node_name: String, box_size: Vector3, box_position: Vector3, material: Material = null) -> void:
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = node_name
|
||||
var box := BoxMesh.new()
|
||||
box.size = box_size
|
||||
mesh_instance.mesh = box
|
||||
mesh_instance.position = box_position
|
||||
mesh_instance.material_override = material if material != null else surface_material
|
||||
holder.add_child(mesh_instance)
|
||||
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
|
||||
var scene_root := get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
mesh_instance.owner = scene_root
|
||||
1
addons/modular_builder_en/modular_building_detail.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dsttda167kuhi
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
@tool
|
||||
class_name ModularBuilderPiece
|
||||
extends "res://core/vendor/modular_builder/modular_piece_geometry.gd"
|
||||
extends "res://addons/modular_builder_en/modular_piece_geometry.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
_rebuild()
|
||||
748
addons/modular_builder_en/modular_piece_geometry.gd
Normal file
|
|
@ -0,0 +1,748 @@
|
|||
@tool
|
||||
class_name ModularBuilderPieceGeometry
|
||||
extends Node3D
|
||||
|
||||
const DETAIL_SCRIPT_PATH := "res://addons/modular_builder_en/modular_building_detail.gd"
|
||||
const DETAIL_TYPE_HATCH := 2
|
||||
const DETAIL_TYPE_PILLAR := 3
|
||||
|
||||
enum PieceType { FOUNDATION_SQUARE, FOUNDATION_TRIANGLE, FLOOR, WALL, BUILDING, GABLE_WALL, ROOF_GABLE, ROOF_SHED, GABLE_WALL_RIGHT, ROOF_PYRAMID, STAIRS, FENCE }
|
||||
|
||||
@export var piece_type: PieceType = PieceType.FOUNDATION_SQUARE:
|
||||
set(value):
|
||||
piece_type = value
|
||||
notify_property_list_changed()
|
||||
_rebuild()
|
||||
@export_range(0.5, 8.0, 0.001, "suffix:m") var unit_size: float = 2.0:
|
||||
set(value):
|
||||
unit_size = value
|
||||
_rebuild()
|
||||
@export_range(0.01, 1.0, 0.001, "suffix:m") var thickness: float = 0.2:
|
||||
set(value):
|
||||
thickness = value
|
||||
_rebuild()
|
||||
for child in get_children():
|
||||
if child.has_method("_on_host_thickness_changed"):
|
||||
child.call("_on_host_thickness_changed")
|
||||
@export_range(0.1, 8.0, 0.001, "suffix:m") var rise: float = 1.0:
|
||||
set(value):
|
||||
rise = value
|
||||
_rebuild()
|
||||
@export_range(0.01, 32.0, 0.001, "suffix:m") var roof_width: float = 2.0:
|
||||
set(value):
|
||||
roof_width = value
|
||||
_rebuild()
|
||||
@export_range(1, 100, 1, "suffix: steps") var stair_count: int = 10:
|
||||
set(value):
|
||||
stair_count = value
|
||||
_rebuild()
|
||||
@export_range(0.01, 8.0, 0.001, "suffix:m") var stair_width: float = 1.0:
|
||||
set(value):
|
||||
stair_width = value
|
||||
_rebuild()
|
||||
@export_range(0, 100, 1, "suffix: posts") var fence_divisions: int = 1:
|
||||
set(value):
|
||||
fence_divisions = value
|
||||
_rebuild()
|
||||
@export_range(0.1, 8.0, 0.001, "suffix:m") var fence_height: float = 1.0:
|
||||
set(value):
|
||||
fence_height = value
|
||||
_rebuild()
|
||||
@export var symmetric_dimensions: bool = true:
|
||||
set(value):
|
||||
if symmetric_dimensions == value:
|
||||
return
|
||||
if not value:
|
||||
_capture_mesh_dimensions()
|
||||
symmetric_dimensions = value
|
||||
notify_property_list_changed()
|
||||
if value:
|
||||
scale = Vector3.ONE
|
||||
else:
|
||||
_apply_dimension_scale()
|
||||
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_width: float = 2.0:
|
||||
set(value):
|
||||
dimension_width = value
|
||||
_apply_dimension_scale()
|
||||
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_height: float = 2.0:
|
||||
set(value):
|
||||
dimension_height = value
|
||||
_apply_dimension_scale()
|
||||
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_depth: float = 2.0:
|
||||
set(value):
|
||||
dimension_depth = value
|
||||
_apply_dimension_scale()
|
||||
@export_storage var _dimension_reference: Vector3 = Vector3.ZERO
|
||||
@export var center_align: bool = false:
|
||||
set(value):
|
||||
center_align = value
|
||||
_rebuild()
|
||||
@export var surface_material: Material:
|
||||
set(value):
|
||||
surface_material = value
|
||||
_rebuild()
|
||||
@export var custom_points: PackedVector3Array = PackedVector3Array():
|
||||
set(value):
|
||||
custom_points = value
|
||||
_rebuild()
|
||||
@export var centered_origin: bool = false:
|
||||
set(value):
|
||||
centered_origin = value
|
||||
_rebuild()
|
||||
|
||||
func _rebuild() -> void:
|
||||
pass
|
||||
|
||||
func _capture_mesh_dimensions() -> void:
|
||||
var geometry := get_node_or_null("Geometry") as MeshInstance3D
|
||||
if geometry == null or geometry.mesh == null:
|
||||
return
|
||||
var dimensions := geometry.mesh.get_aabb().size
|
||||
if dimensions.x > 0.001:
|
||||
dimension_width = dimensions.x
|
||||
if dimensions.y > 0.001:
|
||||
dimension_height = dimensions.y
|
||||
if dimensions.z > 0.001:
|
||||
dimension_depth = dimensions.z
|
||||
_dimension_reference = dimensions
|
||||
|
||||
func _apply_dimension_scale() -> void:
|
||||
if symmetric_dimensions or _dimension_reference.x <= 0.001 or _dimension_reference.y <= 0.001 or _dimension_reference.z <= 0.001:
|
||||
return
|
||||
var target := Vector3(dimension_width, dimension_height, dimension_depth)
|
||||
if piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE or piece_type == PieceType.FLOOR:
|
||||
target.y = _dimension_reference.y
|
||||
if piece_type == PieceType.WALL:
|
||||
target.z = _dimension_reference.z
|
||||
scale = Vector3(target.x / _dimension_reference.x, target.y / _dimension_reference.y, target.z / _dimension_reference.z)
|
||||
|
||||
func _make_mesh() -> ArrayMesh:
|
||||
if piece_type == PieceType.GABLE_WALL and custom_points.size() >= 2:
|
||||
return _make_gable_wall_mesh()
|
||||
if piece_type == PieceType.GABLE_WALL_RIGHT and custom_points.size() >= 2:
|
||||
return _make_right_gable_wall_mesh()
|
||||
if (piece_type == PieceType.ROOF_GABLE or piece_type == PieceType.ROOF_SHED or piece_type == PieceType.ROOF_PYRAMID) and custom_points.size() >= 2:
|
||||
return _make_roof_mesh()
|
||||
if piece_type == PieceType.STAIRS and custom_points.size() >= 2:
|
||||
return _make_stair_mesh()
|
||||
if piece_type == PieceType.FENCE and custom_points.size() >= 2:
|
||||
return _make_fence_mesh()
|
||||
if custom_points.size() >= 2 and piece_type == PieceType.WALL:
|
||||
return _make_custom_wall_mesh()
|
||||
if custom_points.size() >= 3 and piece_type == PieceType.FLOOR:
|
||||
return _make_custom_floor_mesh()
|
||||
if custom_points.size() >= 3 and (piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE):
|
||||
return _make_custom_foundation_mesh()
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var s := unit_size
|
||||
var polygon: Array[Vector2]
|
||||
match piece_type:
|
||||
PieceType.FOUNDATION_TRIANGLE:
|
||||
polygon = [Vector2(-s * 0.5, -s * 0.5), Vector2(s * 0.5, -s * 0.5), Vector2(-s * 0.5, s * 0.5)]
|
||||
_:
|
||||
polygon = [Vector2(-s * 0.5, -s * 0.5), Vector2(s * 0.5, -s * 0.5), Vector2(s * 0.5, s * 0.5), Vector2(-s * 0.5, s * 0.5)]
|
||||
var y0 := -thickness * 0.5 if centered_origin else 0.0
|
||||
var y1 := thickness * 0.5 if centered_origin else thickness
|
||||
if piece_type == PieceType.WALL:
|
||||
polygon = [Vector2(-s * 0.5, -thickness * 0.5), Vector2(s * 0.5, -thickness * 0.5), Vector2(s * 0.5, thickness * 0.5), Vector2(-s * 0.5, thickness * 0.5)]
|
||||
y0 = -s * 0.5 if centered_origin else 0.0
|
||||
y1 = s * 0.5 if centered_origin else s
|
||||
if piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE:
|
||||
var hatch_mesh := _make_hatch_surface_mesh(polygon, y1, y0)
|
||||
if hatch_mesh != null:
|
||||
return hatch_mesh
|
||||
for i in range(1, polygon.size() - 1):
|
||||
_add_tri(vertices, normals, Vector3(polygon[0].x, y1, polygon[0].y), Vector3(polygon[i + 1].x, y1, polygon[i + 1].y), Vector3(polygon[i].x, y1, polygon[i].y))
|
||||
_add_tri(vertices, normals, Vector3(polygon[0].x, y0, polygon[0].y), Vector3(polygon[i].x, y0, polygon[i].y), Vector3(polygon[i + 1].x, y0, polygon[i + 1].y))
|
||||
for i in range(polygon.size()):
|
||||
var j := (i + 1) % polygon.size()
|
||||
var a := polygon[i]
|
||||
var b := polygon[j]
|
||||
_add_tri(vertices, normals, Vector3(a.x, y0, a.y), Vector3(b.x, y0, b.y), Vector3(b.x, y1, b.y))
|
||||
_add_tri(vertices, normals, Vector3(a.x, y0, a.y), Vector3(b.x, y1, b.y), Vector3(a.x, y1, a.y))
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
var result := ArrayMesh.new()
|
||||
result.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return result
|
||||
|
||||
func _make_gable_wall_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var base_points := _get_centered_gable_base() if center_align else PackedVector3Array([custom_points[0], custom_points[1]])
|
||||
var a: Vector3 = base_points[0]
|
||||
var b: Vector3 = base_points[1]
|
||||
var direction := (b - a).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
|
||||
var peak := (a + b) * 0.5 + Vector3.UP * rise
|
||||
var front_a := a + side
|
||||
var front_b := b + side
|
||||
var front_peak := peak + side
|
||||
var back_a := a - side
|
||||
var back_b := b - side
|
||||
var back_peak := peak - side
|
||||
_add_tri(vertices, normals, front_a, front_b, front_peak)
|
||||
_add_tri(vertices, normals, back_b, back_a, back_peak)
|
||||
_add_quad(vertices, normals, front_a, back_a, back_b, front_b)
|
||||
_add_quad(vertices, normals, front_b, back_b, back_peak, front_peak)
|
||||
_add_quad(vertices, normals, front_peak, back_peak, back_a, front_a)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_right_gable_wall_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var base_points := _get_centered_gable_base() if center_align else PackedVector3Array([custom_points[0], custom_points[1]])
|
||||
var a: Vector3 = base_points[0]
|
||||
var b: Vector3 = base_points[1]
|
||||
var direction := (b - a).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
|
||||
var peak := b + Vector3.UP * rise
|
||||
var front_a := a + side
|
||||
var front_b := b + side
|
||||
var front_peak := peak + side
|
||||
var back_a := a - side
|
||||
var back_b := b - side
|
||||
var back_peak := peak - side
|
||||
_add_tri(vertices, normals, front_a, front_b, front_peak)
|
||||
_add_tri(vertices, normals, back_peak, back_b, back_a)
|
||||
_add_quad(vertices, normals, front_a, back_a, back_b, front_b)
|
||||
_add_quad(vertices, normals, front_b, back_b, back_peak, front_peak)
|
||||
_add_quad(vertices, normals, front_peak, back_peak, back_a, front_a)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_roof_mesh() -> ArrayMesh:
|
||||
if piece_type == PieceType.ROOF_SHED and custom_points.size() == 2:
|
||||
return _make_shed_roof_mesh()
|
||||
if custom_points.size() >= 4:
|
||||
return _make_oriented_roof_mesh()
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var first: Vector3 = custom_points[0]
|
||||
var second: Vector3 = custom_points[1]
|
||||
var min_x := minf(first.x, second.x)
|
||||
var max_x := maxf(first.x, second.x)
|
||||
var min_z := minf(first.z, second.z)
|
||||
var max_z := maxf(first.z, second.z)
|
||||
var span_x := max_x - min_x
|
||||
var span_z := max_z - min_z
|
||||
if span_x < 0.02 or span_z < 0.02:
|
||||
return _array_mesh(vertices, normals)
|
||||
var panels: Array = []
|
||||
if piece_type == PieceType.ROOF_GABLE:
|
||||
if span_x <= span_z:
|
||||
var ridge_x := (min_x + max_x) * 0.5
|
||||
panels.append([Vector3(min_x, 0.0, min_z), Vector3(min_x, 0.0, max_z), Vector3(ridge_x, rise, max_z), Vector3(ridge_x, rise, min_z)])
|
||||
panels.append([Vector3(ridge_x, rise, min_z), Vector3(ridge_x, rise, max_z), Vector3(max_x, 0.0, max_z), Vector3(max_x, 0.0, min_z)])
|
||||
else:
|
||||
var ridge_z := (min_z + max_z) * 0.5
|
||||
panels.append([Vector3(min_x, 0.0, min_z), Vector3(max_x, 0.0, min_z), Vector3(max_x, rise, ridge_z), Vector3(min_x, rise, ridge_z)])
|
||||
panels.append([Vector3(min_x, rise, ridge_z), Vector3(max_x, rise, ridge_z), Vector3(max_x, 0.0, max_z), Vector3(min_x, 0.0, max_z)])
|
||||
else:
|
||||
if span_x <= span_z:
|
||||
var low_x := first.x
|
||||
var high_x := second.x
|
||||
panels.append([Vector3(low_x, 0.0, min_z), Vector3(low_x, 0.0, max_z), Vector3(high_x, rise, max_z), Vector3(high_x, rise, min_z)])
|
||||
else:
|
||||
var low_z := first.z
|
||||
var high_z := second.z
|
||||
panels.append([Vector3(min_x, 0.0, low_z), Vector3(max_x, 0.0, low_z), Vector3(max_x, rise, high_z), Vector3(min_x, rise, high_z)])
|
||||
for panel in panels:
|
||||
_append_solid_panel(vertices, normals, panel)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_shed_roof_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var low: Vector3 = custom_points[0]
|
||||
var high: Vector3 = custom_points[1]
|
||||
var slope := high - low
|
||||
var horizontal := Vector3(slope.x, 0.0, slope.z)
|
||||
if horizontal.length_squared() < 0.0001 or roof_width <= 0.0:
|
||||
return _array_mesh(vertices, normals)
|
||||
var across := Vector3(horizontal.z, 0.0, -horizontal.x).normalized() * roof_width * 0.5
|
||||
var panel := [low - across, low + across, high + across, high - across]
|
||||
_append_solid_panel(vertices, normals, panel)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_oriented_roof_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var p0: Vector3 = custom_points[0]
|
||||
var p1: Vector3 = custom_points[1]
|
||||
var p2: Vector3 = custom_points[2]
|
||||
var p3: Vector3 = custom_points[3]
|
||||
var edge_01 := Vector2(p1.x - p0.x, p1.z - p0.z).length()
|
||||
var edge_12 := Vector2(p2.x - p1.x, p2.z - p1.z).length()
|
||||
var panels: Array = []
|
||||
if piece_type == PieceType.ROOF_GABLE:
|
||||
if edge_01 <= edge_12:
|
||||
var ridge_01 := (p0 + p1) * 0.5 + Vector3.UP * rise
|
||||
var ridge_23 := (p2 + p3) * 0.5 + Vector3.UP * rise
|
||||
panels.append([p0, p3, ridge_23, ridge_01])
|
||||
panels.append([ridge_01, ridge_23, p2, p1])
|
||||
else:
|
||||
var ridge_12 := (p1 + p2) * 0.5 + Vector3.UP * rise
|
||||
var ridge_30 := (p3 + p0) * 0.5 + Vector3.UP * rise
|
||||
panels.append([p0, p1, ridge_12, ridge_30])
|
||||
panels.append([ridge_30, ridge_12, p2, p3])
|
||||
elif piece_type == PieceType.ROOF_PYRAMID:
|
||||
var peak := (p0 + p1 + p2 + p3) * 0.25 + Vector3.UP * rise
|
||||
panels.append([p0, p1, peak])
|
||||
panels.append([p1, p2, peak])
|
||||
panels.append([p2, p3, peak])
|
||||
panels.append([p3, p0, peak])
|
||||
else:
|
||||
# The four points define the low and high edges. Keep their centers
|
||||
# fixed when adjusting both edge widths in the Inspector.
|
||||
var base_width := Vector2(p1.x - p0.x, p1.z - p0.z).length()
|
||||
if base_width > 0.001 and roof_width > 0.0:
|
||||
var direction := Vector3(p1.x - p0.x, 0.0, p1.z - p0.z).normalized()
|
||||
var low_center := (p0 + p1) * 0.5
|
||||
var high_center := (p2 + p3) * 0.5
|
||||
var half_width := roof_width * 0.5
|
||||
panels.append([
|
||||
low_center - direction * half_width,
|
||||
low_center + direction * half_width,
|
||||
high_center + direction * half_width,
|
||||
high_center - direction * half_width,
|
||||
])
|
||||
else:
|
||||
panels.append([p0, p1, p2, p3])
|
||||
for panel in panels:
|
||||
_append_solid_panel(vertices, normals, panel)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_stair_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var start: Vector3 = custom_points[0]
|
||||
var finish: Vector3 = custom_points[1]
|
||||
var horizontal := Vector3(finish.x - start.x, 0.0, finish.z - start.z)
|
||||
var run := horizontal.length()
|
||||
var rise_amount := finish.y - start.y
|
||||
if run < 0.05 or rise_amount <= 0.05 or stair_count < 1 or stair_width <= 0.0:
|
||||
return _array_mesh(vertices, normals)
|
||||
var forward := horizontal / run
|
||||
var width_axis := Vector3(-forward.z, 0.0, forward.x) * stair_width * 0.5
|
||||
var profile := PackedVector2Array([Vector2(0.0, 0.0), Vector2(run, 0.0), Vector2(run, rise_amount)])
|
||||
for step_index in range(stair_count - 1, -1, -1):
|
||||
var tread_start := run * float(step_index) / stair_count
|
||||
var tread_end := run * float(step_index + 1) / stair_count
|
||||
var tread_height := rise_amount * float(step_index + 1) / stair_count
|
||||
profile.append(Vector2(tread_start, tread_height))
|
||||
if step_index > 0:
|
||||
profile.append(Vector2(tread_start, rise_amount * float(step_index) / stair_count))
|
||||
var triangulation := Geometry2D.triangulate_polygon(profile)
|
||||
for side_sign in [-1.0, 1.0]:
|
||||
for triangle_index in range(0, triangulation.size(), 3):
|
||||
var a := profile[triangulation[triangle_index]]
|
||||
var b := profile[triangulation[triangle_index + 1]]
|
||||
var c := profile[triangulation[triangle_index + 2]]
|
||||
var point_a: Vector3 = start + forward * a.x + Vector3.UP * a.y + width_axis * side_sign
|
||||
var point_b: Vector3 = start + forward * b.x + Vector3.UP * b.y + width_axis * side_sign
|
||||
var point_c: Vector3 = start + forward * c.x + Vector3.UP * c.y + width_axis * side_sign
|
||||
if side_sign > 0.0:
|
||||
_add_tri(vertices, normals, point_a, point_b, point_c)
|
||||
else:
|
||||
_add_tri(vertices, normals, point_c, point_b, point_a)
|
||||
for profile_index in range(profile.size()):
|
||||
var next_index := (profile_index + 1) % profile.size()
|
||||
var a := profile[profile_index]
|
||||
var b := profile[next_index]
|
||||
var point_a_left := start + forward * a.x + Vector3.UP * a.y - width_axis
|
||||
var point_b_left := start + forward * b.x + Vector3.UP * b.y - width_axis
|
||||
var point_b_right := start + forward * b.x + Vector3.UP * b.y + width_axis
|
||||
var point_a_right := start + forward * a.x + Vector3.UP * a.y + width_axis
|
||||
_add_quad(vertices, normals, point_a_left, point_b_left, point_b_right, point_a_right)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_fence_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var start: Vector3 = custom_points[0]
|
||||
var finish: Vector3 = custom_points[1]
|
||||
var path := finish - start
|
||||
var path_length := path.length()
|
||||
if path_length < 0.05 or fence_height <= 0.0:
|
||||
return _array_mesh(vertices, normals)
|
||||
var path_axis := path / path_length
|
||||
var post_count := fence_divisions + 2
|
||||
var post_positions: Array[Vector3] = []
|
||||
for post_index in range(post_count):
|
||||
var base_position := start.lerp(finish, float(post_index) / float(post_count - 1))
|
||||
post_positions.append(base_position)
|
||||
var side_axis := path_axis.cross(Vector3.UP).normalized()
|
||||
if side_axis.length_squared() < 0.001:
|
||||
side_axis = Vector3.FORWARD
|
||||
_append_oriented_box(vertices, normals, base_position + Vector3.UP * fence_height * 0.5, path_axis, Vector3.UP, side_axis, Vector3(thickness, fence_height, thickness))
|
||||
for section_index in range(post_positions.size() - 1):
|
||||
var section_start := post_positions[section_index]
|
||||
var section_end := post_positions[section_index + 1]
|
||||
var section_axis := (section_end - section_start).normalized()
|
||||
var rail_up := Vector3.UP - section_axis * section_axis.dot(Vector3.UP)
|
||||
if rail_up.length_squared() < 0.001:
|
||||
rail_up = Vector3.FORWARD - section_axis * section_axis.dot(Vector3.FORWARD)
|
||||
rail_up = rail_up.normalized()
|
||||
var rail_side := section_axis.cross(rail_up).normalized()
|
||||
var section_length := section_start.distance_to(section_end)
|
||||
for height_ratio in [0.28, 0.78]:
|
||||
var rail_center: Vector3 = (section_start + section_end) * 0.5 + Vector3.UP * fence_height * height_ratio
|
||||
_append_oriented_box(vertices, normals, rail_center, section_axis, rail_up, rail_side, Vector3(section_length, thickness, thickness))
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _append_oriented_box(vertices: PackedVector3Array, normals: PackedVector3Array, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3, dimensions: Vector3) -> void:
|
||||
var x := axis_x.normalized() * dimensions.x * 0.5
|
||||
var y := axis_y.normalized() * dimensions.y * 0.5
|
||||
var z := axis_z.normalized() * dimensions.z * 0.5
|
||||
var nnn := center - x - y - z
|
||||
var pnn := center + x - y - z
|
||||
var ppn := center + x + y - z
|
||||
var npn := center - x + y - z
|
||||
var nnp := center - x - y + z
|
||||
var pnp := center + x - y + z
|
||||
var ppp := center + x + y + z
|
||||
var npp := center - x + y + z
|
||||
_add_quad(vertices, normals, nnp, pnp, ppp, npp)
|
||||
_add_quad(vertices, normals, pnn, nnn, npn, ppn)
|
||||
_add_quad(vertices, normals, npp, ppp, ppn, npn)
|
||||
_add_quad(vertices, normals, nnn, pnn, pnp, nnp)
|
||||
_add_quad(vertices, normals, nnn, nnp, npp, npn)
|
||||
_add_quad(vertices, normals, pnp, pnn, ppn, ppp)
|
||||
|
||||
func _get_centered_gable_base() -> PackedVector3Array:
|
||||
var selected_a := to_global(custom_points[0])
|
||||
var selected_b := to_global(custom_points[1])
|
||||
var search_root: Node = get_parent()
|
||||
while search_root != null and not search_root is ModularBuilderPieceGeometry:
|
||||
search_root = search_root.get_parent()
|
||||
if search_root == null:
|
||||
return PackedVector3Array([custom_points[0], custom_points[1]])
|
||||
var walls: Array[ModularBuilderPieceGeometry] = []
|
||||
_collect_centering_walls(search_root, walls)
|
||||
var centered_a := selected_a
|
||||
var centered_b := selected_b
|
||||
var nearest_a := INF
|
||||
var nearest_b := INF
|
||||
for wall in walls:
|
||||
if wall.custom_points.size() < 2:
|
||||
continue
|
||||
var wall_a := wall.to_global(wall.custom_points[0] + Vector3.UP * wall.unit_size)
|
||||
var wall_b := wall.to_global(wall.custom_points[1] + Vector3.UP * wall.unit_size)
|
||||
var tolerance: float = wall.thickness + 0.04
|
||||
for endpoint in [wall_a, wall_b]:
|
||||
var distance_a := selected_a.distance_to(endpoint)
|
||||
if distance_a < nearest_a and distance_a <= tolerance:
|
||||
nearest_a = distance_a
|
||||
centered_a = endpoint
|
||||
var distance_b := selected_b.distance_to(endpoint)
|
||||
if distance_b < nearest_b and distance_b <= tolerance:
|
||||
nearest_b = distance_b
|
||||
centered_b = endpoint
|
||||
if nearest_a < INF and nearest_b < INF and centered_a.distance_to(centered_b) >= 0.02:
|
||||
return PackedVector3Array([to_local(centered_a), to_local(centered_b)])
|
||||
return PackedVector3Array([custom_points[0], custom_points[1]])
|
||||
|
||||
func _collect_centering_walls(node: Node, walls: Array[ModularBuilderPieceGeometry]) -> void:
|
||||
if node is ModularBuilderPieceGeometry and node.piece_type == PieceType.WALL:
|
||||
walls.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_centering_walls(child, walls)
|
||||
|
||||
func _append_solid_panel(vertices: PackedVector3Array, normals: PackedVector3Array, panel: Array) -> void:
|
||||
if panel.size() < 3:
|
||||
return
|
||||
var p0: Vector3 = panel[0]
|
||||
var p1: Vector3 = panel[1]
|
||||
var p2: Vector3 = panel[2]
|
||||
var normal: Vector3 = (p1 - p0).cross(p2 - p0).normalized()
|
||||
if normal.is_zero_approx():
|
||||
return
|
||||
var half_depth: Vector3 = normal * thickness * 0.5
|
||||
var front: Array[Vector3] = []
|
||||
var back: Array[Vector3] = []
|
||||
for point in panel:
|
||||
front.append(point + half_depth)
|
||||
back.append(point - half_depth)
|
||||
for i in range(1, panel.size() - 1):
|
||||
_add_tri(vertices, normals, front[0], front[i], front[i + 1])
|
||||
_add_tri(vertices, normals, back[i + 1], back[i], back[0])
|
||||
for i in range(panel.size()):
|
||||
var next := (i + 1) % panel.size()
|
||||
_add_quad(vertices, normals, front[i], back[i], back[next], front[next])
|
||||
|
||||
func _make_custom_wall_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var wall_points: PackedVector3Array = _get_centered_wall_points() if center_align else custom_points
|
||||
var a: Vector3 = wall_points[0]
|
||||
var b: Vector3 = wall_points[1]
|
||||
var direction := (b - a).normalized()
|
||||
var length := a.distance_to(b)
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
|
||||
var height := Vector3.UP * unit_size
|
||||
var openings: Array[PackedVector2Array] = []
|
||||
for child in get_children():
|
||||
if _is_detail_node(child) and int(child.get("detail_type")) != DETAIL_TYPE_HATCH and int(child.get("detail_type")) != DETAIL_TYPE_PILLAR:
|
||||
var detail := child as Node3D
|
||||
var center := Vector2((detail.position - a).dot(direction), detail.position.y - a.y)
|
||||
var axis_x := Vector2(detail.transform.basis.x.dot(direction), detail.transform.basis.x.y).normalized()
|
||||
var axis_y := Vector2(detail.transform.basis.y.dot(direction), detail.transform.basis.y.y).normalized()
|
||||
var opening := PackedVector2Array([
|
||||
center - axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
|
||||
center + axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
|
||||
center + axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5,
|
||||
center - axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5
|
||||
])
|
||||
var wall_rect := PackedVector2Array([Vector2(0.0, 0.0), Vector2(length, 0.0), Vector2(length, unit_size), Vector2(0.0, unit_size)])
|
||||
for clipped_opening in Geometry2D.intersect_polygons(opening, wall_rect):
|
||||
if clipped_opening.size() >= 3:
|
||||
openings.append(clipped_opening)
|
||||
var xs: Array[float] = [0.0, length]
|
||||
var ys: Array[float] = [0.0, unit_size]
|
||||
for opening in openings:
|
||||
for point in opening:
|
||||
xs.append(clampf(point.x, 0.0, length))
|
||||
ys.append(clampf(point.y, 0.0, unit_size))
|
||||
xs.sort()
|
||||
ys.sort()
|
||||
for xi in range(xs.size() - 1):
|
||||
for yi in range(ys.size() - 1):
|
||||
var u0: float = xs[xi]
|
||||
var u1: float = xs[xi + 1]
|
||||
var v0: float = ys[yi]
|
||||
var v1: float = ys[yi + 1]
|
||||
var mid := Vector2((u0 + u1) * 0.5, (v0 + v1) * 0.5)
|
||||
var cell := PackedVector2Array([Vector2(u0, v0), Vector2(u1, v0), Vector2(u1, v1), Vector2(u0, v1)])
|
||||
var remaining: Array[PackedVector2Array] = [cell]
|
||||
for opening in openings:
|
||||
var next_remaining: Array[PackedVector2Array] = []
|
||||
for polygon in remaining:
|
||||
for clipped in Geometry2D.clip_polygons(polygon, opening):
|
||||
if clipped.size() >= 3:
|
||||
next_remaining.append(clipped)
|
||||
remaining = next_remaining
|
||||
for polygon in remaining:
|
||||
var indices := Geometry2D.triangulate_polygon(polygon)
|
||||
for ti in range(0, indices.size(), 3):
|
||||
var p0: Vector2 = polygon[indices[ti]]
|
||||
var p1: Vector2 = polygon[indices[ti + 1]]
|
||||
var p2: Vector2 = polygon[indices[ti + 2]]
|
||||
var f0 := a + direction * p0.x + Vector3.UP * p0.y
|
||||
var f1 := a + direction * p1.x + Vector3.UP * p1.y
|
||||
var f2 := a + direction * p2.x + Vector3.UP * p2.y
|
||||
_add_tri(vertices, normals, f0 + side, f1 + side, f2 + side)
|
||||
_add_tri(vertices, normals, f2 - side, f1 - side, f0 - side)
|
||||
# Close the wall body at its ends, top, and base.
|
||||
_add_quad(vertices, normals, a - side, a + side, a + side + height, a - side + height)
|
||||
_add_quad(vertices, normals, b + side, b - side, b - side + height, b + side + height)
|
||||
_add_quad(vertices, normals, a + side + height, b + side + height, b - side + height, a - side + height)
|
||||
_add_quad(vertices, normals, a - side, b - side, b + side, a + side)
|
||||
for opening in openings:
|
||||
for edge_index in range(opening.size()):
|
||||
var p0 := opening[edge_index]
|
||||
var p1 := opening[(edge_index + 1) % opening.size()]
|
||||
var a0 := a + direction * p0.x + Vector3.UP * p0.y
|
||||
var a1 := a + direction * p1.x + Vector3.UP * p1.y
|
||||
_add_quad(vertices, normals, a0 + side, a0 - side, a1 - side, a1 + side)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _get_centered_wall_points() -> PackedVector3Array:
|
||||
if custom_points.size() < 2:
|
||||
return custom_points
|
||||
var selected_a := to_global(custom_points[0])
|
||||
var selected_b := to_global(custom_points[1])
|
||||
var search_root: Node = get_parent()
|
||||
while search_root != null and not search_root is ModularBuilderPieceGeometry:
|
||||
search_root = search_root.get_parent()
|
||||
if search_root == null:
|
||||
return custom_points
|
||||
var walls: Array[ModularBuilderPieceGeometry] = []
|
||||
_collect_centering_walls(search_root, walls)
|
||||
var centered_a := selected_a
|
||||
var centered_b := selected_b
|
||||
var nearest_a := INF
|
||||
var nearest_b := INF
|
||||
for wall in walls:
|
||||
if wall == self or wall.custom_points.size() < 2:
|
||||
continue
|
||||
var direction := (wall.custom_points[1] - wall.custom_points[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5
|
||||
for endpoint in wall.custom_points:
|
||||
var center := wall.to_global(endpoint + Vector3.UP * wall.unit_size)
|
||||
for surface_corner in [center + side, center - side]:
|
||||
var distance_a := selected_a.distance_to(surface_corner)
|
||||
if distance_a < nearest_a and distance_a <= wall.thickness + 0.04:
|
||||
nearest_a = distance_a
|
||||
centered_a = center
|
||||
var distance_b := selected_b.distance_to(surface_corner)
|
||||
if distance_b < nearest_b and distance_b <= wall.thickness + 0.04:
|
||||
nearest_b = distance_b
|
||||
centered_b = center
|
||||
if nearest_a < INF and nearest_b < INF and centered_a.distance_to(centered_b) >= 0.02:
|
||||
return PackedVector3Array([to_local(centered_a), to_local(centered_b)])
|
||||
return custom_points
|
||||
|
||||
func _make_custom_floor_mesh() -> ArrayMesh:
|
||||
var polygon: Array[Vector2] = []
|
||||
for point in custom_points:
|
||||
polygon.append(Vector2(point.x, point.z))
|
||||
var hatch_mesh := _make_hatch_surface_mesh(polygon, custom_points[0].y, custom_points[0].y - thickness)
|
||||
if hatch_mesh != null:
|
||||
return hatch_mesh
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var points := custom_points
|
||||
var bottom: Array[Vector3] = []
|
||||
for point in points:
|
||||
bottom.append(point - Vector3.UP * thickness)
|
||||
for i in range(1, points.size() - 1):
|
||||
_add_tri(vertices, normals, points[0], points[i + 1], points[i])
|
||||
_add_tri(vertices, normals, bottom[0], bottom[i], bottom[i + 1])
|
||||
for i in range(points.size()):
|
||||
var j := (i + 1) % points.size()
|
||||
_add_quad(vertices, normals, bottom[i], bottom[j], points[j], points[i])
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_hatch_surface_mesh(polygon: Array[Vector2], top_y: float, bottom_y: float) -> ArrayMesh:
|
||||
var openings: Array[PackedVector2Array] = []
|
||||
for child in get_children():
|
||||
if _is_detail_node(child) and int(child.get("detail_type")) == DETAIL_TYPE_HATCH:
|
||||
var detail := child as Node3D
|
||||
var center := Vector2(detail.position.x, detail.position.z)
|
||||
var axis_x := Vector2(detail.transform.basis.x.x, detail.transform.basis.x.z).normalized()
|
||||
var axis_y := Vector2(detail.transform.basis.z.x, detail.transform.basis.z.z).normalized()
|
||||
var opening := PackedVector2Array([
|
||||
center - axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
|
||||
center + axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
|
||||
center + axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5,
|
||||
center - axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5
|
||||
])
|
||||
openings.append(opening)
|
||||
if openings.is_empty() or polygon.size() < 3:
|
||||
return null
|
||||
var min_x := polygon[0].x
|
||||
var max_x := polygon[0].x
|
||||
var min_z := polygon[0].y
|
||||
var max_z := polygon[0].y
|
||||
var xs: Array[float] = []
|
||||
var zs: Array[float] = []
|
||||
for point in polygon:
|
||||
min_x = minf(min_x, point.x)
|
||||
max_x = maxf(max_x, point.x)
|
||||
min_z = minf(min_z, point.y)
|
||||
max_z = maxf(max_z, point.y)
|
||||
xs.append(point.x)
|
||||
zs.append(point.y)
|
||||
for opening in openings:
|
||||
for point in opening:
|
||||
xs.append(clampf(point.x, min_x, max_x))
|
||||
zs.append(clampf(point.y, min_z, max_z))
|
||||
xs.sort()
|
||||
zs.sort()
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var source_polygon := PackedVector2Array()
|
||||
for point in polygon:
|
||||
source_polygon.append(point)
|
||||
for xi in range(xs.size() - 1):
|
||||
for zi in range(zs.size() - 1):
|
||||
var x0: float = xs[xi]
|
||||
var x1: float = xs[xi + 1]
|
||||
var z0: float = zs[zi]
|
||||
var z1: float = zs[zi + 1]
|
||||
if x1 - x0 < 0.0001 or z1 - z0 < 0.0001:
|
||||
continue
|
||||
var cell := PackedVector2Array([Vector2(x0, z0), Vector2(x1, z0), Vector2(x1, z1), Vector2(x0, z1)])
|
||||
var remaining: Array[PackedVector2Array] = []
|
||||
for clipped_surface in Geometry2D.intersect_polygons(source_polygon, cell):
|
||||
remaining.append(clipped_surface)
|
||||
for opening in openings:
|
||||
var next_remaining: Array[PackedVector2Array] = []
|
||||
for surface_polygon in remaining:
|
||||
for clipped_surface in Geometry2D.clip_polygons(surface_polygon, opening):
|
||||
if clipped_surface.size() >= 3:
|
||||
next_remaining.append(clipped_surface)
|
||||
remaining = next_remaining
|
||||
for clipped_surface in remaining:
|
||||
var triangulated := Geometry2D.triangulate_polygon(clipped_surface)
|
||||
for ti in range(0, triangulated.size(), 3):
|
||||
var a: Vector2 = clipped_surface[triangulated[ti]]
|
||||
var b: Vector2 = clipped_surface[triangulated[ti + 1]]
|
||||
var c: Vector2 = clipped_surface[triangulated[ti + 2]]
|
||||
_add_tri(vertices, normals, Vector3(a.x, top_y, a.y), Vector3(c.x, top_y, c.y), Vector3(b.x, top_y, b.y))
|
||||
_add_tri(vertices, normals, Vector3(a.x, bottom_y, a.y), Vector3(b.x, bottom_y, b.y), Vector3(c.x, bottom_y, c.y))
|
||||
for i in range(polygon.size()):
|
||||
var j := (i + 1) % polygon.size()
|
||||
var a := polygon[i]
|
||||
var b := polygon[j]
|
||||
_add_quad(vertices, normals, Vector3(a.x, bottom_y, a.y), Vector3(b.x, bottom_y, b.y), Vector3(b.x, top_y, b.y), Vector3(a.x, top_y, a.y))
|
||||
for opening in openings:
|
||||
var visible_edges := Geometry2D.intersect_polygons(opening, source_polygon)
|
||||
for visible_opening in visible_edges:
|
||||
for edge_index in range(visible_opening.size()):
|
||||
var p0 := visible_opening[edge_index]
|
||||
var p1 := visible_opening[(edge_index + 1) % visible_opening.size()]
|
||||
_add_quad(vertices, normals, Vector3(p0.x, bottom_y, p0.y), Vector3(p0.x, top_y, p0.y), Vector3(p1.x, top_y, p1.y), Vector3(p1.x, bottom_y, p1.y))
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _is_detail_node(node: Node) -> bool:
|
||||
if not node is Node3D or node.get_script() == null:
|
||||
return false
|
||||
return node.get_script().resource_path == DETAIL_SCRIPT_PATH
|
||||
|
||||
func _make_custom_foundation_mesh() -> ArrayMesh:
|
||||
var polygon: Array[Vector2] = []
|
||||
for point in custom_points:
|
||||
polygon.append(Vector2(point.x, point.z))
|
||||
var bottom_y := -thickness * 0.5 if centered_origin else 0.0
|
||||
var top_y := thickness * 0.5 if centered_origin else thickness
|
||||
var hatch_mesh := _make_hatch_surface_mesh(polygon, top_y, bottom_y)
|
||||
if hatch_mesh != null:
|
||||
return hatch_mesh
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
for i in range(1, custom_points.size() - 1):
|
||||
var a := custom_points[0]
|
||||
var b := custom_points[i]
|
||||
var c := custom_points[i + 1]
|
||||
_add_tri(vertices, normals, Vector3(a.x, top_y, a.z), Vector3(c.x, top_y, c.z), Vector3(b.x, top_y, b.z))
|
||||
_add_tri(vertices, normals, Vector3(a.x, bottom_y, a.z), Vector3(b.x, bottom_y, b.z), Vector3(c.x, bottom_y, c.z))
|
||||
for i in range(custom_points.size()):
|
||||
var j := (i + 1) % custom_points.size()
|
||||
var a := custom_points[i]
|
||||
var b := custom_points[j]
|
||||
_add_quad(vertices, normals, Vector3(a.x, bottom_y, a.z), Vector3(b.x, bottom_y, b.z), Vector3(b.x, top_y, b.z), Vector3(a.x, top_y, a.z))
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _add_quad(v: PackedVector3Array, n: PackedVector3Array, a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> void:
|
||||
_add_tri(v, n, a, b, c)
|
||||
_add_tri(v, n, a, c, d)
|
||||
|
||||
func _array_mesh(v: PackedVector3Array, n: PackedVector3Array) -> ArrayMesh:
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = v
|
||||
arrays[Mesh.ARRAY_NORMAL] = n
|
||||
var result := ArrayMesh.new()
|
||||
result.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return result
|
||||
|
||||
func _add_tri(v: PackedVector3Array, n: PackedVector3Array, a: Vector3, b: Vector3, c: Vector3) -> void:
|
||||
var normal := (b - a).cross(c - a).normalized()
|
||||
v.append(a)
|
||||
v.append(b)
|
||||
v.append(c)
|
||||
n.append(normal)
|
||||
n.append(normal)
|
||||
n.append(normal)
|
||||
# Also generate reversed faces to show both sides.
|
||||
# This keeps floors and foundations visible from below with any material.
|
||||
v.append(c)
|
||||
v.append(b)
|
||||
v.append(a)
|
||||
n.append(-normal)
|
||||
n.append(-normal)
|
||||
n.append(-normal)
|
||||
1
addons/modular_builder_en/modular_piece_geometry.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dp1333xlylabf
|
||||
|
|
@ -8,7 +8,7 @@ extends RefCounted
|
|||
## Skript braucht die Editor-Umgebung (Dialoge, OS.execute, Rendering).
|
||||
## Deutsch für Sly: alle Status-/Fehlermeldungen auf Deutsch.
|
||||
|
||||
const KERN := preload("res://addons/modular_builder/scopedvr_export.gd")
|
||||
const KERN := preload("res://addons/modular_builder_en/scopedvr_export.gd")
|
||||
const BUDGET := 20000 # Export-Strukturen (match-server BUILDER_BUDGET)
|
||||
const THUMB_SIZE := 128
|
||||
|
||||
|
|
@ -133,8 +133,8 @@ func _export(oid: String, name_roh: String) -> void:
|
|||
if not editor_dir.is_empty():
|
||||
var img := await _thumbnail_bilden(mesh)
|
||||
if img != null:
|
||||
DirAccess.make_dir_recursive_absolute(editor_dir + "/core/thumbs")
|
||||
img.save_png(editor_dir + "/core/thumbs/" + oid + ".png")
|
||||
DirAccess.make_dir_recursive_absolute(editor_dir + "/library/thumbs")
|
||||
img.save_png(editor_dir + "/library/thumbs/" + oid + ".png")
|
||||
|
||||
# 6) Editor-Sync (Registry-Kopie; vorhandene Thumbnails bleiben stehen).
|
||||
var schritt4 := _lauf(objekt_tool, ["editor", oid])
|
||||
1
core
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 5e98535a0afc83af4e4017806686a76af4913afd
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
# Bauen — eigene Häuser aus Modulteilen
|
||||
|
||||
Stand 25.10.2026 (Umbau). Vorlage: **Modular Builder 0.2.0** von Michel Savadim,
|
||||
Godot-Erweiterung, Lizenz **CC0 1.0** (Code, Icons, Doku — siehe
|
||||
`addons/modular_builder/LICENSE`). Die Demo-Szene/Demo-Texturen der Vorlage sind
|
||||
nicht CC0 und liegen bewusst nicht im Repo (`ADDON_LIZENZ.txt`).
|
||||
|
||||
## Prinzip
|
||||
|
||||
Ein Bauteil ist ein **ganz normales Bibliotheks-Objekt** (`bau_*`, Kategorie
|
||||
„bauteile“). Gesetzte Teile stehen in `objects`, ein Gebäude ist eine **Gruppe**
|
||||
(`group` + `group_names`). Dadurch gelten ohne Sonderfälle:
|
||||
|
||||
* Undo/Redo, Auswahl, Verschieben, Drehen, Duplizieren, Löschen, Symmetrie
|
||||
* Speichern in der `.svmap` (kein eigener Block mehr, Server-Validierung V1–V17)
|
||||
* **Kollision serverkonsistent**: jedes Teil hat eine Trimesh-Datei
|
||||
(`library/kollisions/bau_*.json`) — Türen, Fenster, Schießscharten sind echte
|
||||
Löcher, die Treppe ist eine Stufenfläche (Stufen 20 cm). Editor-Playtest, Client
|
||||
und Server lesen dieselbe Datei.
|
||||
* Poly-Budget zählt wie bei jedem Objekt (`poly` = Dreiecke des Teils).
|
||||
|
||||
## Teile (Katalog: `core/editor/bau_katalog.gd`)
|
||||
|
||||
| Gruppe | Teile |
|
||||
| --- | --- |
|
||||
| Fundament & Boden | Fundament, Boden/Decke (2 × 2 m, 20 cm) |
|
||||
| Wände (2 m breit, 3 m hoch) | Wand, Wand mit Tür (1,2 × 2,2 m), Wand mit Fenster, Wand mit Schießscharte, Brüstung (1,1 m), Eckpfeiler |
|
||||
| Dach & Giebel (klein 2 m) | Giebelwand, Satteldach, Pyramidendach |
|
||||
| Dach & Giebel (mittel 4 m) | Giebelwand 4 m, Satteldach 4×4, Pyramidendach 4×4 (Spitze 2 m) |
|
||||
| Dach & Giebel (groß 6 m) | Giebelwand 6 m, Satteldach 6×6, Pyramidendach 6×6 (Spitze 3 m) |
|
||||
| Dach aus Einzelteilen (45°) | Dachschräge 2×2 m (2 m hoch), Dachecke außen (Grat), Dachecke innen (Kehle), Dachplateau, Dachstirn — massiv, rundum geschlossen; tiefe Kante/Ecke zeigt Richtung Vorschau, R dreht |
|
||||
| Treppe | Treppe 4 × 2 m, 3 m hoch (15 Stufen) |
|
||||
|
||||
Fertige Pack-Objekte (Container, Hangar-Tore, Betonsperren) stehen NICHT im Bauen-Tab,
|
||||
sondern wie alle Objekte in der Objekt-Bibliothek unten.
|
||||
|
||||
## Snap (`core/editor/build_snap.gd`)
|
||||
|
||||
* **XZ:** Wände auf die nächste Zellkante — und zwar GANZ in die Zelle, in der die
|
||||
Maus ist (Außenseite bündig mit der Kante, kein Überhang, Ecken schließen ohne
|
||||
Lücke). Klick auf eine vorhandene Wand (Seite oder Oberkante) übernimmt deren Lage
|
||||
exakt (ersetzen bzw. stapeln). Böden/Dächer/Fundamente in die Zellmitte, große
|
||||
Dächer (4 m / 6 m) je nach Zellzahl auf Zellecke bzw. Zellmitte, Pfeiler auf die
|
||||
Zellecke, Treppen auf eine 4 × 2-m-Fläche. `R` dreht die Wand um. Zellen sind
|
||||
2 × 2 m, global ab (0, 0).
|
||||
* **Y:** nie die Klickhöhe, sondern eine **Ebene** — Gelände (höchster Punkt unter
|
||||
der Grundfläche) oder Oberkante eines tragenden Teils darunter. **Wände und
|
||||
Pfeiler zählen von der Unterkante einer Decke/eines Fundaments**, die Decke selbst
|
||||
liegt auf der Wandoberkante: jedes Geschoss liegt auf Vielfachen von 3 m, mit oder
|
||||
ohne Decke darunter stehen die oberen Wände gleich hoch.
|
||||
* **Geschoss fest:** Bild↑/Bild↓ (oder Knöpfe im Tab) wählen ein festes Geschoss
|
||||
(Basis = tiefstes Teil in der Nähe, je 3 m). Der Mausstrahl trifft dann die
|
||||
Geschoss-Ebene statt Wand/Boden darunter — Decken und Böden lassen sich mitten im
|
||||
großen Raum setzen. Ganz unten = Automatik.
|
||||
* **Kein Z-Fighting:** Wand-Außenseite steht 1 cm vor der Plattenkante.
|
||||
* **Ersetzen:** gleiche Art auf demselben Platz wird ersetzt (Tür in vorhandene
|
||||
Wand). Dasselbe Teil nochmal wird mit Erklärung abgelehnt.
|
||||
|
||||
## Dateien erzeugen
|
||||
|
||||
Meshes, GLBs, Kollision und Registry-Fragment kommen aus dem Katalog:
|
||||
|
||||
```bash
|
||||
godot --headless --path . --script res://tools/gen_bauteile.gd -- \
|
||||
--client ../scopedvr-client --server ../scopedvr-match-server
|
||||
(cd ../scopedvr-match-server && python3 tools/merge_object_library.py)
|
||||
```
|
||||
|
||||
Danach `library/objects.json` in Client und Editor angleichen (`tools/sync_objects.sh`).
|
||||
`tests/test_bauen.gd` prüft, dass die Registry zum Katalog passt.
|
||||
|
||||
## Alte Maps
|
||||
|
||||
Die frühere Sektion `gebaeude` (Bauen-Modus vom 20.10.) wird beim Öffnen in
|
||||
`bau_*`-Objekte einer Gruppe übernommen (`BauOps.migriere_gebaeude`). Wandhöhe ist
|
||||
jetzt 3 m statt 2 m — alte Häuser bitte kurz prüfen und neu speichern.
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
# Server-Sektion (Tab „Server“)
|
||||
|
||||
Der Editor ist ein Client der Match-Server-Admin-API (`docs/admin-api.md` im
|
||||
Repo scopedvr-match-server). Damit verwaltet jeder seinen eigenen Server – ohne
|
||||
Konsole, ohne Tailscale, auf jedem Betriebssystem.
|
||||
|
||||
## Einrichten
|
||||
1. Auf dem Server in der `server.cfg` `[admin] enabled=true` setzen und starten.
|
||||
Das Token steht danach in der `state_file` des Servers (Standard `admin.cfg`).
|
||||
2. Editor → Tab **Server** → **➕ Neu**: Name, Adresse, Port (Standard 24580), Token.
|
||||
„🔌 Verbindung testen“ prüft Erreichbarkeit und Token.
|
||||
3. **🔌 Verbinden**.
|
||||
|
||||
**Sicher erreichen:** Die Admin-API spricht Klartext-HTTP und hört standardmäßig nur
|
||||
auf localhost. Entweder SSH-Tunnel (`ssh -L 24580:127.0.0.1:24580 benutzer@server`,
|
||||
im Editor dann Adresse `127.0.0.1`) oder ein Reverse-Proxy mit https (Haken
|
||||
„Sichere Verbindung“).
|
||||
|
||||
## Funktionen
|
||||
- Mehrere Server speichern; **Token verschlüsselt** (Schlüssel aus Rechner-ID +
|
||||
Nutzerordner), nie im Klartext in Datei oder Log. Schutz gegen Zufallsleser, nicht
|
||||
gegen Schadsoftware auf deinem Benutzerkonto.
|
||||
- **Übersicht:** Name, Map, Modus, Phase, Spieler/Bots, Laufzeit, öffentlich/privat.
|
||||
- **⬆ Aktuelle Map hochladen:** prüft und signiert die Map im Editor (wie Speichern),
|
||||
lädt sie hoch, der Server prüft noch einmal. Ungültige Maps werden gar nicht erst
|
||||
gesendet; 422-Fehler erscheinen als verständliche Liste mit Hilfetexten.
|
||||
Existiert der Name schon, fragt der Editor, ob ersetzt werden soll. Optional:
|
||||
direkt in die Rotation aufnehmen.
|
||||
- **Einstellungen:** Formular direkt aus der Server-Spec; gesendet werden nur
|
||||
geänderte Werte. Die Antwort zeigt je Wert: sofort / ab nächster Map / nach
|
||||
Neustart. „↩ Zurücksetzen“ verwirft Änderungen.
|
||||
- **Spieler:** Kick, Sperren (10 min … dauerhaft), Sperrliste mit „Aufheben“.
|
||||
- **Match & Maps:** Match neu starten, Map jetzt laden, Rotation per Häkchen,
|
||||
hochgeladene Maps löschen. Alles Zerstörerische fragt vorher nach.
|
||||
- **⏻ Server neu starten:** fragt nach, trennt alle Spieler, zeigt „Server startet neu…“
|
||||
und verbindet sich wieder, sobald der neue Prozess antwortet (bis 120 s). Der Server
|
||||
beendet sich dafür mit Exit-Code 75; neu starten muss ihn Docker (Restart-Policy),
|
||||
systemd oder `tools/start_server.*` (`docs/admin-api.md` im Server-Repo).
|
||||
- **Bot-Anzahl** geht bis 32, die Grenze kommt vom Server. Reicht die Teamgröße nicht
|
||||
(2 × Teamgröße = Platz für Bots), zeigt das Speichern eine Warnung mit der nötigen Größe.
|
||||
- **Server-Log** mit Filter.
|
||||
|
||||
## Code
|
||||
`core/server/` (AdminClient, ServerStore, AdminTexte, MapPush),
|
||||
`core/ui/panels/server_panel.gd`, `SvmapIO.to_signed_text`.
|
||||
|
||||
## Tests
|
||||
`tests/test_server_sektion.gd` (Unit), `tests/run_server_sektion_e2e.sh`
|
||||
(echter Match-Server + echtes Panel: Verbinden, falsches Token, unerreichbar,
|
||||
Einstellungen, Push gültig/doppelt/ersetzen/lokal ungültig/422, Rotation,
|
||||
Map laden/löschen, Restart, Kick, Logs, 20 Bots, Server-Neustart).
|
||||
211
docs/tasten.md
|
|
@ -1,211 +0,0 @@
|
|||
# Tastatur & Maus — Map-Editor
|
||||
|
||||
Deutsche Tastenbelegung des ScopedVR Map-Editors. **Diese Datei ist die Source
|
||||
of truth** für alle Shortcut-Karten — FINALE Version nach allen drei Karten:
|
||||
Karte 1/3 (t_c46d31c2): Navigation, Auswahl, Löschen · Karte 2/3 (t_55257792):
|
||||
Objektmanipulation + Terrain · Karte 3/3 (t_11b5c25b): Datei & Undo,
|
||||
Werkzeugwechsel, Hilfe, Test. Das Kurzhilfe-Overlay im Editor (Taste **H**)
|
||||
zeigt dieselbe Tabelle (Quelle: `core/ui/help_text.gd`).
|
||||
|
||||
Konventionen (alle Karten):
|
||||
|
||||
- Tastaturcode-basiert: InputMap-Actions mit physischen `KEY_*`-Konstanten
|
||||
(`core/editor/shortcut_keys.gd`). Die physische Taste liegt unabhängig vom
|
||||
Tastaturlayout gleich — deutsches (QWERTZ) und Windows-Layout verhalten sich
|
||||
identisch, keine OS-Hardcodes (PLAN.md Plattform-Regel).
|
||||
- Jede Objekt-Operation = **EIN** ActionHistory-Eintrag (Strg+Z nimmt die
|
||||
ganze Operation zurück). Ausnahme: reine Kamera-Aktionen (Orbit/Flug/Zoom)
|
||||
betreffen die Map nicht und sind nicht in der Historie.
|
||||
- **Strg halten = temporärer Snap-Zwang** (Karte 2/3): egal was der
|
||||
Raster-Snap-Schalter sagt — solange Strg unten ist, rastet Bewegen
|
||||
(Ziehen/Greifen) und Platzieren auf ganze Meter.
|
||||
|
||||
## Navigation (Kamera)
|
||||
|
||||
| Eingabe | Wirkung |
|
||||
| --- | --- |
|
||||
| Mittlere Maustaste ziehen | Orbit: Um die Karte drehen |
|
||||
| Mittlere Maustaste + Umschalt ziehen | Pan: Karte verschieben |
|
||||
| Mausrad | Zoom (aus, wenn Mausrad gerade ein Objekt dreht) |
|
||||
| **Rechte Maustaste GEDACHT halten** | **Freelook-Flug:** Umsehen + W vor / S zurück / A links / D rechts; **Leertaste hoch / Umschalt runter** (Shooter-Konvention wie Half-Life/Counter-Strike — Sly-Wunsch 17.10.). **Funktioniert in JEDEM Werkzeug** (Terrain, Bauen, Spawn-Zonen, Platzieren) — die rechte Maustaste hat ABSOLUTEN Vorrang, kein Werkzeug belegt sie (Karte t_2470de89) |
|
||||
| Strg halten (im Flug) | schneller fliegen (×3) — temporär, Godot-üblich |
|
||||
| Alt halten (im Flug) | langsamer fliegen (×0,3) |
|
||||
| Mausrad (im Flug) | Grundgeschwindigkeit dauerhaft ändern (2–300 m/s) |
|
||||
| **Umschalt+F** | Freelook-Flug dauerhaft an/aus (Flug bleibt ohne gedrückte rechte Maustaste aktiv) |
|
||||
| **F** | Auswahl ins Bild zentrieren (Focus Selection) |
|
||||
|
||||
Der Orbit-Modus ist der Default: Beim Verlassen des Flugs springt die Kamera
|
||||
nicht — Ziel/Winkel/Abstand werden aus der Flugposition zurückgerechnet.
|
||||
|
||||
## Auswahl
|
||||
|
||||
| Eingabe | Wirkung |
|
||||
| --- | --- |
|
||||
| Linksklick | Objekt auswählen (ersetzt die Auswahl) |
|
||||
| **Strg+Klick** (Viewport **und** Objektbaum) | Objekt zur Auswahl hinzufügen/entfernen (Mehrfachauswahl) |
|
||||
| Umschalt+Klick | wie Strg+Klick (Mehrfachauswahl umschalten) |
|
||||
| Ziehen über leeren Boden | Rahmen-Aufziehen: alle Objekte im Rahmen wählen |
|
||||
| Umschalt + Rahmen | Rahmen-Auswahl zur bestehenden Auswahl hinzufügen |
|
||||
| Esc | Werkzeug abbrechen → **Greifen zurückrollen** → Ghost-Preview abbrechen → Auswahl aufheben (Prioritätskette) |
|
||||
|
||||
Alle Bestandsaktionen wirken auf die GANZE Mehrfachauswahl: Entf löscht alle,
|
||||
Strg+D dupliziert alle, Ziehen verschiebt gemeinsam, Anheben/Absenken hebt
|
||||
alle, Strg+G gruppieren / Strg+Umschalt+G lösen.
|
||||
|
||||
## Objekt-Manipulation (Karte 2/3)
|
||||
|
||||
| Eingabe | Wirkung |
|
||||
| --- | --- |
|
||||
| **R** | Auswahl 90° drehen (Vierteldrehung; Umschalt+R = −90°). Objekte (auch Bauteile) drehen um den Auswahl-Mittelpunkt. Ein Objekt aus einer Gruppe dreht die GANZE Gruppe. Dauerhaft (kein Vorschau-Zustand) |
|
||||
| **Strg+R / Strg+Umschalt+R** | Auswahl um X **kippen** (+15° / −15°) — auch im Platzier-Modus (Ghost kippt mit). Karte t_e166e656 |
|
||||
| **Alt+R / Alt+Umschalt+R** | Auswahl um Z **kippen** (+15° / −15°) — auch im Platzier-Modus. Karte t_e166e656 |
|
||||
| Mausrad (mit Auswahl) | Feindrehen im 15°-Raster (bleibt wie bisher) |
|
||||
| **Pfeiltasten** | Auswahl im **2-m-Raster** verschieben — EINE Zelle pro Tastendruck, Richtung = Kamera-Ausrichtung nächste Weltachse (Godot-Editor-Konvention: ↑ = von der Kamera weg). Fußpunkt bleibt am Raster, Höhe wandert mit |
|
||||
| **Bild↑ / Bild↓** | Auswahl +0,25 m / −0,25 m anheben/absenken (Mehrfachdruck = EIN Undo-Schritt). Absenken UNTER die Terrain-Höhe ist erlaubt (Versenken — z. B. Steine im Boden); Untergrenze ist die Match-Grenze |
|
||||
| **Strg+Bild↑** | Auswahl direkt auf das Terrain/Boden absetzen (Godot „Snap Object to Floor“-Äquivalent fürs Fußpunkt-Modell) |
|
||||
| **G** | Auswahl **greifen** (Blender-Semantik): sie hängt an der Maus, Linksklick setzt die neue Position (mit Raster-Snap bzw. Strg-Zwang), **Esc** bricht ab und setzt die exakten alten Positionen zurück. Greifen der ganzen Gruppe/mehreren Objekte wandert gemeinsam |
|
||||
| **Entf (DELETE)** | Auswahl SOFORT löschen, ohne Rückfrage — Strg+Z holt alles zurück. Wirkt auf Szene-Auswahl UND Objektbaum-Markierung |
|
||||
| **Strg+D** | Auswahl duplizieren: Kopie +2 m in X versetzt (Rasterversatz), Füßpunkt unverändert; bei Mehrfachauswahl ALLE Objekte mit EINEM Undo-Schritt |
|
||||
| Strg+G / Strg+Umschalt+G | Auswahl gruppieren / Gruppe lösen |
|
||||
|
||||
## Datei & Undo (Karte 3/3)
|
||||
|
||||
Vor „Neue Map“ und „Öffnen“ fragt der Editor NACH, wenn die Map noch
|
||||
ungespeicherte Änderungen hat (deutscher Dialog „Verwerfen / Abbrechen“) —
|
||||
nichts geht versehentlich verloren.
|
||||
|
||||
| Eingabe | Wirkung |
|
||||
| --- | --- |
|
||||
| **Strg+Z** | Rückgängig (EIN Eintrag pro Operation) |
|
||||
| **Strg+Y** UND **Strg+Umschalt+Z** | Wiederherstellen (Redo) — beide Wege sind belegt |
|
||||
| **Strg+S** | Map speichern (.svmap, bekannter Pfad; noch nie gespeichert → „Speichern unter“) |
|
||||
| **Strg+Umschalt+S** | Map speichern unter … |
|
||||
| **Strg+O** | Map öffnen |
|
||||
| **Strg+N** | Neue Map |
|
||||
|
||||
## Updates (Datei-Menü)
|
||||
|
||||
Der Editor aktualisiert sich selbst aus seinem Git-Klon (Karte t_d95f087a) —
|
||||
kein Programmdownload, nur „Versionsstände holen“:
|
||||
|
||||
| Weg | Wirkung |
|
||||
| --- | --- |
|
||||
| **Beim Start** | Still prüfen. Gibt es eine neuere Version, erscheint eine Bernstei-Leiste unter der Menüleiste: **„🔄 Update verfügbar: `<alt>` → `<neu>`“** |
|
||||
| **Banner → „🔄 Jetzt updaten“** | Installiert das Update: neue Version laden (git pull), Import-Lauf, dann startet der Editor von selbst neu. Ungespeicherte Änderungen werden vorher abgefragt (derselbe Dialog wie bei „Neue Map“/„Öffnen“ — **Abbrechen** bricht das Update ab) |
|
||||
| **Datei → „🔄 Updates prüfen“** | Gleicher Check auf Wunsch, mit Ergebnis-Meldung in der Statuszeile: „Update verfügbar“ / „Du bist auf dem neusten Stand (`<sha>`)“ / „Nicht erreichbar“ |
|
||||
|
||||
- **Offline / Tunnel weg:** kein Banner, keine Meldung — der Editor bleibt
|
||||
einfach so, wie er ist. Prüfen kann man trotzdem: die Meldung sagt es dann
|
||||
in der Statuszeile.
|
||||
- **Zugangsdaten fehlen** (Karte t_1457e935): die Update-Prüfung läuft
|
||||
bewusst NIE interaktiv — es öffnet sich **niemals** ein Login-Fenster im
|
||||
Editor. Stattdessen kommt in der Statuszeile „Updates nicht prüfbar —
|
||||
Zugangsdaten fehlen“. Die Zugangsdaten liegen im Git-Credential-Store
|
||||
(`~/.git-credentials`, Host `127.0.0.1:13020`); wenn sie fehlen, füllt sie
|
||||
ein Eintrag der Form `http://<user>:<token>@127.0.0.1:13020` nach.
|
||||
- **Fehlgeschlagener Import:** bricht das Update mit Klartext-Meldung ab —
|
||||
der Editor läuft UNVERÄNDERT weiter (kein Neustart in einen kaputten Stand,
|
||||
Lektion 17.10. class_name-Cache).
|
||||
- **Neustart:** Der Editor startet sich selbst neu (dasselbe Godot-Programm,
|
||||
derselbe Projektordner, wieder im Editor-Modus). Wer den Editor als
|
||||
systemd-User-Unit betreibt, setzt in `scopedvr_editor.cfg` (Godot-App-Daten)
|
||||
Abschnitt `[update]` einen `restart_command` — z. B.
|
||||
`restart_command="systemd-run --user --on-active=3s systemctl --user restart map-editor"`.
|
||||
Ohne Eintrag wird unter Linux automatisch erkannt, ob die Unit
|
||||
`map-editor.service` existiert; sonst Selbst-Restart.
|
||||
- **Konfiguration** (alle optional, `user://scopedvr_editor.cfg`, Abschnitt
|
||||
`[update]`): `restart_command` (eigener Neustart-Befehl), `git_command`
|
||||
(eigener git-Pfad). Der Import-Lauf nutzt dieselbe Godot-Binary wie der
|
||||
Editor selbst (bzw. `[test] godot_path`).
|
||||
|
||||
## Kartengröße ändern (Datei-Menü)
|
||||
|
||||
„Kartengröße ändern…“ (Datei-Menü) öffnet einen Dialog mit Live-Vorschau:
|
||||
|
||||
- **Aktuelle Größe** steht oben, darunter die **neue Breite (X) und Tiefe (Z)
|
||||
in Metern** — Freifelder plus Preset-Buttons 256 / 512 / 1024 / 2048 m (beide
|
||||
Achsen gleichzeitig). Erlaubt sind **256–4096 m** (Werte außerhalb werden
|
||||
beim Anwenden an die Grenzen geklemmt).
|
||||
- **Anker Mitte:** Die Karte bleibt am Ursprung zentriert. Das bestehende
|
||||
Terrain behält seine WELTPOSITION (bilinear neu abgetastet — kein Stretch),
|
||||
die neue Randzone wird flach auf Grundhöhe + Gras. Beim Vergrößern steigt
|
||||
die Terrain-Auflösung automatisch mit (bis zum Format-Deckel 513 Punkte).
|
||||
- **Verkleinern — Vorschau:** Der Dialog listet VORHER, was draußen liegen
|
||||
würde („Beim Verkleinern liegen draußen: 5 Objekte, 2 Bauteile, 1
|
||||
Spawn-Punkt“). Per Schalter entscheidest du, ob diese beim Anwenden
|
||||
**entfernt** werden (Standard: ja — Strg+Z holt sie zurück) oder über den
|
||||
Rand hinaus in der Map bleiben — dann ist die Map **nicht speicherbar**
|
||||
(Regel: nichts außerhalb der Match-Grenzen), bis du die Einträge verschiebst
|
||||
oder löschst. Warnung, falls ein Team dadurch unter
|
||||
**4 Spawn-Punkte** fällt (Maps brauchen mindestens 4 pro Team zum Speichern).
|
||||
- **Vergrößern über ~1024 m — Warnband:** Ab dieser Größe wird das Terrain
|
||||
grober aufgelöst (mehr Meter pro Höhenpunkt) — der Dialog weist auf die
|
||||
geringere Detail-Dichte und das Quest-Perf-Budget (Objektzahl/Draw-Calls) hin.
|
||||
- **Wann erlaubt?** Jederzeit — es muss nur eine Map existieren.
|
||||
- **Undo:** Der Resize ist **EINE** Undo-Einheit: Terrain, Objekte, Bauteile
|
||||
und Spawn-Zonen zusammen. **Strg+Z stellt den kompletten Vorher-Zustand
|
||||
her** (inkl. gelöschter Objekte), Strg+Y stellt den Resize wieder her.
|
||||
Nach dem Resize sind ungespeicherte Änderungen vorhanden (Speichern mit
|
||||
Strg+S signiert die Prüfsumme neu).
|
||||
|
||||
## Terrain-Werkzeug (Karte 2/3) — nur aktiv, wenn das Terrain-Werkzeug an ist
|
||||
|
||||
| Eingabe | Wirkung |
|
||||
| --- | --- |
|
||||
| **[** | Pinselgröße verkleinern — durch die 3 Presets Klein (4 m) / Mittel (12 m) / Groß (32 m) zyklisch; Preset-Buttons, Slider und Label wandern mit |
|
||||
| **]** | Pinselgröße vergrößern (gleicher Zyklus, andere Richtung) |
|
||||
| **1** | Pinselmodus Heben |
|
||||
| **2** | Pinselmodus Senken |
|
||||
| **3** | Pinselmodus Glätten |
|
||||
| **4** | Pinselmodus Malen (mit der zuletzt gewählten Materialfarbe) |
|
||||
| **Linksklick-Ziehen** | Boden formen bzw. malen (je nach Pinselmodus) |
|
||||
| **Strg + Linksklick-Ziehen** | **Gegenteil:** bei Heben senkt er, bei Senken hebt er (Invert-Konvention wie Blender/Photoshop); im Malen-Modus malt Strg+Klick Gras zurück |
|
||||
| Rechte Maustaste | **Kein Terrain-Strich mehr** — gehört dem Freelook-Flug (absoluter Vorrang, siehe Navigation) |
|
||||
| Strg halten | temporärer Snap-Zwang beim Objekt-Bewegen/Platzieren (gilt editorweit, nicht nur hier) |
|
||||
|
||||
Konfliktfreiheit: Die Ziffern 1–4 sind NUR im Terrain-Werkzeug belegt — im
|
||||
Objekt-/Bauen-Modus sind sie unbenutzt, im Flugmodus (rechte Maustaste
|
||||
gehalten) bleiben Leertaste/Shift/Strg die Kamera. **Leertaste, Umschalt und
|
||||
Strg triggern im Flug KEINE Werkzeug-Aktion:** Terrain senkt nur bei
|
||||
Strg+gedrücktem Linksklick (nicht bei Strg allein), Entf/Strg+Z & Co. bleiben
|
||||
unangetastet, und der Flug-Einstieg gibt den GUI-Fokus frei, damit Leertaste
|
||||
keinen fokussierten Button auslöst.
|
||||
|
||||
## Werkzeuge (Karte 3/3: F-Tasten wechseln das Werkzeug)
|
||||
|
||||
| Eingabe | Wirkung |
|
||||
| --- | --- |
|
||||
| **F1** | Werkzeug: Terrain formen |
|
||||
| **F2** | Werkzeug: Objekte platzieren/auswählen (Standard) |
|
||||
| **F3** | Tab „Bauen“ öffnen (Bauteile wählen, dann in die Karte klicken) |
|
||||
| **F4** | Werkzeug: Spawn-Zonen malen |
|
||||
| Esc (im Terrain-/Spawn-Werkzeug) | Werkzeug beenden — zurück zum Objekt-Werkzeug |
|
||||
| Esc (beim Greifen) | Greifen abbrechen, alte Positionen zurück |
|
||||
| Esc (beim Platzieren) | Ghost-Preview abbrechen |
|
||||
| Esc (Kurzhilfe offen) | Kurzhilfe schließen |
|
||||
| **L** | **Höhenlinien fixieren** (Karte t_89c0ec95): Linien immer sichtbar/unsichtbar — unabhängig vom Werkzeug. Ohne Fix erscheinen sie NUR im Terrain-Werkzeug (F1) |
|
||||
|
||||
F1–F4 und F5/H/L wirken NUR, wenn kein Textfeld/SpinBox fokussiert ist und kein
|
||||
Dialog offen ist (ShortcutPolicy) — Tippen geht immer vor.
|
||||
|
||||
Die Terrain-Ziffern 1–4 und [ / ] bleiben (Karte 2/3) NUR im Terrain-Werkzeug
|
||||
belegt — der Werkzeugwechsel läuft jetzt über die F-Tasten, nicht über Ziffern.
|
||||
|
||||
Höhenlinien-Kopplung (Karte t_89c0ec95): Die Konturen (1 m, alle 5 m kräftiger)
|
||||
sind Werkzeug-gekoppelt — **F1 (Terrain) blendet sie ein, jeder andere
|
||||
Werkzeugwechsel blendet sie aus** (hart, ohne Fade). Wer sie dauerhaft will,
|
||||
fixiert sie mit **L** oder dem Button „≈ Höhenlinien“ oben. Das Relief-Shading
|
||||
(Hillshade + Höhen-Kurve) bleibt davon unberührt und immer an. Auf flachen Maps
|
||||
(< 2 m Gesamtrelief) sind die Linien automatisch aus. L ist sonst unbesetzt
|
||||
(Kollisions-Prüfung t_89c0ec95).
|
||||
|
||||
## Hilfe & Test (Karte 3/3)
|
||||
|
||||
| Eingabe | Wirkung |
|
||||
| --- | --- |
|
||||
| **H** | Kurzhilfe-Overlay an/aus — alle Tasten dieser Datei als Panel über der Karte; Esc schließt ebenfalls. Nicht-blockierend: der Editor bleibt bedienbar |
|
||||
| **T** | Werkzeug: Test-Startpunkt setzen (Karte t_96e3c4ee): Klick in die Karte = dort startet F5. Höhe kommt automatisch vom Terrain; Klick auf Objekt/Bauteil = Boden darunter; Entf im Werkzeug löscht den Punkt. **KEIN Spiel-Spawnpunkt** — er landet in einer Editor-Sidecar-Datei, nicht in der `.svmap` (Spawn-Checks/Analysen unberührt). Im Testlauf versetzt **T** dich sofort zum Blickpunkt und setzt dort den Test-Start |
|
||||
| **F5** | ▶ Karte testen (aktuelle Map sofort als Spiel durchlaufen; der Editor bleibt offen). Start-Reihenfolge: gesetzter Test-Start → erster Team-Spawnpunkt → Kartenmitte (je mit Klartext-Hinweis) |
|
||||
|
||||
Die Statuszeile unten zeigt PERMANENT das aktive Werkzeug („Werkzeug: …“) plus
|
||||
den Hinweis „Umschalt+F = Flugmodus“ — man sieht immer, wo man ist.
|
||||