Compare commits
176 commits
m2-ux-fund
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
313500b988 | ||
|
|
8f01775a92 | ||
|
|
295f79011f | ||
|
|
949d73dfdd | ||
|
|
e8ab954876 | ||
|
|
ff1c0e345e | ||
|
|
91c66a0b98 | ||
|
|
330365e59b | ||
|
|
9589703f7c | ||
|
|
b04a953190 | ||
|
|
48db200d3e | ||
|
|
92e2c688c1 | ||
|
|
f42e133bae | ||
|
|
bb9eb71770 | ||
|
|
8345903ce7 | ||
|
|
902181500a | ||
|
|
bcbd416960 | ||
|
|
5e32c771fc | ||
|
|
961e8c6ff6 | ||
|
|
075735f426 | ||
|
|
fa2feee780 | ||
|
|
3f0d0f3f33 | ||
|
|
ab9a798343 | ||
|
|
491a47d3f1 | ||
|
|
a76c3ecae7 | ||
|
|
fcc7f16f1c | ||
|
|
e3a6b2ad21 | ||
|
|
35a047cc2a | ||
|
|
93cdcb0cd4 | ||
|
|
59c932dc35 | ||
|
|
3094395444 | ||
|
|
3d083648d8 | ||
|
|
8cde37b85c | ||
|
|
26bcbe0cdd | ||
|
|
b1d8f7626d | ||
|
|
a1f386f5e6 | ||
|
|
99a06c6cb8 | ||
|
|
9f2a32df70 | ||
|
|
5a70824dd5 | ||
|
|
7f06e28472 | ||
|
|
4e973795f6 | ||
|
|
4a7b99ee3e | ||
|
|
40ca5ab6ba | ||
|
|
69668eb250 | ||
|
|
66df3bdc1e | ||
|
|
1d8763b881 | ||
|
|
04c6c67ba8 | ||
|
|
f5a4ab54d0 | ||
|
|
8468b89456 | ||
|
|
338e7152a0 | ||
|
|
42a205b6ff | ||
|
|
78d311c75f | ||
|
|
0505463ace | ||
|
|
35a1d80d3e | ||
|
|
65b33a207a | ||
|
|
7a1b5037e7 | ||
|
|
812da1a261 | ||
|
|
6f00737dd3 | ||
|
|
52919b36b4 | ||
|
|
0cdf68622c | ||
|
|
fabbbcce20 | ||
|
|
1f1a413065 | ||
|
|
d637314b82 | ||
|
|
ae551f387a | ||
|
|
f4cfe17594 | ||
|
|
f6c754115a | ||
|
|
0e1406a63e | ||
|
|
835a7f7363 | ||
|
|
5dca5513da | ||
|
|
f16b9c8fb6 | ||
|
|
403895b743 | ||
|
|
60b09bfb74 | ||
|
|
3d7ba993d5 | ||
|
|
a62357dad8 | ||
|
|
e317706df0 | ||
|
|
8c97d00871 | ||
|
|
7c7dc38566 | ||
|
|
4fb883a44c | ||
|
|
ab173ac8aa | ||
|
|
0f86472cfa | ||
|
|
40449932b9 | ||
|
|
85bee00f3d | ||
|
|
8f0e011a07 | ||
|
|
69ffe23a71 | ||
|
|
a5d8f7f79d | ||
|
|
b53e05a145 | ||
|
|
1b943ceb35 | ||
|
|
969e928d74 | ||
|
|
2c5b58f04c | ||
|
|
dca0947a71 | ||
|
|
2c10863759 | ||
|
|
238ca7bb2f | ||
|
|
c15c606311 | ||
|
|
0adb161386 | ||
|
|
437f6bf368 | ||
|
|
d0d4fbd221 | ||
|
|
aa94bde708 | ||
|
|
c5bdb0518e | ||
|
|
967e4dc54a | ||
|
|
370917f96e | ||
|
|
e664d09666 | ||
|
|
9fa8029910 | ||
|
|
c8b9c59fda | ||
|
|
366d482a1a | ||
|
|
c8a41c0794 | ||
|
|
f4748af371 | ||
|
|
ccf1c43f2e | ||
|
|
43636c840f | ||
|
|
91e2b03843 | ||
|
|
2bd1e671e4 | ||
|
|
90c3c7fdc7 | ||
|
|
3e07e91f5a | ||
|
|
785a858ae0 | ||
|
|
c0cbe4bb0c | ||
|
|
e1e07e57b0 | ||
|
|
8f598dc67d | ||
|
|
6572f1e10c | ||
|
|
6f81add8f4 | ||
|
|
6c65b13a8f | ||
|
|
b288e8a1e8 | ||
|
|
c92d35dca2 | ||
|
|
29b1668f57 | ||
|
|
56a808f052 | ||
|
|
c0d426152e | ||
|
|
ebe3dd276b | ||
|
|
0f86182a2c | ||
|
|
310aa695e6 | ||
|
|
76f8143540 | ||
|
|
9dca88e10e | ||
|
|
1cfc4e8c3a | ||
|
|
f6f5ef6c90 | ||
|
|
f99d1e2a28 | ||
|
|
23f7e1d39c | ||
|
|
4c960e2587 | ||
|
|
73847ed1ce | ||
|
|
f2b4f6a824 | ||
|
|
2065ef2252 | ||
|
|
116acd63ed | ||
|
|
2d8b9822f0 | ||
|
|
b85e3bed52 | ||
|
|
d6a0fe2196 | ||
|
|
ffb27645d4 | ||
|
|
e1c737df61 | ||
|
|
db6bcec971 | ||
|
|
d6c472603f | ||
|
|
7521db7f8d | ||
|
|
cda6f48b01 | ||
|
|
afcbda8dd0 | ||
|
|
08a75ac430 | ||
|
|
095af5ff3c | ||
|
|
b20dff2b5d | ||
|
|
e505329a42 | ||
|
|
b7b160d3bc | ||
|
|
adb585d30e | ||
|
|
b3fcb355d6 | ||
|
|
e2954a38ed | ||
|
|
905edc021a | ||
|
|
87e7a96fd5 | ||
|
|
5dd37e32d7 | ||
|
|
cddebb8c26 | ||
|
|
23ede45efd | ||
|
|
423b223454 | ||
|
|
0eae5f56ad | ||
|
|
595d1217d9 | ||
|
|
849129d67f | ||
|
|
eca11bbf83 | ||
|
|
ce1dc4130d | ||
|
|
684b4cfd08 | ||
|
|
3a5c9d44bb | ||
|
|
82269acfbb | ||
|
|
12022d1f05 | ||
|
|
ada7e449c2 | ||
|
|
0a0ca65846 | ||
|
|
10fdb4914c | ||
|
|
30379bcc7b | ||
|
|
9b377a7720 |
8
.gitignore
vendored
|
|
@ -2,5 +2,13 @@
|
|||
.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
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[submodule "core"]
|
||||
path = core
|
||||
url = ../scopedvr-map-core.git
|
||||
branch = main
|
||||
ignore = untracked
|
||||
16
ADDON_LIZENZ.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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.
|
||||
284
README.md
|
|
@ -48,17 +48,18 @@ 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 (`src/editor/action_history.gd`):** zentrale ActionHistory über
|
||||
- **Undo/Redo (`core/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 (`src/editor/interaction.gd`):**
|
||||
- **Objekt-Platzierung mit Ghost-Preview (`core/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, Rechtsklick/ESC bricht ab,
|
||||
(Höhe kommt aus dem Terrain), Klick setzt, ESC bricht ab (Rechte
|
||||
Maustaste = Flug, das Objekt bleibt in der Hand — Karte t_2470de89),
|
||||
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
|
||||
|
|
@ -78,10 +79,281 @@ Boden/Wetter-Stimmung — siehe Commit 84bc081 und `svmap_io.gd`.)*
|
|||
Terrain-Höhen-Abfrage, Platzhalter-Meshes).
|
||||
Lauf: `godot --headless --path . --script res://tests/test_ux.gd`
|
||||
|
||||
**Bekannte Falle (15.10. live passiert):** Nach `git pull` mit NEUEN
|
||||
`class_name`-Skripten (z. B. `action_history.gd`) muss einmal
|
||||
`godot --headless --import` laufen, bevor die GUI gestartet wird — sonst ist
|
||||
der globale Klassen-Cache (`.godot/`) alt, `main.gd` wirft „Identifier not
|
||||
declared" und das Fenster startet ohne UI. Symptom: Parse-Errors im App-Log
|
||||
trotz grünem Test-Lauf (der ja selbst mit importiertem Cache lief).
|
||||
|
||||
**Bibliothek:** `library/objects.json` ist die Editor-Kopie der offiziellen
|
||||
Registry aus dem Match-Server-Repo — neue Objekte DORT registrieren und hier
|
||||
synchron ziehen (IDs sind der Vertrag, Entscheidung 20).
|
||||
|
||||
**Noch offen (folgende Karten der MVP-Kette):** Terrain-Werkzeuge
|
||||
(heben/senken/glätten — nutzen ActionHistory mit `["terrain"]`),
|
||||
Spawn-Zonen-Editor, Performance-Check-UI, Export-Workflow.
|
||||
## Status (M2, Karte t_3a998ae4 — Objekt-Bibliothek FERTIG)
|
||||
|
||||
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
|
||||
„⚡ 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.
|
||||
- **Raster-Snap (1 m):** CheckButton in den Objekt-Werkzeugen — beim
|
||||
Platzieren UND Ziehen rasten X/Z auf ganze Meter (Höhe folgt weiter dem
|
||||
Terrain). AUS = freies Platzieren wie bisher.
|
||||
- **svmap-Roundtrip:** Objekte (Bibliotheks-ID + Pos/Rot/Scale) überleben
|
||||
Speichern/Laden identisch, save→load→save bleibt byte-identisch
|
||||
(`tests/test_library.gd`).
|
||||
- **Tests:** `tests/test_library.gd` (Bibliothek laden, Gruppen komplett/
|
||||
überschneidungsfrei, Thumbnails 128×128, Raster-Snap, Snap-to-Ground + je
|
||||
Undo für setzen/drehen/verschieben/löschen über ALLE 11 Objekte,
|
||||
svmap-Roundtrip mit Objekten + Poly-Budget aus dem Katalog).
|
||||
- **Desktop-Smoke:** `godot … -- --library-demo [ordner] [--library-demo-quit]`
|
||||
fährt Bibliothek/Platzierung/Schweben/Snap programmatisch durch und
|
||||
speichert Screenshots.
|
||||
|
||||
**Noch offen (folgende Karten der MVP-Kette):** Performance-Check-UI,
|
||||
Export-Workflow.
|
||||
|
||||
## Status (M2, Karte t_c4162c5d — Spawn-Zonen FERTIG)
|
||||
|
||||
Auf der Objekt-Bibliothek (t_3a998ae4) aufgebaut, nach dem Chef-Grundsatz:
|
||||
|
||||
- **Spawn-Zonen malen (`core/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“
|
||||
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
|
||||
nötig)“ + Klartext-Hinweis, WIE die Zone ergänzt wird. Fehler zeigt der
|
||||
Editor VOR dem Export, nicht erst beim Speichern.
|
||||
- **Map-Fertig-Check:** Button „✅ Karte prüfen“ oben — Klartext-Liste
|
||||
✅/❌ (Name, Autor, beide Teams, Objekt-IDs, Poly-Budget) in einem
|
||||
Fenster; fehlt etwas, wackelt (Shake-Tween) der Button und die
|
||||
Statusleiste sagt „Map ist noch nicht bereit — siehe Warnungen“.
|
||||
Der harte Gate bleibt das Speichern (`SvmapIO.save_to_file` —
|
||||
dieselbe `_validate_core`-Funktion, EINE Implementierung, kein Drift).
|
||||
- **Export-Signierung:** unverändert der eine Code-Pfad — Editor-Speichern
|
||||
ruft `SvmapIO.sign_map` (SHA-256 über kanonisierte Payload). Abnahme mit
|
||||
ECHTEM Server-Loader: Editor-Export durch `SvmapLoader.parse_and_validate`
|
||||
aus scopedvr-match-server gejagt → ok=true inkl. V13-Prüfsummen-Check
|
||||
(kein Copy-Paste-Drift, Interop bewiesen).
|
||||
- **Radius-Persistenz:** Der Zonen-Radius ist KEIN Formatfeld (spawn_zones
|
||||
speichert Punkte) — er wohnt als `design_notes.spawn_radius_m` in der
|
||||
Map (Loader prüft design_notes nicht → rundtrip-sicher). Zonen sitzen
|
||||
AUF dem Terrain (`terrain_height_at`), egal wie hügelig die Map ist.
|
||||
- **Tests:** `tests/test_spawns.gd` (52 Checks: setzen/ändern/löschen +
|
||||
je Undo/Redo mit Radius, Merge-Tags, Live-Status ✅/❌, ready_report
|
||||
grün/rot, Export→Re-Import identisch + byte-identischer Roundtrip,
|
||||
Zone auf Terrain-Höhe). Parse-Check: 23 Scripts, 0 Fehler.
|
||||
- **Desktop-Smoke:** `godot … -- --spawns-demo [ordner] [--spawns-demo-quit]`
|
||||
fährt Zonen-Übersicht → Live-Check-Alarm → Undo → „Karte prüfen“
|
||||
programmatisch durch und speichert Screenshots.
|
||||
|
||||
## Status (M2, Karte t_7567a06f — Objekt-Ops: Duplizieren + Hoch/Runter + Spiegeln)
|
||||
|
||||
Auf dem UX-Fundament (t_40d855d0) und der Objekt-Bibliothek (t_3a998ae4)
|
||||
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
|
||||
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
|
||||
ändern die Höhe in 0,25-m-Schritten. MEHRERE Klicks in Serie = EIN
|
||||
Undo-Schritt. Design-Entscheidungen, dokumentiert:
|
||||
- Die Höhe wohnt direkt in `object.pos.y` — das svmap-Format erlaubt
|
||||
freie y-Werte innerhalb der Match-Grenzen (Loader-Regel V9), deshalb
|
||||
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.
|
||||
- **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
|
||||
Verschieben in derselben Höhe über dem (neuen) Boden weiter; bodennah
|
||||
bleibt bodennah.
|
||||
- Die Auswahl-Anzeige zeigt die Höhe mit („+0,25 m über dem Boden“).
|
||||
- **Spiegeln (`⇄ Links↔Rechts` / `⇅ Vor↔Zurück`):** Das Objekt wird AN
|
||||
SEINER STelle gespiegelt — Position bleibt, die Blickrichtung dreht:
|
||||
- links↔rechts (Welt-X-Spiegelung): `rot_y_deg → −rot`
|
||||
- vor↔zurück (Welt-Z-Spiegelung): `rot_y_deg → 180° − rot`
|
||||
(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.
|
||||
- **Alles über ActionHistory:** Snapshot vor/nach JEDE Aktion
|
||||
(Duplizieren = EIN Schritt, Anheben-Serie = EIN Schritt via Merge-Tag,
|
||||
Spiegeln = EIN Schritt). Statusleiste erklärt nach jeder Aktion den
|
||||
nächsten Schritt und die Grenzen auf Deutsch.
|
||||
- **Tests:** `tests/test_ux.gd` auf 90 Checks erweitert (42 neu):
|
||||
Duplizieren (Raster/ohne Raster/Undo-Roundtrip/Index-Schutz), Anheben/
|
||||
Absenken (Schrittweite, Boden-Stopp, Deckel bounds.max, versteckt-Regel,
|
||||
Merge-Tags), Spiegeln (X/Z-Mapping, Selbstinvers, Position unverändert),
|
||||
svmap-Roundtrip mit angehobener Höhe. `godot … --quit-after` smoke:
|
||||
UI baut alle neuen Buttons fehlerfrei.
|
||||
## Status (M2, Karte t_f6f29d86 — Auswahl-System FERTIG)
|
||||
|
||||
Mehrfachauswahl + Gruppieren + Gruppen-Verschieben, auf dem UX-Fundament
|
||||
(t_40d855d0) aufgebaut, nach dem Chef-Grundsatz:
|
||||
|
||||
- **Mehrfachauswahl (`core/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).
|
||||
- **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
|
||||
Gruppen-Mitglied nimmt automatisch die ganze Gruppe (und markiert sie),
|
||||
die relative Anordnung bleibt exakt erhalten. Drehen geschieht um den
|
||||
Gruppen-Mittelpunkt. „Gruppe lösen“ befreit ALLE Mitglieder (auch die
|
||||
nicht mitgewählten), Strg+Z baut sie wieder zusammen.
|
||||
- **ENTSCHIEDUNG Gruppen-Persistenz:** Gruppen-Zugehörigkeit liegt als
|
||||
OPTIONALES Objekt-Feld `"group"` (int ≥ 0, abwesend = ungruppiert)
|
||||
DIREKT im svmap-Format — nicht als Editor-Sitzungsstatus. Damit
|
||||
überleben Gruppen Speichern/Laden UND Undo/Redo (ActionHistory kopiert
|
||||
den objects-Block tief, das Feld reist mit). Loader-Kompatibilität
|
||||
nachgewiesen (scopedvr-match-server `src/maps/svmap_loader.gd`):
|
||||
V4 prüft nur Pflichtfelder, unbekannte Objekt-Keys werden nicht
|
||||
abgelehnt, die SHA-256-Prüfsumme läuft über das ganze kanonisierte
|
||||
Dict (Feld reist automatisch mit). Alte Maps ohne Feld bleiben 1:1
|
||||
ladbar, `format_version` bleibt 1 — KEIN Version-Bump, KEINE
|
||||
Server-Loader-Änderung nötig. Format-Doku-Anpassung (map_format.md im
|
||||
Server-Repo) als Folge-Karte.
|
||||
- **Gruppen-Verschieben + Undo:** Ziehen einer Mehrfachauswahl oder Gruppe
|
||||
bewegt ALLE beteiligten Objekte (Terrain-Clamp PRO Objekt: X/Z bleiben
|
||||
in der Map, Y = Terrain-Höhe an der neuen Stelle) — EINE Aktion =
|
||||
EIN ActionHistory-Schritt (auch gemeinsames Drehen, Mausrad-Ticks
|
||||
derselben Auswahl sind merge-gekoppelt). Löschen löscht die ganze
|
||||
Auswahl in EINEM Schritt.
|
||||
- **Tests:** `tests/test_ux.gd` erweitert auf 52 Checks (Gruppen-
|
||||
gruppieren/lösen/IDs, gemeinsames Verschieben mit Terrain-Clamp,
|
||||
Drehen um die Mitte mit Anordnungs-Erhalt, EIN-Schritt-Undo/Redo,
|
||||
svmap-Roundtrip inkl. Prüfsumme + alte-Map-Kompatibilität,
|
||||
Rubberband-Projektion inkl. Kamera-Rückseiten-Filter). Komplette
|
||||
`run_tests.sh`-Suite grün (206 Checks).
|
||||
- **Desktop-Smoke:** `godot … -- --groups-demo [ordner] [--groups-demo-quit]`
|
||||
fährt Mehrfachauswahl → Gruppieren → gemeinsames Ziehen/Drehen →
|
||||
Undo programmatisch durch und speichert Screenshots.
|
||||
|
||||
## Karte testen (M2, Karte t_b84af40f — FERTIG)
|
||||
|
||||
Button „▶ Karte testen“ oben (auch im Datei-Menü) startet die AKTUELLE Map —
|
||||
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
|
||||
`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
|
||||
noch nicht lauffähig (z. B. Spawn-Zonen fehlen), kommt der Klartext-Block
|
||||
mit den fehlenden Punkten, GESTARTET wird nichts.
|
||||
- **Client-Suche (erste Treffer-Zeile gewinnt):**
|
||||
1. Konfigurationsdatei `user://scopedvr_editor.cfg`:
|
||||
```ini
|
||||
[test]
|
||||
client_dir="/pfad/zu/scopedvr-client"
|
||||
godot_path="/pfad/zu/godot" ; nur nötig, wenn der Editor NICHT über Godot läuft
|
||||
```
|
||||
2. Klon neben dem Editor: `<Editor-Ordner>/../scopedvr-client`
|
||||
(erkennbar an `project.godot`).
|
||||
- **Start:** Der Client läuft als eigener Godot-Prozess
|
||||
(`godot --path <client> -- --testmap <map.svmap> --mock-master`) — kein
|
||||
Master, kein Match-Server, alles lokal. Godot-Binary = das, unter dem der
|
||||
Editor selbst läuft.
|
||||
- **Tests:** `tests/test_testrun.gd` (14 Checks: Temp-Export mit Roundtrip,
|
||||
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`).
|
||||
|
||||
|
|
|
|||
85
README_INSTALL.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# 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.*
|
||||
19
addons/modular_builder/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Changelog
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Add gable walls, gable/shed/pyramid roofs, stairs, fences, and parapets.
|
||||
- Add plain pillars between vertices or vertically from one vertex with `Alt+click`.
|
||||
- Add manual vertex creation, movement, removal, merging, and simplified marker display.
|
||||
- Add wall/gable/pillar centering controls and editable asymmetric dimensions.
|
||||
- Export a finished structure as one textured mesh in a new scene or as a copy beside the editable structure.
|
||||
- Include the demo scene and its example textures.
|
||||
- Split the editor and geometry code into focused scripts.
|
||||
|
||||
## 0.1.0 — Initial release
|
||||
|
||||
- Create square and triangular foundations, walls, and square or triangular floors directly in the Godot editor.
|
||||
- Snap pieces together using visible vertices and directional foundation placement controls.
|
||||
- Split walls at intermediate foundation vertices for separate texturing.
|
||||
- Add windows, doors, and hatches as openings or separate editable objects.
|
||||
- Generate general concave collision and save reusable building modules.
|
||||
125
addons/modular_builder/LICENSE
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
CC0 1.0 Universal
|
||||
|
||||
Copyright (c) 2026 Michel Savadim
|
||||
|
||||
This CC0 dedication applies only to the original Modular Builder source code,
|
||||
icons, and documentation in this directory authored by Michel Savadim. It does
|
||||
not apply to Godot Engine, user-created project content, trademarks, patents,
|
||||
or third-party materials distributed separately.
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
|
||||
https://creativecommons.org/publicdomain/zero/1.0/legalcode
|
||||
37
addons/modular_builder/README.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Modular Builder 0.2.0
|
||||
|
||||
Build modular structures directly in the Godot 4.7 editor. The panel appears at the bottom of the 3D editor. Dimensions are shown in meters; one Godot 3D unit equals one meter.
|
||||
|
||||
## 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/`.
|
||||
|
||||
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.
|
||||
|
||||
## Build
|
||||
|
||||
Click **Add ModularBuilderPiece** to create a structure container and its first foundation. Green tools choose square or triangular foundations; a green `+` on a free edge attaches another foundation. Red tools place walls and triangular gables between vertices. Yellow tools place square floors using opposite diagonal vertices or triangular floors using three vertices. Wall segments crossing several foundations are split at intermediate foundation vertices so each segment can be textured separately.
|
||||
|
||||
Blue markers show connection vertices; selected points turn yellow. Enable **Only show points on selected pieces** to reduce clutter, or **Simplify vertices** to show points at the middle of a piece's thickness. The separate vertex tools merge markers and create, move, or remove manual connection points. Manual vertices belong to their selected piece or detail and do not deform its mesh.
|
||||
|
||||
The blue roof and stairs tools create gable, shed, and pyramid roofs and stairs. Gable roofs use two gable peaks or diagonal top corners of opposite walls. Shed roofs use a low point and a diagonally opposite high point; pyramid roofs use two diagonal points at the same height. The Inspector lets you adjust roof width and rise, stair width, and step count. Walls can be placed on other walls or floors. **Center walls** and **Center** for gables align them with supporting walls.
|
||||
|
||||
Pink detail tools place windows, doors, hatches, pillars, fences, and parapets. Windows, doors, and hatches can be openings alone or openings with separately editable and textured objects. Pillars connect two vertices; `Alt+click` one vertex to create a vertical pillar from it. The **Center** option aligns a pillar with the supporting piece. Fences have two rails; parapets have one thicker rail. Their height and intermediate post count are editable.
|
||||
|
||||
Building pieces start with **Symmetric dimensions** enabled. Turn it off to edit width, height, and depth independently. **Thickness** continues to control the relevant dimension for walls, floors, and foundations. Select a piece in the Scene tree to edit its material and dimensions, or use the rotation button to rotate it by 90 degrees. The active creation tool remains selected until you click it again or choose another tool. `Esc` clears the current points; `Ctrl+Z` undoes placements and vertex edits.
|
||||
|
||||
## Collision, modules, and export
|
||||
|
||||
Pieces have no collision by default. Select the `ModularBuilderPiece` container and use **Add general collision** to generate or update one concave static collision shape for the structure. Connection markers and internal contact faces are excluded.
|
||||
|
||||
Save a selected structure as a reusable `.tscn` module, or insert a saved module into the open scene. For a finished structure, **Export** combines visible meshes into one `MeshInstance3D`, preserving material surfaces and UVs. Choose **New scene** for an independent `.tscn`, or **Copy in this scene** to add a sibling of the editable `ModularBuilderPiece`. The original remains editable.
|
||||
|
||||
## 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 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.
|
||||
|
||||
## AI usage disclosure
|
||||
|
||||
Generative AI (OpenAI ChatGPT/Codex) assisted with GDScript implementation and revisions, debugging, translation from Portuguese to English, and documentation. Michel Savadim defined the requirements and reviewed and tested the project.
|
||||
6
addons/modular_builder/icons/add_building.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="m5 17 15-8 15 8-15 8L5 17Z"/><path d="M5 17v16l15 8 15-8V17M20 25v16"/>
|
||||
<path d="M38 13v16m-8-8h16"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
43
addons/modular_builder/icons/add_building.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://kwgsfdxprj4p"
|
||||
path="res://.godot/imported/add_building.svg-02348d81d48c2064f5946a668739ab6e.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"]
|
||||
|
||||
[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
|
||||
3
addons/modular_builder/icons/cancel.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<path d="m12 12 24 24m0-24L12 36" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 195 B |
43
addons/modular_builder/icons/cancel.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://np6bb2n6ohj3"
|
||||
path="res://.godot/imported/cancel.svg-48b548656752953c76fdbf1fa691b961.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/cancel.svg"
|
||||
dest_files=["res://.godot/imported/cancel.svg-48b548656752953c76fdbf1fa691b961.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
|
||||
6
addons/modular_builder/icons/collision.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="m5 16 14-8 14 8-14 8L5 16Z"/><path d="M5 16v17l14 8 14-8V16M19 24v17"/>
|
||||
<path d="M39 23v16m-8-8h16"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
43
addons/modular_builder/icons/collision.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cjc2d332bqgfy"
|
||||
path="res://.godot/imported/collision.svg-c665b3652a42642be8f703d82faa449e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/collision.svg"
|
||||
dest_files=["res://.godot/imported/collision.svg-c665b3652a42642be8f703d82faa449e.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
addons/modular_builder/icons/create_vertex.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<circle cx="32" cy="32" r="19" fill="#9b7bd1" stroke="#222" stroke-width="4"/>
|
||||
<path d="M32 20v24M20 32h24" stroke="#fff" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 259 B |
43
addons/modular_builder/icons/create_vertex.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[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
|
||||
1
addons/modular_builder/icons/door.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round"><path d="M12 54V10h40v44"/><path d="M19 54V17l26-4v41"/></g><circle cx="39" cy="35" r="2.8" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 265 B |
43
addons/modular_builder/icons/door.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://booy2vg0ci6p7"
|
||||
path="res://.godot/imported/door.svg-5067a7f9805041b1e0019db73b7f247e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/door.svg"
|
||||
dest_files=["res://.godot/imported/door.svg-5067a7f9805041b1e0019db73b7f247e.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
|
||||
5
addons/modular_builder/icons/fence.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M9 24h46M9 42h46" fill="none" stroke="#eee" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M15 13v39M32 13v39M49 13v39" fill="none" stroke="#eee" stroke-width="7" stroke-linecap="round"/>
|
||||
<path d="M15 13v39M32 13v39M49 13v39" fill="none" stroke="#222" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 404 B |
43
addons/modular_builder/icons/fence.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://62ci7tsy4yeq"
|
||||
path="res://.godot/imported/fence.svg-b115d5a88a7e382552216868149e7186.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/fence.svg"
|
||||
dest_files=["res://.godot/imported/fence.svg-b115d5a88a7e382552216868149e7186.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
|
||||
6
addons/modular_builder/icons/floor_square.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="m8 18 16-9 16 9-16 9-16-9Z"/>
|
||||
<path d="M8 18v5l16 9 16-9v-5M24 27v5" opacity=".82"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 298 B |
43
addons/modular_builder/icons/floor_square.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bey5yf5dc53ms"
|
||||
path="res://.godot/imported/floor_square.svg-c870ce008800866d2fb4f4c19615fe80.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"]
|
||||
|
||||
[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
|
||||
6
addons/modular_builder/icons/floor_triangle.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="m24 9 18 29H6L24 9Z"/>
|
||||
<path d="M6 38v3l18 4 18-4v-3M24 38v7" opacity=".82"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 291 B |
43
addons/modular_builder/icons/floor_triangle.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dghymeocq75is"
|
||||
path="res://.godot/imported/floor_triangle.svg-cbffdc894d9762c4cb9806dbfa52405c.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"]
|
||||
|
||||
[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
|
||||
6
addons/modular_builder/icons/foundation_square.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M8 16 24 8l16 8-16 8-16-8Z"/>
|
||||
<path d="M8 16v17l16 8 16-8V16M24 24v17"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
43
addons/modular_builder/icons/foundation_square.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://3df01la6lxht"
|
||||
path="res://.godot/imported/foundation_square.svg-23bf2f453c993134ea4ea05145d98488.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"]
|
||||
|
||||
[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
|
||||
6
addons/modular_builder/icons/foundation_triangle.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M24 8 42 38H6L24 8Z"/>
|
||||
<path d="m24 16 10 17H14l10-17Z" opacity=".72"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
43
addons/modular_builder/icons/foundation_triangle.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ln0jh5ibvhe0"
|
||||
path="res://.godot/imported/foundation_triangle.svg-4b3e87f2198d6b6a8427029009457134.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"]
|
||||
|
||||
[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
addons/modular_builder/icons/gable_wall.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<path d="M8 54V29L32 9l24 20v25H8Z" fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round"/>
|
||||
<path d="M8 29h48M32 9v45" fill="none" stroke="#fff" stroke-width="3" opacity=".7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 262 B |
43
addons/modular_builder/icons/gable_wall.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b16auokjgcjuv"
|
||||
path="res://.godot/imported/gable_wall.svg-1d82415321f6b6ad59f51ba7640a6c53.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"]
|
||||
|
||||
[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
addons/modular_builder/icons/gable_wall_right.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M11 51V13l42 38z" fill="#eeeeee" stroke="#222222" stroke-width="5" stroke-linejoin="round"/>
|
||||
<circle cx="11" cy="51" r="4" fill="#c62828"/><circle cx="11" cy="13" r="4" fill="#c62828"/><circle cx="53" cy="51" r="4" fill="#c62828"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 336 B |
43
addons/modular_builder/icons/gable_wall_right.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://0jckxct26pqs"
|
||||
path="res://.godot/imported/gable_wall_right.svg-3336022dcfa05da5960f0dc8e3ecc6ff.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"]
|
||||
|
||||
[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
|
||||
1
addons/modular_builder/icons/hatch.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round"><path d="M8 20 32 9l24 11-24 12z"/><path d="M8 20v26l24 11 24-11V20M32 32v25"/><path d="m32 32 13-6v14l-13 6z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
43
addons/modular_builder/icons/hatch.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cf2okj2f2eq1h"
|
||||
path="res://.godot/imported/hatch.svg-f0ee2bc7f824245d3082af2deb5cb59a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/hatch.svg"
|
||||
dest_files=["res://.godot/imported/hatch.svg-f0ee2bc7f824245d3082af2deb5cb59a.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
|
||||
1
addons/modular_builder/icons/load_module.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round" d="M10 8h38l8 8v40H10zM19 8v17h25V8M19 56V35h26v21"/><path fill="none" stroke="#fff" stroke-width="4" d="M32 51V40m-6 5 6-6 6 6"/></svg>
|
||||
|
After Width: | Height: | Size: 293 B |
43
addons/modular_builder/icons/load_module.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dqhgwj72rp5o1"
|
||||
path="res://.godot/imported/load_module.svg-308877482b25606c5b7ed43292d640f3.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"]
|
||||
|
||||
[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
addons/modular_builder/icons/merge_vertices.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M15 15L32 32 49 15M32 32v17" fill="none" stroke="#eeeeee" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="15" cy="15" r="7" fill="#9b7bd1" stroke="#222" stroke-width="3"/><circle cx="49" cy="15" r="7" fill="#9b7bd1" stroke="#222" stroke-width="3"/><circle cx="32" cy="49" r="7" fill="#9b7bd1" stroke="#222" stroke-width="3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 460 B |
43
addons/modular_builder/icons/merge_vertices.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://n7jw3p1fa5eu"
|
||||
path="res://.godot/imported/merge_vertices.svg-d9204aed762a783069511285341aaecc.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"]
|
||||
|
||||
[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
addons/modular_builder/icons/move_vertex.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M32 6l-8 9h5v12h6V15h5zM58 32l-9-8v5H37v6h12v5zM32 58l8-9h-5V37h-6v12h-5zM6 32l9 8v-5h12v-6H15v-5z" fill="#eee" stroke="#222" stroke-width="2" stroke-linejoin="round"/>
|
||||
<circle cx="32" cy="32" r="8" fill="#9b7bd1" stroke="#222" stroke-width="3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
43
addons/modular_builder/icons/move_vertex.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://buetyw0nroq6k"
|
||||
path="res://.godot/imported/move_vertex.svg-a43c7dd371bb862b05efa6f900fd0556.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/move_vertex.svg"
|
||||
dest_files=["res://.godot/imported/move_vertex.svg-a43c7dd371bb862b05efa6f900fd0556.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
|
||||
6
addons/modular_builder/icons/parapet.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<g fill="none" stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="6">
|
||||
<path d="M12 50V14M52 50V14M12 25H52"/>
|
||||
<path d="M8 50H16M48 50H56"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
43
addons/modular_builder/icons/parapet.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bkxxyh8ec5oed"
|
||||
path="res://.godot/imported/parapet.svg-f91880fefed49e8dc8e533dcad6bb584.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/parapet.svg"
|
||||
dest_files=["res://.godot/imported/parapet.svg-f91880fefed49e8dc8e533dcad6bb584.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
|
||||
6
addons/modular_builder/icons/pillar.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round">
|
||||
<path d="M15 8h18v32H15z"/>
|
||||
<path d="M12 8h18l3 3M12 8v32l3 0M12 40h18l3-3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 257 B |
43
addons/modular_builder/icons/pillar.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dukm4mprtq7l"
|
||||
path="res://.godot/imported/pillar.svg-ccd9ad9da9982abc939a9856fa296417.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/pillar.svg"
|
||||
dest_files=["res://.godot/imported/pillar.svg-ccd9ad9da9982abc939a9856fa296417.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
addons/modular_builder/icons/remove_vertex.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<circle cx="32" cy="32" r="21" fill="#9b7bd1" stroke="#222" stroke-width="4"/>
|
||||
<path d="M20 32h24" stroke="#fff" stroke-width="7" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 250 B |
43
addons/modular_builder/icons/remove_vertex.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bjvg4halxhoy5"
|
||||
path="res://.godot/imported/remove_vertex.svg-824eacf485182fddca49a06831d65906.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"]
|
||||
|
||||
[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
addons/modular_builder/icons/roof_gable.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<path d="M5 30 32 8l27 22-5 6L32 19 10 36l-5-6Z" fill="#fff"/>
|
||||
<path d="M14 35v17h36V35L32 21 14 35Z" fill="none" stroke="#fff" stroke-width="4" stroke-linejoin="round" opacity=".75"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 257 B |
43
addons/modular_builder/icons/roof_gable.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dvtqw0h5dd5yl"
|
||||
path="res://.godot/imported/roof_gable.svg-8b4da12734ae02486f4e916ee6f2b16d.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"]
|
||||
|
||||
[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
addons/modular_builder/icons/roof_pyramid.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M8 45L32 13 56 45 32 53z" fill="#4d83ae" stroke="#222" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M32 13v40M8 45l24 8 24-8" fill="none" stroke="#eee" stroke-width="3" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
43
addons/modular_builder/icons/roof_pyramid.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ccew6wsdsj0kn"
|
||||
path="res://.godot/imported/roof_pyramid.svg-97d144eff93367622d2cd47f9133b57d.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"]
|
||||
|
||||
[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
addons/modular_builder/icons/roof_shed.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<path d="m8 22 48-12v9L8 32v-10Z" fill="#fff"/>
|
||||
<path d="M13 34v18h38V24L13 34Z" fill="none" stroke="#fff" stroke-width="4" stroke-linejoin="round" opacity=".75"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
43
addons/modular_builder/icons/roof_shed.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://wyg331tl3rdt"
|
||||
path="res://.godot/imported/roof_shed.svg-47c9ee22596d5277674eb88f312c9478.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"]
|
||||
|
||||
[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
|
||||
6
addons/modular_builder/icons/rotate.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M37 20A15 15 0 0 0 11 14L7 19"/><path d="M7 10v9h9"/>
|
||||
<path d="M11 28a15 15 0 0 0 26 6l4-5"/><path d="M41 38v-9h-9"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 329 B |
43
addons/modular_builder/icons/rotate.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cxcb3kjn18udy"
|
||||
path="res://.godot/imported/rotate.svg-84e58dd8fc90e4d634e7fbc55f8eda70.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/rotate.svg"
|
||||
dest_files=["res://.godot/imported/rotate.svg-84e58dd8fc90e4d634e7fbc55f8eda70.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
|
||||
1
addons/modular_builder/icons/save_module.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round" d="M10 8h38l8 8v40H10zM19 8v17h25V8M19 56V35h26v21"/><path fill="none" stroke="#fff" stroke-width="4" d="M32 40v11m-6-6 6 6 6-6"/></svg>
|
||||
|
After Width: | Height: | Size: 293 B |
43
addons/modular_builder/icons/save_module.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://pnq0qqniy5hx"
|
||||
path="res://.godot/imported/save_module.svg-eecd37f83ae3c8dc3d40dabc8e8213e4.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"]
|
||||
|
||||
[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
addons/modular_builder/icons/stairs.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M8 52h12V40h12V28h12V16h12" fill="none" stroke="#eee" stroke-width="7" stroke-linejoin="round" stroke-linecap="square"/>
|
||||
<path d="M8 52h12V40h12V28h12V16h12" fill="none" stroke="#222" stroke-width="2" stroke-linejoin="round" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
43
addons/modular_builder/icons/stairs.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b6ibyaujhybao"
|
||||
path="res://.godot/imported/stairs.svg-9b9203b050bb5cf817168a5866b66b2a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/stairs.svg"
|
||||
dest_files=["res://.godot/imported/stairs.svg-9b9203b050bb5cf817168a5866b66b2a.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
|
||||
5
addons/modular_builder/icons/wall.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M8 10h32v28H8zM8 20h32M8 29h32M19 10v10m12 0v9M19 29v9"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 267 B |
43
addons/modular_builder/icons/wall.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cmkpq8teurcwt"
|
||||
path="res://.godot/imported/wall.svg-b57c613ae16e9db4887d98c30036dee9.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/wall.svg"
|
||||
dest_files=["res://.godot/imported/wall.svg-b57c613ae16e9db4887d98c30036dee9.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
|
||||
1
addons/modular_builder/icons/window.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round"><path d="M10 12h44v40H10z"/><path d="M32 12v40M10 32h44"/></g></svg>
|
||||
|
After Width: | Height: | Size: 222 B |
43
addons/modular_builder/icons/window.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cu21qr4i7dns1"
|
||||
path="res://.godot/imported/window.svg-b79f89a5f56cfe779422a43f44ba067f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder/icons/window.svg"
|
||||
dest_files=["res://.godot/imported/window.svg-b79f89a5f56cfe779422a43f44ba067f.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
|
||||
614
addons/modular_builder/modular_builder_editor_actions.gd
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder/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")
|
||||
var _scopedvr_export: RefCounted = null
|
||||
|
||||
func _add_piece() -> void:
|
||||
var root := get_editor_interface().get_edited_scene_root()
|
||||
if root == null or not root is Node3D:
|
||||
status_label.text = "Open a 3D scene before adding a piece."
|
||||
return
|
||||
var piece := Node3D.new()
|
||||
piece.name = "ModularBuilderPiece"
|
||||
piece.set_script(PIECE_SCRIPT)
|
||||
piece.set("piece_type", ModularBuilderPiece.PieceType.BUILDING)
|
||||
var module_size_units := _meters_to_godot_units(size_spin.value)
|
||||
piece.set("unit_size", module_size_units)
|
||||
var foundation := Node3D.new()
|
||||
foundation.name = "Foundation"
|
||||
foundation.set_script(PIECE_SCRIPT)
|
||||
var chosen_type := _foundation_type_selected
|
||||
if chosen_type == 1:
|
||||
foundation.set("piece_type", ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE)
|
||||
else:
|
||||
foundation.set("piece_type", ModularBuilderPiece.PieceType.FOUNDATION_SQUARE)
|
||||
foundation.set("unit_size", module_size_units)
|
||||
foundation.set("centered_origin", true)
|
||||
foundation.position.y = foundation.thickness * 0.5
|
||||
piece.add_child(foundation, true)
|
||||
foundation.owner = root
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Add ModularBuilderPiece")
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", root)
|
||||
undo.add_do_property(foundation, "owner", root)
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.add_do_method(get_editor_interface().get_selection(), "clear")
|
||||
undo.add_do_method(get_editor_interface().get_selection(), "add_node", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = foundation
|
||||
_active_button_key = TOOL_FOUNDATION_TRIANGLE if _foundation_type_selected == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE else TOOL_FOUNDATION_SQUARE
|
||||
_set_tool(TOOL_FOUNDATION)
|
||||
|
||||
func _meters_to_godot_units(meters: float) -> float:
|
||||
return meters * METERS_TO_GODOT_UNITS
|
||||
|
||||
func _show_save_module_dialog() -> void:
|
||||
var building := _get_selected_piece()
|
||||
if building == null:
|
||||
status_label.text = "Select a ModularBuilderPiece structure to save as a module."
|
||||
return
|
||||
_save_module_dialog.current_file = "%s.tscn" % building.name
|
||||
_save_module_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
func _show_load_module_dialog() -> void:
|
||||
_load_module_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
func _on_export_pressed() -> void:
|
||||
var building := _get_selected_piece()
|
||||
if building == null:
|
||||
status_label.text = "Select a ModularBuilderPiece to export."
|
||||
return
|
||||
var baked_mesh := _bake_building_mesh(building)
|
||||
if baked_mesh == null or baked_mesh.get_surface_count() == 0:
|
||||
status_label.text = "No visible meshes were found to export."
|
||||
return
|
||||
if _export_mode_option.get_selected_id() == EXPORT_LIBRARY:
|
||||
_export_to_library(building, baked_mesh)
|
||||
return
|
||||
if _export_mode_option.get_selected_id() == EXPORT_SCENE_COPY:
|
||||
_add_baked_copy(building, baked_mesh)
|
||||
return
|
||||
_pending_export_mesh = baked_mesh
|
||||
_pending_export_name = "%s_Baked" % building.name
|
||||
_pending_export_basis = building.global_transform.basis
|
||||
_export_scene_dialog.current_file = "%s.tscn" % _pending_export_name
|
||||
_export_scene_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
func _bake_building_mesh(building: ModularBuilderPiece) -> ArrayMesh:
|
||||
return MESH_BAKER.new().bake(building)
|
||||
|
||||
## Export-Modus EXPORT_LIBRARY (Karte t_82d2a8dd): Name-Dialog öffnen, danach
|
||||
## läuft die Objekt-Pipeline (scopedvr_export_ui.gd); der Status landet im
|
||||
## Dock-Statusfeld. Die Instanz bleibt gespeichert, damit der Dialog nicht
|
||||
## vom Garbage Collector gefressen wird, während er offen ist.
|
||||
func _export_to_library(building: ModularBuilderPiece, baked_mesh: ArrayMesh) -> void:
|
||||
_scopedvr_export = SCOPEDVR_EXPORT_UI.new()
|
||||
_scopedvr_export.starten(self, building, baked_mesh, func(text: String) -> void:
|
||||
status_label.text = str(text))
|
||||
|
||||
func _add_baked_copy(building: ModularBuilderPiece, mesh: ArrayMesh) -> void:
|
||||
var edited_scene_root := get_editor_interface().get_edited_scene_root()
|
||||
if edited_scene_root == null or building.get_parent() == null:
|
||||
status_label.text = "Open a scene before adding the exported copy."
|
||||
return
|
||||
var sibling_parent := building.get_parent()
|
||||
var baked_root := _make_baked_scene_root(building.name + "_Baked", mesh)
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Export structure as a mesh copy")
|
||||
undo.add_do_method(self, "_attach_baked_copy", sibling_parent, edited_scene_root, baked_root, building.global_transform)
|
||||
undo.add_undo_method(self, "_detach_baked_copy", sibling_parent, baked_root)
|
||||
undo.commit_action()
|
||||
status_label.text = "Copy exported as one mesh with %d material surface(s). Ctrl+Z removes the copy." % mesh.get_surface_count()
|
||||
|
||||
func _attach_baked_copy(sibling_parent: Node, edited_scene_root: Node, baked_root: Node3D, target_transform: Transform3D) -> void:
|
||||
if baked_root.get_parent() != sibling_parent:
|
||||
if baked_root.get_parent() != null:
|
||||
baked_root.get_parent().remove_child(baked_root)
|
||||
sibling_parent.add_child(baked_root, true)
|
||||
baked_root.global_transform = target_transform
|
||||
baked_root.owner = edited_scene_root
|
||||
_set_export_owner_recursive(baked_root, edited_scene_root)
|
||||
var editor_selection := get_editor_interface().get_selection()
|
||||
editor_selection.clear()
|
||||
editor_selection.add_node(baked_root)
|
||||
|
||||
func _detach_baked_copy(sibling_parent: Node, baked_root: Node3D) -> void:
|
||||
if baked_root.get_parent() == sibling_parent:
|
||||
sibling_parent.remove_child(baked_root)
|
||||
|
||||
func _make_baked_scene_root(root_name: String, mesh: ArrayMesh) -> Node3D:
|
||||
var baked_root := Node3D.new()
|
||||
baked_root.name = root_name
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = "BakedMesh"
|
||||
mesh_instance.mesh = mesh
|
||||
baked_root.add_child(mesh_instance)
|
||||
return baked_root
|
||||
|
||||
func _export_baked_scene_to_path(path: String) -> void:
|
||||
if _pending_export_mesh == null:
|
||||
status_label.text = "The exported mesh is no longer available. Bake it again."
|
||||
return
|
||||
var baked_root := _make_baked_scene_root(_pending_export_name, _pending_export_mesh)
|
||||
baked_root.transform = Transform3D(_pending_export_basis, Vector3.ZERO)
|
||||
_set_export_owner_recursive(baked_root, baked_root)
|
||||
var packed := PackedScene.new()
|
||||
var pack_error := packed.pack(baked_root)
|
||||
baked_root.free()
|
||||
if pack_error != OK:
|
||||
status_label.text = "Could not pack the exported mesh (%s)." % error_string(pack_error)
|
||||
return
|
||||
var save_error := ResourceSaver.save(packed, path)
|
||||
if save_error != OK:
|
||||
status_label.text = "Could not save the exported scene (%s)." % error_string(save_error)
|
||||
return
|
||||
get_editor_interface().get_resource_filesystem().scan()
|
||||
status_label.text = "Exported scene: %s (%d material surface(s))." % [path, _pending_export_mesh.get_surface_count()]
|
||||
_pending_export_mesh = null
|
||||
|
||||
func _set_export_owner_recursive(node: Node, scene_owner: Node) -> void:
|
||||
for child in node.get_children():
|
||||
child.owner = scene_owner
|
||||
_set_export_owner_recursive(child, scene_owner)
|
||||
|
||||
func _save_module_to_path(path: String) -> void:
|
||||
var building := _get_selected_piece()
|
||||
if building == null:
|
||||
status_label.text = "Select a ModularBuilderPiece structure to save as a module."
|
||||
return
|
||||
var descendants: Array[Node] = []
|
||||
_collect_module_nodes(building, descendants)
|
||||
var original_owners: Dictionary = {}
|
||||
for node in descendants:
|
||||
original_owners[node] = node.owner
|
||||
node.owner = building
|
||||
var packed := PackedScene.new()
|
||||
var pack_error := packed.pack(building)
|
||||
for node in descendants:
|
||||
node.owner = original_owners[node]
|
||||
if pack_error != OK:
|
||||
status_label.text = "Could not pack the structure as a scene (%s)." % error_string(pack_error)
|
||||
return
|
||||
var save_error := ResourceSaver.save(packed, path)
|
||||
if save_error != OK:
|
||||
status_label.text = "Could not save the module (%s)." % error_string(save_error)
|
||||
return
|
||||
status_label.text = "Module saved: %s" % path
|
||||
|
||||
func _collect_module_nodes(node: Node, result: Array[Node]) -> void:
|
||||
for child in node.get_children():
|
||||
if child.name == "ModularBuilderMarkers" or child.has_meta("modular_builder_preview"):
|
||||
continue
|
||||
result.append(child)
|
||||
_collect_module_nodes(child, result)
|
||||
|
||||
func _load_module_from_path(path: String) -> void:
|
||||
var packed := ResourceLoader.load(path) as PackedScene
|
||||
if packed == null:
|
||||
status_label.text = "The selected file is not a valid Godot scene."
|
||||
return
|
||||
var instance := packed.instantiate()
|
||||
if not instance is Node3D:
|
||||
instance.free()
|
||||
status_label.text = "The module scene must have a Node3D root."
|
||||
return
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
if scene_root == null:
|
||||
instance.free()
|
||||
status_label.text = "Open a scene before inserting a module."
|
||||
return
|
||||
var anchor := _get_selected_piece()
|
||||
if anchor != null:
|
||||
var target_world := anchor.global_position + anchor.global_transform.basis.x * maxf(anchor.unit_size, 1.0)
|
||||
instance.position = scene_root.to_local(target_world)
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Insert module %s" % instance.name)
|
||||
undo.add_do_method(self, "_attach_module", scene_root, instance)
|
||||
undo.add_undo_method(self, "_detach_module", scene_root, instance)
|
||||
undo.commit_action()
|
||||
status_label.text = "Module inserted. Move the piece to the desired location."
|
||||
|
||||
func _attach_module(scene_root: Node, instance: Node) -> void:
|
||||
if instance.get_parent() != scene_root:
|
||||
scene_root.add_child(instance, true)
|
||||
instance.owner = scene_root
|
||||
var editor_selection := get_editor_interface().get_selection()
|
||||
editor_selection.clear()
|
||||
editor_selection.add_node(instance)
|
||||
|
||||
func _detach_module(scene_root: Node, instance: Node) -> void:
|
||||
if instance.get_parent() == scene_root:
|
||||
scene_root.remove_child(instance)
|
||||
|
||||
func _rotate_selected_piece() -> void:
|
||||
var selected_piece := _raw_selected_piece()
|
||||
var target := selected_piece
|
||||
if selected_piece == null:
|
||||
target = _last_created_piece if is_instance_valid(_last_created_piece) and _last_created_piece.is_inside_tree() else null
|
||||
elif selected_piece.piece_type == ModularBuilderPiece.PieceType.BUILDING:
|
||||
target = null
|
||||
if is_instance_valid(_last_created_piece) and _last_created_piece.is_inside_tree() and _is_descendant_of(_last_created_piece, selected_piece):
|
||||
target = _last_created_piece
|
||||
if target == null:
|
||||
status_label.text = "Select or create a piece to rotate."
|
||||
return
|
||||
var old_rotation := target.rotation_degrees
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Rotate piece 90 degrees")
|
||||
undo.add_do_property(target, "rotation_degrees", old_rotation + Vector3(0.0, 90.0, 0.0))
|
||||
undo.add_undo_property(target, "rotation_degrees", old_rotation)
|
||||
undo.commit_action()
|
||||
_last_created_piece = target
|
||||
status_label.text = "%s rotated by 90°." % target.name
|
||||
update_overlays()
|
||||
|
||||
func _is_descendant_of(node: Node, ancestor: Node) -> bool:
|
||||
var current := node.get_parent()
|
||||
while current != null:
|
||||
if current == ancestor:
|
||||
return true
|
||||
current = current.get_parent()
|
||||
return false
|
||||
|
||||
func _raw_selected_piece() -> ModularBuilderPiece:
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected.size() == 1 and selected[0] is ModularBuilderPiece:
|
||||
return selected[0] as ModularBuilderPiece
|
||||
return null
|
||||
|
||||
func _add_general_collision() -> void:
|
||||
var piece := _get_selected_piece()
|
||||
if piece == null:
|
||||
status_label.text = "Select a structure to generate general collision."
|
||||
return
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
var faces := PackedVector3Array()
|
||||
var structure_pieces: Array[ModularBuilderPiece] = []
|
||||
_collect_structure_pieces(piece, structure_pieces)
|
||||
_collect_scene_pieces(scene_root, structure_pieces)
|
||||
_collect_collision_faces(piece, piece, faces, {}, structure_pieces)
|
||||
if faces.is_empty():
|
||||
status_label.text = "The selected structure has no geometry for collision."
|
||||
return
|
||||
var concave := ConcavePolygonShape3D.new()
|
||||
concave.set_faces(faces)
|
||||
var body := piece.find_child("GeneralCollision", true, false) as StaticBody3D
|
||||
var undo := get_undo_redo()
|
||||
if body != null:
|
||||
var shape_node := body.get_node_or_null("Shape") as CollisionShape3D
|
||||
if shape_node == null:
|
||||
shape_node = CollisionShape3D.new()
|
||||
shape_node.name = "Shape"
|
||||
shape_node.shape = concave
|
||||
undo.create_action("Add shape to general collision")
|
||||
undo.add_do_method(body, "add_child", shape_node, true)
|
||||
undo.add_do_property(shape_node, "owner", scene_root)
|
||||
undo.add_undo_method(body, "remove_child", shape_node)
|
||||
else:
|
||||
var old_shape: Shape3D = shape_node.shape
|
||||
undo.create_action("Update general collision")
|
||||
undo.add_do_property(shape_node, "shape", concave)
|
||||
undo.add_undo_property(shape_node, "shape", old_shape)
|
||||
else:
|
||||
body = StaticBody3D.new()
|
||||
body.name = "GeneralCollision"
|
||||
var shape_node := CollisionShape3D.new()
|
||||
shape_node.name = "Shape"
|
||||
shape_node.shape = concave
|
||||
body.add_child(shape_node)
|
||||
undo.create_action("Add general collision")
|
||||
undo.add_do_method(piece, "add_child", body, true)
|
||||
undo.add_do_property(body, "owner", scene_root)
|
||||
undo.add_do_property(shape_node, "owner", scene_root)
|
||||
undo.add_undo_method(piece, "remove_child", body)
|
||||
undo.commit_action()
|
||||
status_label.text = "General collision added or updated. Snap markers are excluded."
|
||||
|
||||
func _collect_structure_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece:
|
||||
pieces.append(node)
|
||||
for child in node.get_children():
|
||||
if child.name == "ModularBuilderMarkers" or child.name == "GeneralCollision":
|
||||
continue
|
||||
_collect_structure_pieces(child, pieces)
|
||||
|
||||
func _collect_scene_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and not pieces.has(node):
|
||||
pieces.append(node)
|
||||
for child in node.get_children():
|
||||
if child.name == "ModularBuilderMarkers" or child.name == "GeneralCollision":
|
||||
continue
|
||||
_collect_scene_pieces(child, pieces)
|
||||
|
||||
func _collect_collision_faces(node: Node, piece_root: ModularBuilderPiece, faces: PackedVector3Array, seen_faces: Dictionary, structure_pieces: Array[ModularBuilderPiece]) -> void:
|
||||
if node is MeshInstance3D and node.name == "Geometry" and node.mesh != null:
|
||||
var relative_transform: Transform3D = piece_root.global_transform.affine_inverse() * node.global_transform
|
||||
var mesh_faces: PackedVector3Array = node.mesh.get_faces()
|
||||
var owner_piece := node.get_parent() as ModularBuilderPiece
|
||||
for i in range(0, mesh_faces.size(), 3):
|
||||
if i + 2 >= mesh_faces.size():
|
||||
break
|
||||
var local_a: Vector3 = mesh_faces[i]
|
||||
var local_b: Vector3 = mesh_faces[i + 1]
|
||||
var local_c: Vector3 = mesh_faces[i + 2]
|
||||
if _is_connection_face(owner_piece, local_a, local_b, local_c, structure_pieces):
|
||||
continue
|
||||
# Procedural meshes have both sides. Deduplication may retain
|
||||
# either orientation; normalize top faces beforehand to
|
||||
# keep custom foundations navigable.
|
||||
if _is_piece_top_face(owner_piece, local_a, local_b, local_c):
|
||||
if (local_b - local_a).cross(local_c - local_a).y < 0.0:
|
||||
var swap := local_b
|
||||
local_b = local_c
|
||||
local_c = swap
|
||||
var a: Vector3 = relative_transform * local_a
|
||||
var b: Vector3 = relative_transform * local_b
|
||||
var c: Vector3 = relative_transform * local_c
|
||||
var key_parts := [_collision_vertex_key(a), _collision_vertex_key(b), _collision_vertex_key(c)]
|
||||
key_parts.sort()
|
||||
var face_key := "%s|%s|%s" % key_parts
|
||||
if seen_faces.has(face_key):
|
||||
continue
|
||||
seen_faces[face_key] = true
|
||||
faces.append_array(PackedVector3Array([a, b, c]))
|
||||
for child in node.get_children():
|
||||
if child.name == "ModularBuilderMarkers" or child.name == "GeneralCollision":
|
||||
continue
|
||||
_collect_collision_faces(child, piece_root, faces, seen_faces, structure_pieces)
|
||||
|
||||
func _is_piece_top_face(piece: ModularBuilderPiece, a: Vector3, b: Vector3, c: Vector3) -> bool:
|
||||
if piece == null:
|
||||
return false
|
||||
if piece.piece_type != ModularBuilderPiece.PieceType.FLOOR and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
return false
|
||||
if absf(a.y - b.y) > 0.002 or absf(a.y - c.y) > 0.002:
|
||||
return false
|
||||
var top_y := piece.thickness * 0.5 if piece.centered_origin else piece.thickness
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR and piece.custom_points.size() >= 3:
|
||||
top_y = piece.custom_points[0].y
|
||||
return absf(a.y - top_y) < 0.002 and absf(b.y - top_y) < 0.002 and absf(c.y - top_y) < 0.002
|
||||
|
||||
func _is_connection_face(piece: ModularBuilderPiece, a: Vector3, b: Vector3, c: Vector3, structure_pieces: Array[ModularBuilderPiece]) -> bool:
|
||||
if piece == null:
|
||||
return false
|
||||
if (piece.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or piece.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE) and _foundation_edge_is_shared(piece, a, b, c, structure_pieces):
|
||||
return true
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR and piece.custom_points.size() >= 3:
|
||||
var bottom_point := piece.custom_points[0] - Vector3.UP * piece.thickness
|
||||
return _triangle_on_plane(a, b, c, bottom_point, Vector3.UP)
|
||||
if piece.piece_type != ModularBuilderPiece.PieceType.WALL or piece.custom_points.size() < 2:
|
||||
return false
|
||||
var start: Vector3 = piece.custom_points[0]
|
||||
var finish: Vector3 = piece.custom_points[1]
|
||||
var direction := (finish - start).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * piece.thickness * 0.5
|
||||
var base_normal := (finish - start).cross(side).normalized()
|
||||
if _triangle_on_plane(a, b, c, start, base_normal):
|
||||
return true
|
||||
if _wall_has_floor_contact(piece, structure_pieces):
|
||||
if _triangle_on_plane(a, b, c, start + Vector3.UP * piece.unit_size, base_normal):
|
||||
return true
|
||||
for other in structure_pieces:
|
||||
if other == piece or other.piece_type != ModularBuilderPiece.PieceType.WALL or other.custom_points.size() < 2:
|
||||
continue
|
||||
for endpoint in piece.custom_points:
|
||||
var endpoint_world := piece.to_global(endpoint)
|
||||
for other_endpoint in other.custom_points:
|
||||
if endpoint_world.distance_to(other.to_global(other_endpoint)) < 0.02:
|
||||
var local_endpoint := piece.to_local(endpoint_world)
|
||||
if _triangle_on_plane(a, b, c, local_endpoint, direction):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _wall_has_floor_contact(wall: ModularBuilderPiece, structure_pieces: Array[ModularBuilderPiece]) -> bool:
|
||||
var endpoints: PackedVector3Array = wall.custom_points
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
var top := endpoint + Vector3.UP * wall.unit_size
|
||||
for wall_corner in [wall.to_global(top + side), wall.to_global(top - side)]:
|
||||
for other in structure_pieces:
|
||||
if other.piece_type != ModularBuilderPiece.PieceType.FLOOR or other.custom_points.size() < 3:
|
||||
continue
|
||||
for floor_point in other.custom_points:
|
||||
if wall_corner.distance_to(other.to_global(floor_point)) < 0.02:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _foundation_edge_is_shared(piece: ModularBuilderPiece, a: Vector3, b: Vector3, c: Vector3, structure_pieces: Array[ModularBuilderPiece]) -> bool:
|
||||
var polygon := _foundation_polygon(piece)
|
||||
for other in structure_pieces:
|
||||
if other == piece or (other.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and other.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE):
|
||||
continue
|
||||
var other_polygon := _foundation_polygon(other)
|
||||
for i in range(polygon.size()):
|
||||
var p0: Vector2 = polygon[i]
|
||||
var p1: Vector2 = polygon[(i + 1) % polygon.size()]
|
||||
for j in range(other_polygon.size()):
|
||||
var q0: Vector2 = other_polygon[j]
|
||||
var q1: Vector2 = other_polygon[(j + 1) % other_polygon.size()]
|
||||
var other_base_y := -other.thickness * 0.5 if other.centered_origin else 0.0
|
||||
var q0_local := piece.to_local(other.to_global(Vector3(q0.x, other_base_y, q0.y)))
|
||||
var q1_local := piece.to_local(other.to_global(Vector3(q1.x, other_base_y, q1.y)))
|
||||
var shared := _segments_overlap(p0, p1, Vector2(q0_local.x, q0_local.z), Vector2(q1_local.x, q1_local.z))
|
||||
var piece_base_y := -piece.thickness * 0.5 if piece.centered_origin else 0.0
|
||||
if shared and absf(q0_local.y - piece_base_y) < 0.02 and absf(q1_local.y - piece_base_y) < 0.02 and _triangle_on_edge(a, b, c, p0, p1):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _triangle_on_edge(a: Vector3, b: Vector3, c: Vector3, edge_a: Vector2, edge_b: Vector2) -> bool:
|
||||
return _point_on_edge(Vector2(a.x, a.z), edge_a, edge_b) and _point_on_edge(Vector2(b.x, b.z), edge_a, edge_b) and _point_on_edge(Vector2(c.x, c.z), edge_a, edge_b)
|
||||
|
||||
func _point_on_edge(point: Vector2, edge_a: Vector2, edge_b: Vector2) -> bool:
|
||||
var edge := edge_b - edge_a
|
||||
var length_squared := edge.length_squared()
|
||||
if length_squared < 0.000001:
|
||||
return false
|
||||
var t := (point - edge_a).dot(edge) / length_squared
|
||||
var closest := edge_a + edge * clampf(t, 0.0, 1.0)
|
||||
return t >= -0.002 and t <= 1.002 and point.distance_to(closest) < 0.002
|
||||
|
||||
func _segments_overlap(a: Vector2, b: Vector2, c: Vector2, d: Vector2) -> bool:
|
||||
var direction := b - a
|
||||
var length := direction.length()
|
||||
if length < 0.0001:
|
||||
return false
|
||||
var unit := direction / length
|
||||
if absf(unit.x * (c - a).y - unit.y * (c - a).x) > 0.02 or absf(unit.x * (d - a).y - unit.y * (d - a).x) > 0.02:
|
||||
return false
|
||||
var c_proj := (c - a).dot(unit)
|
||||
var d_proj := (d - a).dot(unit)
|
||||
var overlap := minf(length, maxf(c_proj, d_proj)) - maxf(0.0, minf(c_proj, d_proj))
|
||||
return overlap > 0.02
|
||||
|
||||
func _triangle_on_plane(a: Vector3, b: Vector3, c: Vector3, plane_point: Vector3, plane_normal: Vector3) -> bool:
|
||||
if plane_normal.length_squared() < 0.000001:
|
||||
return false
|
||||
var normal := plane_normal.normalized()
|
||||
return absf((a - plane_point).dot(normal)) < 0.002 and absf((b - plane_point).dot(normal)) < 0.002 and absf((c - plane_point).dot(normal)) < 0.002
|
||||
|
||||
func _collision_vertex_key(point: Vector3) -> String:
|
||||
return "%.4f,%.4f,%.4f" % [point.x, point.y, point.z]
|
||||
|
||||
func _set_tool(tool: String) -> void:
|
||||
active_tool = tool
|
||||
_detail_options_row.visible = _is_detail_tool(tool)
|
||||
_rise_options_row.visible = tool == TOOL_WALL or tool == TOOL_GABLE_WALL or tool == TOOL_GABLE_WALL_RIGHT or tool == TOOL_ROOF_GABLE or tool == TOOL_ROOF_PYRAMID
|
||||
_linear_options_row.visible = tool == TOOL_STAIRS or tool == TOOL_FENCE
|
||||
_stair_count_label.visible = tool == TOOL_STAIRS
|
||||
stair_count_spin.visible = tool == TOOL_STAIRS
|
||||
_stair_width_label.visible = tool == TOOL_STAIRS
|
||||
stair_width_spin.visible = tool == TOOL_STAIRS
|
||||
_fence_divisions_label.visible = tool == TOOL_FENCE
|
||||
fence_divisions_spin.visible = tool == TOOL_FENCE
|
||||
_gable_centralize_check.visible = tool == TOOL_WALL or tool == TOOL_GABLE_WALL or tool == TOOL_GABLE_WALL_RIGHT
|
||||
_rise_label.text = "Peak rise:"
|
||||
if not _is_detail_tool(tool):
|
||||
call("_clear_detail_preview")
|
||||
if tool == TOOL_NONE:
|
||||
_active_button_key = ""
|
||||
pending_points.clear()
|
||||
for button_tool in _tool_buttons:
|
||||
_tool_buttons[button_tool].set_pressed_no_signal(button_tool == _active_button_key)
|
||||
if tool == TOOL_NONE:
|
||||
status_label.text = "Tool deactivated."
|
||||
elif _get_selected_piece() == null:
|
||||
status_label.text = "Select the corresponding piece in the Scene tree."
|
||||
elif tool == TOOL_FOUNDATION:
|
||||
status_label.text = "Click a + button beside any foundation. Click the tool again to deactivate it."
|
||||
elif tool == TOOL_WINDOW or tool == TOOL_DOOR:
|
||||
status_label.text = "Click a wall face to insert the detail. Edit its mode and dimensions in the Inspector."
|
||||
elif tool == TOOL_HATCH:
|
||||
status_label.text = "Click a foundation or floor face to insert the hatch. Edit it in the Inspector."
|
||||
elif tool == TOOL_GABLE_WALL:
|
||||
status_label.text = "Click the two base vertices of the gable. Adjust peak height below."
|
||||
elif tool == TOOL_GABLE_WALL_RIGHT:
|
||||
status_label.text = "Click the base vertices; the second point sets the high side of the right triangular gable."
|
||||
elif tool == TOOL_WALL:
|
||||
status_label.text = "Click two vertices on foundations, floors, or wall tops. Enable Center walls to align the endpoints with supporting walls."
|
||||
elif tool == TOOL_ROOF_GABLE:
|
||||
status_label.text = "Click the peaks of two gables or diagonal corners atop opposite walls."
|
||||
elif tool == TOOL_ROOF_SHED:
|
||||
status_label.text = "Click a low point, then a diagonally opposite high point. The roof finds the other corners; adjust width in the Inspector."
|
||||
elif tool == TOOL_ROOF_PYRAMID:
|
||||
status_label.text = "Click two diagonal vertices at the same height. Adjust pyramid peak height below or in the Inspector."
|
||||
elif tool == TOOL_STAIRS:
|
||||
status_label.text = "Click the low point, then the high point to create stairs. Adjust steps and width below."
|
||||
elif tool == TOOL_FENCE or tool == TOOL_PARAPET:
|
||||
status_label.text = "Click two vertices to set the path. Adjust height and intermediate posts below."
|
||||
elif tool == TOOL_MERGE_VERTICES:
|
||||
status_label.text = "Click two vertices to merge their markers. Ctrl+Z undoes the merge."
|
||||
elif tool == TOOL_CREATE_VERTEX:
|
||||
status_label.text = "Click a surface to create a connection point linked to the selected piece or detail."
|
||||
elif tool == TOOL_MOVE_VERTEX:
|
||||
status_label.text = "Click a blue manual point, then the surface where you want to move it."
|
||||
elif tool == TOOL_REMOVE_VERTEX:
|
||||
status_label.text = "Click a blue manual point to remove it."
|
||||
elif tool == TOOL_PILLAR:
|
||||
status_label.text = "Click two vertices to connect them. Alt+click one vertex to create a vertical pillar with the panel length."
|
||||
else:
|
||||
status_label.text = "Click the blue structure vertices. Click the tool again to deactivate it."
|
||||
call("_sync_vertex_markers")
|
||||
update_overlays()
|
||||
|
||||
func _get_selected_piece() -> ModularBuilderPiece:
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected.is_empty():
|
||||
return null
|
||||
var common_root: ModularBuilderPiece
|
||||
for selected_node in selected:
|
||||
var selected_piece := selected_node as ModularBuilderPiece
|
||||
if selected_piece == null and selected_node is ModularBuilderDetail:
|
||||
var parent := selected_node.get_parent()
|
||||
while parent != null and not parent is ModularBuilderPiece:
|
||||
parent = parent.get_parent()
|
||||
selected_piece = parent as ModularBuilderPiece
|
||||
if selected_piece == null:
|
||||
return null
|
||||
var piece_root := selected_piece
|
||||
var ancestor := selected_piece.get_parent()
|
||||
while ancestor != null:
|
||||
if ancestor is ModularBuilderPiece and ancestor.piece_type == ModularBuilderPiece.PieceType.BUILDING:
|
||||
piece_root = ancestor
|
||||
break
|
||||
ancestor = ancestor.get_parent()
|
||||
if common_root == null:
|
||||
common_root = piece_root
|
||||
elif common_root != piece_root:
|
||||
return null
|
||||
return common_root
|
||||
|
||||
func _on_editor_selection_changed() -> void:
|
||||
if _redirecting_selection:
|
||||
return
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
var redirected_selection: Array[Node] = []
|
||||
var needs_redirect := false
|
||||
for selected_node in selected:
|
||||
var target_node: Node = selected_node
|
||||
if selected_node is MeshInstance3D:
|
||||
var ancestor := selected_node.get_parent()
|
||||
while ancestor != null and not (ancestor is ModularBuilderPiece or ancestor is ModularBuilderDetail):
|
||||
ancestor = ancestor.get_parent()
|
||||
if ancestor != null:
|
||||
target_node = ancestor
|
||||
needs_redirect = true
|
||||
if not redirected_selection.has(target_node):
|
||||
redirected_selection.append(target_node)
|
||||
if needs_redirect:
|
||||
_redirecting_selection = true
|
||||
var editor_selection := get_editor_interface().get_selection()
|
||||
editor_selection.clear()
|
||||
for target_node in redirected_selection:
|
||||
editor_selection.add_node(target_node)
|
||||
_redirecting_selection = false
|
||||
if active_tool != TOOL_NONE:
|
||||
pending_points.clear()
|
||||
_sync_pillar_centralize_control()
|
||||
call("_sync_vertex_markers")
|
||||
update_overlays()
|
||||
|
||||
func _on_marker_scope_toggled(_enabled: bool) -> void:
|
||||
call("_sync_vertex_markers")
|
||||
update_overlays()
|
||||
|
||||
func _on_simplify_vertices_toggled(_enabled: bool) -> void:
|
||||
pending_points.clear()
|
||||
call("_sync_vertex_markers")
|
||||
update_overlays()
|
||||
|
||||
func _foundation_polygon(root: ModularBuilderPiece) -> Array[Vector2]:
|
||||
if root.custom_points.size() >= 3:
|
||||
var custom: Array[Vector2] = []
|
||||
for point in root.custom_points:
|
||||
custom.append(Vector2(point.x, point.z))
|
||||
return custom
|
||||
var half := root.unit_size * 0.5
|
||||
if root.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
return [Vector2(-half, -half), Vector2(half, -half), Vector2(-half, half)]
|
||||
return [Vector2(-half, -half), Vector2(half, -half), Vector2(half, half), Vector2(-half, half)]
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bem5uer4jw8ik
|
||||
430
addons/modular_builder/modular_builder_editor_details.gd
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder/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")
|
||||
|
||||
func _create_detail_from_click(camera: Camera3D, screen_position: Vector2) -> bool:
|
||||
var placement := _find_detail_placement(camera, screen_position)
|
||||
if placement.is_empty():
|
||||
return false
|
||||
var detail := _new_detail(placement, false)
|
||||
var best_piece := placement.host as ModularBuilderPiece
|
||||
if best_piece == null:
|
||||
return false
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Add %s" % detail.name)
|
||||
undo.add_do_method(self, "_attach_detail", best_piece, detail, get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(self, "_detach_detail", best_piece, detail)
|
||||
undo.commit_action()
|
||||
get_editor_interface().get_selection().clear()
|
||||
get_editor_interface().get_selection().add_node(detail)
|
||||
_set_tool(TOOL_NONE)
|
||||
status_label.text = "%s created. Edit style, materials, and dimensions in the Inspector." % detail.name
|
||||
return true
|
||||
|
||||
func _create_pillar_from_vertices(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> void:
|
||||
var placement := _find_pillar_placement(root, first, second)
|
||||
if placement.is_empty():
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose two different structure vertices to connect with the pillar."
|
||||
return
|
||||
_commit_pillar_placement(placement, "Plain pillar created between vertices. Edit its section and material in the Inspector.")
|
||||
|
||||
func _create_vertical_pillar_from_vertex(root: ModularBuilderPiece, anchor: Vector3) -> void:
|
||||
pending_points.clear()
|
||||
var length := maxf(0.2, detail_height_spin.value * METERS_TO_GODOT_UNITS)
|
||||
var direction := _vertical_pillar_direction(root, anchor)
|
||||
var placement := _find_pillar_placement(root, anchor, anchor + direction * length)
|
||||
if placement.is_empty():
|
||||
status_label.text = "Could not place a vertical pillar at this vertex."
|
||||
return
|
||||
_commit_pillar_placement(placement, "Vertical pillar created from the vertex. Edit its length in the Inspector.")
|
||||
|
||||
func _commit_pillar_placement(placement: Dictionary, message: String) -> void:
|
||||
var detail := _new_detail(placement, false)
|
||||
var host := placement.host as ModularBuilderPiece
|
||||
detail.set("height", float(placement.length))
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Add plain pillar")
|
||||
undo.add_do_method(self, "_attach_detail", host, detail, get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(self, "_detach_detail", host, detail)
|
||||
undo.commit_action()
|
||||
get_editor_interface().get_selection().clear()
|
||||
get_editor_interface().get_selection().add_node(detail)
|
||||
_set_tool(TOOL_NONE)
|
||||
status_label.text = message
|
||||
|
||||
func _vertical_pillar_direction(root: ModularBuilderPiece, anchor: Vector3) -> Vector3:
|
||||
var selected_pillar := _get_selected_pillar_detail()
|
||||
if selected_pillar != null:
|
||||
var pillar_local := selected_pillar.to_local(anchor)
|
||||
if absf(pillar_local.x) <= selected_pillar.width * 0.5 + 0.05 and absf(pillar_local.z) <= selected_pillar.width * 0.5 + 0.05 and absf(absf(pillar_local.y) - selected_pillar.height * 0.5) <= 0.05:
|
||||
return PILLAR_SUPPORT.vertical_direction_for_pillar(selected_pillar, anchor)
|
||||
var source := _get_pillar_vertex_source(root, anchor)
|
||||
if source != null:
|
||||
return PILLAR_SUPPORT.vertical_direction_for_piece(source, anchor)
|
||||
var pillar := _find_pillar_at_vertex(root, anchor)
|
||||
return PILLAR_SUPPORT.vertical_direction_for_pillar(pillar, anchor) if pillar != null else Vector3.UP
|
||||
|
||||
func _find_pillar_at_vertex(node: Node, anchor: Vector3) -> ModularBuilderDetail:
|
||||
if node is ModularBuilderDetail and node.detail_type == ModularBuilderDetail.DetailType.PILLAR:
|
||||
var detail := node as ModularBuilderDetail
|
||||
var local := detail.to_local(anchor)
|
||||
var half := detail.width * 0.5
|
||||
var at_corner := absf(absf(local.x) - half) <= 0.05 and absf(absf(local.z) - half) <= 0.05
|
||||
var at_center := _simplify_vertices_check != null and _simplify_vertices_check.button_pressed and absf(local.x) <= 0.05 and absf(local.z) <= 0.05
|
||||
if (at_corner or at_center) and absf(absf(local.y) - detail.height * 0.5) <= 0.05:
|
||||
return detail
|
||||
for child in node.get_children():
|
||||
var pillar := _find_pillar_at_vertex(child, anchor)
|
||||
if pillar != null:
|
||||
return pillar
|
||||
return null
|
||||
|
||||
func _create_fence_decoration(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> void:
|
||||
if first.distance_to(second) < 0.05:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose two vertices far enough apart."
|
||||
return
|
||||
var is_parapet := active_tool == TOOL_PARAPET
|
||||
var detail := Node3D.new()
|
||||
detail.set_script(DETAIL_SCRIPT)
|
||||
detail.name = "Parapet" if is_parapet else "Fence"
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.PARAPET if is_parapet else ModularBuilderDetail.DetailType.FENCE)
|
||||
detail.set("detail_mode", ModularBuilderDetail.DetailMode.SEPARATE_OBJECT)
|
||||
detail.set("path_points", PackedVector3Array([root.to_local(first), root.to_local(second)]))
|
||||
detail.set("fence_height", fence_height_spin.value * METERS_TO_GODOT_UNITS)
|
||||
detail.set("fence_divisions", int(fence_divisions_spin.value))
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Add parapet" if is_parapet else "Add decorative fence")
|
||||
undo.add_do_method(self, "_attach_detail", root, detail, get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(self, "_detach_detail", root, detail)
|
||||
undo.commit_action()
|
||||
get_editor_interface().get_selection().clear()
|
||||
get_editor_interface().get_selection().add_node(detail)
|
||||
_set_tool(TOOL_NONE)
|
||||
pending_points.clear()
|
||||
status_label.text = "%s created with %d intermediate post(s)." % [detail.name, detail.fence_divisions]
|
||||
|
||||
func _find_pillar_placement(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> Dictionary:
|
||||
if first.distance_to(second) < 0.02:
|
||||
return {}
|
||||
var local_first := root.to_local(first)
|
||||
var local_second := root.to_local(second)
|
||||
var segment := local_second - local_first
|
||||
var segment_length := segment.length()
|
||||
if segment_length < 0.02:
|
||||
return {}
|
||||
var y_axis := segment / segment_length
|
||||
var reference_axis := Vector3.FORWARD if absf(y_axis.dot(Vector3.UP)) > 0.99 else Vector3.UP
|
||||
var x_axis := reference_axis.cross(y_axis).normalized()
|
||||
var z_axis := x_axis.cross(y_axis).normalized()
|
||||
var basis := Basis(x_axis, y_axis, z_axis).orthonormalized()
|
||||
var source := _get_pillar_vertex_source(root, first)
|
||||
var alignment_basis := Basis.IDENTITY
|
||||
if source != null:
|
||||
alignment_basis = (root.global_transform.basis.inverse() * source.global_transform.basis).orthonormalized()
|
||||
return {
|
||||
"host": root,
|
||||
"position": (local_first + local_second) * 0.5,
|
||||
"rotation": Vector3.ZERO,
|
||||
"basis": basis,
|
||||
"direction": segment,
|
||||
"alignment_basis": alignment_basis,
|
||||
"length": segment_length
|
||||
}
|
||||
|
||||
func _get_pillar_vertex_source(root: ModularBuilderPiece, point: Vector3) -> ModularBuilderPiece:
|
||||
var best_piece: ModularBuilderPiece
|
||||
var best_distance := 0.05
|
||||
var pieces: Array[ModularBuilderPiece] = []
|
||||
call("_collect_all_modular_pieces", root, pieces)
|
||||
for piece in pieces:
|
||||
if piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE and piece.piece_type != ModularBuilderPiece.PieceType.FLOOR and piece.piece_type != ModularBuilderPiece.PieceType.WALL and piece.piece_type != ModularBuilderPiece.PieceType.GABLE_WALL and piece.piece_type != ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT:
|
||||
continue
|
||||
var vertices: Array[Vector3] = []
|
||||
call("_collect_piece_geometry_vertices", piece, vertices)
|
||||
for vertex in vertices:
|
||||
var distance := vertex.distance_to(point)
|
||||
if _simplify_vertices_check != null and _simplify_vertices_check.button_pressed:
|
||||
distance = minf(distance, VERTEX_SIMPLIFIER.piece_vertex(piece, vertex).distance_to(point))
|
||||
if distance < best_distance:
|
||||
best_distance = distance
|
||||
best_piece = piece
|
||||
return best_piece
|
||||
|
||||
func _point_inside_surface_or_edge(piece: ModularBuilderPiece, point: Vector2, edge_tolerance: float) -> bool:
|
||||
if _point_inside_surface(piece, point):
|
||||
return true
|
||||
var polygon := _foundation_polygon(piece) if piece.piece_type != ModularBuilderPiece.PieceType.FLOOR else _piece_polygon_xz(piece)
|
||||
for i in range(polygon.size()):
|
||||
var a: Vector2 = polygon[i]
|
||||
var b: Vector2 = polygon[(i + 1) % polygon.size()]
|
||||
var edge := b - a
|
||||
var t := clampf((point - a).dot(edge) / maxf(edge.length_squared(), 0.000001), 0.0, 1.0)
|
||||
if point.distance_to(a + edge * t) <= edge_tolerance + 0.01:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _find_detail_placement(camera: Camera3D, screen_position: Vector2) -> Dictionary:
|
||||
var root := _get_selected_piece()
|
||||
if root == null or not _is_detail_tool(active_tool):
|
||||
return {}
|
||||
var ray_origin := camera.project_ray_origin(screen_position)
|
||||
var ray_direction := camera.project_ray_normal(screen_position)
|
||||
var candidates: Array[ModularBuilderPiece] = []
|
||||
if active_tool == TOOL_HATCH or active_tool == TOOL_PILLAR:
|
||||
_collect_surface_pieces(root, candidates)
|
||||
else:
|
||||
_collect_wall_pieces(root, candidates)
|
||||
var best_distance := INF
|
||||
var best_piece: ModularBuilderPiece
|
||||
var best_local := Vector3.ZERO
|
||||
var best_surface_y := 0.0
|
||||
for candidate in candidates:
|
||||
if not is_instance_valid(candidate) or candidate.piece_type == ModularBuilderPiece.PieceType.BUILDING:
|
||||
continue
|
||||
var local_origin := candidate.to_local(ray_origin)
|
||||
var local_direction := candidate.to_local(ray_origin + ray_direction) - local_origin
|
||||
if active_tool != TOOL_HATCH and candidate.custom_points.size() < 2:
|
||||
continue
|
||||
var uses_surface := active_tool == TOOL_HATCH or active_tool == TOOL_PILLAR
|
||||
var plane_normal := Vector3.UP if uses_surface else Vector3(-candidate.custom_points[1].z + candidate.custom_points[0].z, 0.0, candidate.custom_points[1].x - candidate.custom_points[0].x).normalized()
|
||||
if uses_surface and candidate.custom_points.size() >= 3:
|
||||
plane_normal = Vector3.UP
|
||||
if absf(local_direction.dot(plane_normal)) < 0.0001:
|
||||
continue
|
||||
var plane_y := 0.0
|
||||
var plane_point := Vector3.ZERO
|
||||
if uses_surface:
|
||||
plane_y = candidate.thickness * 0.5 if candidate.centered_origin else candidate.thickness
|
||||
plane_point.y = plane_y
|
||||
var distance := (plane_point - local_origin).dot(plane_normal) / local_direction.dot(plane_normal)
|
||||
if distance < 0.0 or distance >= best_distance:
|
||||
continue
|
||||
var hit := local_origin + local_direction * distance
|
||||
if uses_surface:
|
||||
if candidate.piece_type != ModularBuilderPiece.PieceType.FLOOR and candidate.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and candidate.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
continue
|
||||
if not _point_inside_surface(candidate, Vector2(hit.x, hit.z)):
|
||||
continue
|
||||
else:
|
||||
if candidate.piece_type != ModularBuilderPiece.PieceType.WALL or candidate.custom_points.size() < 2:
|
||||
continue
|
||||
var a: Vector3 = candidate.custom_points[0]
|
||||
var b: Vector3 = candidate.custom_points[1]
|
||||
var wall_dir := (b - a).normalized()
|
||||
var along := (hit - a).dot(wall_dir)
|
||||
var vertical := hit.y - a.y
|
||||
if along < 0.0 or along > a.distance_to(b) or vertical < 0.0 or vertical > candidate.unit_size:
|
||||
continue
|
||||
best_distance = distance
|
||||
best_piece = candidate
|
||||
best_local = hit
|
||||
best_surface_y = plane_y
|
||||
if best_piece == null:
|
||||
return {}
|
||||
# best_local uses the selected piece’s local coordinates. The child detail
|
||||
# must use the same space, especially for attached foundations.
|
||||
var host_local := best_local
|
||||
var detail_position: Vector3
|
||||
var detail_rotation := Vector3.ZERO
|
||||
if active_tool == TOOL_HATCH:
|
||||
detail_position = Vector3(host_local.x, 0.0, host_local.z)
|
||||
elif active_tool == TOOL_PILLAR:
|
||||
detail_position = Vector3(host_local.x, best_surface_y, host_local.z)
|
||||
else:
|
||||
var wall_a: Vector3 = best_piece.custom_points[0]
|
||||
var wall_b: Vector3 = best_piece.custom_points[1]
|
||||
var direction := (wall_b - wall_a).normalized()
|
||||
var n := Vector3(-direction.z, 0.0, direction.x)
|
||||
detail_rotation.y = atan2(n.x, n.z)
|
||||
var opening_center_y := wall_a.y + detail_height_spin.value * 0.5 + detail_height_offset_spin.value
|
||||
detail_position = Vector3((wall_a.x + wall_b.x) * 0.5, opening_center_y, (wall_a.z + wall_b.z) * 0.5)
|
||||
return {"host": best_piece, "position": detail_position, "rotation": detail_rotation}
|
||||
|
||||
func _new_detail(placement: Dictionary, preview: bool) -> ModularBuilderDetail:
|
||||
var detail := Node3D.new()
|
||||
detail.set_script(DETAIL_SCRIPT)
|
||||
if active_tool == TOOL_WINDOW:
|
||||
detail.name = "Window"
|
||||
elif active_tool == TOOL_DOOR:
|
||||
detail.name = "Door"
|
||||
elif active_tool == TOOL_HATCH:
|
||||
detail.name = "Hatch"
|
||||
elif active_tool == TOOL_PILLAR:
|
||||
detail.name = "Pillar"
|
||||
elif active_tool == TOOL_FENCE:
|
||||
detail.name = "Fence"
|
||||
else:
|
||||
detail.name = "Parapet"
|
||||
if active_tool == TOOL_WINDOW:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.WINDOW)
|
||||
elif active_tool == TOOL_DOOR:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.DOOR)
|
||||
elif active_tool == TOOL_HATCH:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.HATCH)
|
||||
elif active_tool == TOOL_FENCE:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.FENCE)
|
||||
elif active_tool == TOOL_PARAPET:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.PARAPET)
|
||||
else:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.PILLAR)
|
||||
detail.set("width", detail_width_spin.value * METERS_TO_GODOT_UNITS)
|
||||
detail.set("height", float(placement.length) if active_tool == TOOL_PILLAR and placement.has("length") else detail_height_spin.value * METERS_TO_GODOT_UNITS)
|
||||
detail.set("detail_mode", ModularBuilderDetail.DetailMode.SEPARATE_OBJECT if preview or active_tool == TOOL_PILLAR else detail_mode_option.get_selected_id())
|
||||
detail.set("surface_material", _preview_material if preview else null)
|
||||
if preview:
|
||||
var host := placement.host as ModularBuilderPiece
|
||||
var preview_depth := host.thickness if host != null else 0.08
|
||||
if active_tool == TOOL_WINDOW or active_tool == TOOL_DOOR:
|
||||
preview_depth *= 1.25
|
||||
detail.set("follow_host_thickness", false)
|
||||
detail.set("frame_depth", preview_depth)
|
||||
if active_tool == TOOL_WINDOW:
|
||||
detail.set("window_style", _selected_window_style)
|
||||
elif active_tool == TOOL_DOOR:
|
||||
detail.set("door_style", _selected_door_style)
|
||||
if preview:
|
||||
detail.name = "ModularBuilderPreview"
|
||||
detail.set_meta("modular_builder_preview", true)
|
||||
var detail_basis: Basis = placement.basis if placement.has("basis") else Basis.from_euler(placement.rotation)
|
||||
detail.transform = Transform3D(detail_basis, placement.position)
|
||||
if active_tool == TOOL_PILLAR:
|
||||
detail.set("pillar_direction", placement.direction)
|
||||
detail.set("pillar_alignment_basis", placement.alignment_basis)
|
||||
detail.set("center_align", _pillar_centralize_check.button_pressed)
|
||||
if active_tool == TOOL_FENCE or active_tool == TOOL_PARAPET:
|
||||
detail.set("fence_height", fence_height_spin.value * METERS_TO_GODOT_UNITS)
|
||||
detail.set("fence_divisions", int(fence_divisions_spin.value))
|
||||
return detail as ModularBuilderDetail
|
||||
|
||||
func _update_detail_preview() -> void:
|
||||
if not _is_detail_tool(active_tool) or active_tool == TOOL_FENCE or active_tool == TOOL_PARAPET or _preview_camera == null:
|
||||
return
|
||||
var placement: Dictionary
|
||||
if active_tool == TOOL_PILLAR:
|
||||
var root := _get_selected_piece()
|
||||
if root == null or pending_points.is_empty():
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.visible = false
|
||||
return
|
||||
var second_vertex := _nearest_vertex(_preview_camera, _preview_screen_position, call("_get_tool_vertices", root))
|
||||
if second_vertex == null:
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.visible = false
|
||||
return
|
||||
placement = _find_pillar_placement(root, pending_points[0], second_vertex)
|
||||
else:
|
||||
placement = _find_detail_placement(_preview_camera, _preview_screen_position)
|
||||
if placement.is_empty():
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.visible = false
|
||||
return
|
||||
if not is_instance_valid(_detail_preview) or _detail_preview.detail_type != _detail_type_for_tool(active_tool):
|
||||
_clear_detail_preview()
|
||||
_detail_preview = _new_detail(placement, true)
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
if scene_root == null:
|
||||
_detail_preview = null
|
||||
return
|
||||
var preview_host := placement.host as ModularBuilderPiece
|
||||
if active_tool == TOOL_PILLAR and preview_host != null:
|
||||
preview_host.add_child(_detail_preview, true)
|
||||
else:
|
||||
scene_root.add_child(_detail_preview, true)
|
||||
_detail_preview.owner = scene_root
|
||||
_detail_preview.visible = true
|
||||
if not is_equal_approx(_detail_preview.width, detail_width_spin.value):
|
||||
_detail_preview.width = detail_width_spin.value
|
||||
var preview_height := float(placement.length) if active_tool == TOOL_PILLAR and placement.has("length") else detail_height_spin.value
|
||||
if not is_equal_approx(_detail_preview.height, preview_height):
|
||||
_detail_preview.height = preview_height
|
||||
var host := placement.host as ModularBuilderPiece
|
||||
if host != null:
|
||||
var preview_basis: Basis = placement.basis if placement.has("basis") else Basis.from_euler(placement.rotation)
|
||||
if active_tool == TOOL_PILLAR:
|
||||
_detail_preview.transform = Transform3D(preview_basis, placement.position)
|
||||
_detail_preview.pillar_direction = placement.direction
|
||||
_detail_preview.pillar_alignment_basis = placement.alignment_basis
|
||||
_detail_preview.center_align = _pillar_centralize_check.button_pressed
|
||||
else:
|
||||
_detail_preview.global_transform = host.global_transform * Transform3D(preview_basis, placement.position)
|
||||
|
||||
func _detail_type_for_tool(tool: String) -> int:
|
||||
if tool == TOOL_DOOR:
|
||||
return ModularBuilderDetail.DetailType.DOOR
|
||||
if tool == TOOL_HATCH:
|
||||
return ModularBuilderDetail.DetailType.HATCH
|
||||
if tool == TOOL_PILLAR:
|
||||
return ModularBuilderDetail.DetailType.PILLAR
|
||||
if tool == TOOL_FENCE:
|
||||
return ModularBuilderDetail.DetailType.FENCE
|
||||
if tool == TOOL_PARAPET:
|
||||
return ModularBuilderDetail.DetailType.PARAPET
|
||||
return ModularBuilderDetail.DetailType.WINDOW
|
||||
|
||||
func _refresh_detail_preview(_value: float = 0.0) -> void:
|
||||
if _preview_camera != null:
|
||||
_update_detail_preview()
|
||||
|
||||
func _clear_detail_preview() -> void:
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.queue_free()
|
||||
_detail_preview = null
|
||||
|
||||
func _attach_detail(host: ModularBuilderPiece, detail: Node, scene_root: Node) -> void:
|
||||
if detail.get_parent() != host:
|
||||
host.add_child(detail, true)
|
||||
detail.owner = scene_root
|
||||
host._rebuild()
|
||||
|
||||
func _detach_detail(host: ModularBuilderPiece, detail: Node) -> void:
|
||||
if detail.get_parent() == host:
|
||||
host.remove_child(detail)
|
||||
host._rebuild()
|
||||
|
||||
func _collect_surface_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and (node.piece_type == ModularBuilderPiece.PieceType.FLOOR or node.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or node.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE):
|
||||
pieces.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_surface_pieces(child, pieces)
|
||||
|
||||
func _collect_wall_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and node.piece_type == ModularBuilderPiece.PieceType.WALL:
|
||||
pieces.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_wall_pieces(child, pieces)
|
||||
|
||||
func _point_inside_surface(piece: ModularBuilderPiece, point: Vector2) -> bool:
|
||||
var polygon := _foundation_polygon(piece) if piece.piece_type != ModularBuilderPiece.PieceType.FLOOR else _piece_polygon_xz(piece)
|
||||
if polygon.size() < 3:
|
||||
return false
|
||||
var inside := false
|
||||
var j := polygon.size() - 1
|
||||
for i in range(polygon.size()):
|
||||
var a: Vector2 = polygon[i]
|
||||
var b: Vector2 = polygon[j]
|
||||
if (a.y > point.y) != (b.y > point.y) and point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x:
|
||||
inside = not inside
|
||||
j = i
|
||||
return inside
|
||||
|
||||
func _piece_polygon_xz(piece: ModularBuilderPiece) -> Array[Vector2]:
|
||||
var polygon: Array[Vector2] = []
|
||||
for point in piece.custom_points:
|
||||
polygon.append(Vector2(point.x, point.z))
|
||||
return polygon
|
||||
|
||||
func _nearest_vertex(camera: Camera3D, screen_position: Vector2, vertices: Array[Vector3]) -> Variant:
|
||||
var nearest: Variant = null
|
||||
var nearest_distance := MARKER_HIT_RADIUS
|
||||
for world_point in vertices:
|
||||
if camera.is_position_behind(world_point):
|
||||
continue
|
||||
var distance := camera.unproject_position(world_point).distance_to(screen_position)
|
||||
if distance < nearest_distance:
|
||||
nearest_distance = distance
|
||||
nearest = world_point
|
||||
return nearest
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dd8jw301grvq8
|
||||
544
addons/modular_builder/modular_builder_editor_ui.gd
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
@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 TOOL_NONE := ""
|
||||
const TOOL_FOUNDATION := "foundation"
|
||||
const TOOL_FOUNDATION_SQUARE := "foundation_square"
|
||||
const TOOL_FOUNDATION_TRIANGLE := "foundation_triangle"
|
||||
const TOOL_WALL := "wall"
|
||||
const TOOL_GABLE_WALL := "gable_wall"
|
||||
const TOOL_GABLE_WALL_RIGHT := "gable_wall_right"
|
||||
const TOOL_FLOOR_SQUARE := "floor_square"
|
||||
const TOOL_FLOOR_TRIANGLE := "floor_triangle"
|
||||
const TOOL_ROOF_GABLE := "roof_gable"
|
||||
const TOOL_ROOF_SHED := "roof_shed"
|
||||
const TOOL_ROOF_PYRAMID := "roof_pyramid"
|
||||
const TOOL_STAIRS := "stairs"
|
||||
const TOOL_FENCE := "fence"
|
||||
const TOOL_PARAPET := "parapet"
|
||||
const TOOL_MERGE_VERTICES := "merge_vertices"
|
||||
const TOOL_CREATE_VERTEX := "create_vertex"
|
||||
const TOOL_MOVE_VERTEX := "move_vertex"
|
||||
const TOOL_REMOVE_VERTEX := "remove_vertex"
|
||||
const MERGED_VERTEX_GROUPS_META := &"modular_builder_merged_vertex_groups"
|
||||
const CUSTOM_VERTICES_META := &"modular_builder_custom_vertices"
|
||||
const TOOL_WINDOW := "detail_window"
|
||||
const TOOL_DOOR := "detail_door"
|
||||
const TOOL_HATCH := "detail_hatch"
|
||||
const TOOL_PILLAR := "detail_pillar"
|
||||
const EXPORT_NEW_SCENE := 0
|
||||
const EXPORT_SCENE_COPY := 1
|
||||
const EXPORT_LIBRARY := 2
|
||||
const METERS_TO_GODOT_UNITS := 1.0
|
||||
const DOOR_DEFAULT_SCALE := 0.8
|
||||
const DOOR_DEFAULT_WIDTH_METERS := 0.87 * DOOR_DEFAULT_SCALE
|
||||
const DOOR_DEFAULT_HEIGHT_METERS := 2.15 * DOOR_DEFAULT_SCALE
|
||||
const MARKER_RADIUS := 8.0
|
||||
const MARKER_HIT_RADIUS := 18.0
|
||||
|
||||
var dock: VBoxContainer
|
||||
var _creation_row: HFlowContainer
|
||||
var _export_row: HFlowContainer
|
||||
var _tool_row: HFlowContainer
|
||||
var _vertex_edit_row: HFlowContainer
|
||||
var _marker_scope_check: CheckBox
|
||||
var _simplify_vertices_check: CheckBox
|
||||
var _rise_options_row: HFlowContainer
|
||||
var _linear_options_row: HFlowContainer
|
||||
var _stair_count_label: Label
|
||||
var _stair_width_label: Label
|
||||
var _fence_divisions_label: Label
|
||||
var _detail_options_row: HFlowContainer
|
||||
var _foundation_type_selected := ModularBuilderPiece.PieceType.FOUNDATION_SQUARE
|
||||
var size_spin: SpinBox
|
||||
var rise_spin: SpinBox
|
||||
var stair_count_spin: SpinBox
|
||||
var stair_width_spin: SpinBox
|
||||
var fence_divisions_spin: SpinBox
|
||||
var fence_height_spin: SpinBox
|
||||
var _fence_height_label: Label
|
||||
var _rise_label: Label
|
||||
var _gable_centralize_check: CheckBox
|
||||
var _gable_centralize_default := false
|
||||
var detail_width_spin: SpinBox
|
||||
var detail_height_spin: SpinBox
|
||||
var detail_height_offset_spin: SpinBox
|
||||
var _detail_height_offset_label: Label
|
||||
var _detail_width_label: Label
|
||||
var _detail_height_label: Label
|
||||
var detail_mode_option: OptionButton
|
||||
var detail_style_option: OptionButton
|
||||
var status_label: Label
|
||||
var active_tool := TOOL_NONE
|
||||
var pending_points: Array[Vector3] = []
|
||||
var _redirecting_selection := false
|
||||
var _marker_container: Node3D
|
||||
var _marker_root: ModularBuilderPiece
|
||||
var _marker_point_nodes: Array[Label3D] = []
|
||||
var _side_button_nodes: Array[Label3D] = []
|
||||
var _side_material: StandardMaterial3D
|
||||
var _tool_button_group: ButtonGroup
|
||||
var _tool_buttons: Dictionary = {}
|
||||
var _pillar_centralize_check: CheckBox
|
||||
var _pillar_centralize_default := false
|
||||
var _active_button_key := ""
|
||||
var _last_created_piece: ModularBuilderPiece
|
||||
var _detail_preview: ModularBuilderDetail
|
||||
var _preview_material: StandardMaterial3D
|
||||
var _preview_camera: Camera3D
|
||||
var _preview_screen_position := Vector2.ZERO
|
||||
var _save_module_dialog: FileDialog
|
||||
var _load_module_dialog: FileDialog
|
||||
var _export_scene_dialog: FileDialog
|
||||
var _export_mode_option: OptionButton
|
||||
var _pending_export_mesh: ArrayMesh
|
||||
var _pending_export_name := "BakedBuilding"
|
||||
var _pending_export_basis := Basis.IDENTITY
|
||||
var _selected_window_style := ModularBuilderDetail.WindowStyle.SINGLE_PANE
|
||||
var _selected_door_style := ModularBuilderDetail.DoorStyle.FLAT
|
||||
var _editor_sync_elapsed := 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
set_process(true)
|
||||
_side_material = StandardMaterial3D.new()
|
||||
_side_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_side_material.albedo_color = Color(0.18, 0.95, 0.52)
|
||||
_side_material.no_depth_test = true
|
||||
_side_material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_editor_sync_elapsed += delta
|
||||
if _editor_sync_elapsed < 1.0 / 15.0:
|
||||
return
|
||||
_editor_sync_elapsed = 0.0
|
||||
call("_sync_vertex_markers")
|
||||
_sync_pillar_centralize_control()
|
||||
_sync_gable_centralize_control()
|
||||
|
||||
func _enter_tree() -> void:
|
||||
dock = VBoxContainer.new()
|
||||
dock.name = "Modular Builder"
|
||||
dock.custom_minimum_size = Vector2(0, 110)
|
||||
_creation_row = HFlowContainer.new()
|
||||
dock.add_child(_creation_row)
|
||||
_creation_row.add_child(_label("Size:"))
|
||||
size_spin = SpinBox.new()
|
||||
size_spin.min_value = 0.5
|
||||
size_spin.max_value = 8.0
|
||||
size_spin.step = 0.001
|
||||
size_spin.value = 2.0
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
load_module_button.pressed.connect(Callable(self, "_show_load_module_dialog"))
|
||||
_creation_row.add_child(load_module_button)
|
||||
_export_row = HFlowContainer.new()
|
||||
dock.add_child(_export_row)
|
||||
_export_row.add_child(_label("Export structure as mesh:") )
|
||||
_export_mode_option = OptionButton.new()
|
||||
_export_mode_option.add_item("New scene", EXPORT_NEW_SCENE)
|
||||
_export_mode_option.add_item("Copy in this scene", EXPORT_SCENE_COPY)
|
||||
_export_mode_option.add_item("In ScopedVR-Bibliothek (Objekt-Pipeline)", EXPORT_LIBRARY)
|
||||
_export_mode_option.tooltip_text = "Choose whether to save the combined mesh as a new scene, add a copy to the current scene, or export it into the ScopedVR object library (svmap-ready with trimesh collision)."
|
||||
_export_row.add_child(_export_mode_option)
|
||||
var export_button := Button.new()
|
||||
export_button.text = "Export"
|
||||
export_button.tooltip_text = "Generate one static mesh with materials and UVs from the selected structure."
|
||||
export_button.pressed.connect(Callable(self, "_on_export_pressed"))
|
||||
_export_row.add_child(export_button)
|
||||
_tool_row = HFlowContainer.new()
|
||||
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))
|
||||
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)
|
||||
var marker_options_row := HFlowContainer.new()
|
||||
dock.add_child(marker_options_row)
|
||||
_marker_scope_check = CheckBox.new()
|
||||
_marker_scope_check.text = "Only show points on selected pieces"
|
||||
_marker_scope_check.tooltip_text = "Limit markers to pieces selected in the Scene tree. Selecting the structure container shows all its points."
|
||||
_marker_scope_check.toggled.connect(Callable(self, "_on_marker_scope_toggled"))
|
||||
marker_options_row.add_child(_marker_scope_check)
|
||||
_simplify_vertices_check = CheckBox.new()
|
||||
_simplify_vertices_check.text = "Simplify vertices"
|
||||
_simplify_vertices_check.tooltip_text = "Merge front and back points at the middle of walls and pillars; floors and foundations show their top outline. Vertex editing tools show the exact points."
|
||||
_simplify_vertices_check.toggled.connect(Callable(self, "_on_simplify_vertices_toggled"))
|
||||
marker_options_row.add_child(_simplify_vertices_check)
|
||||
_rise_options_row = HFlowContainer.new()
|
||||
dock.add_child(_rise_options_row)
|
||||
_rise_label = _label("Peak rise:")
|
||||
_rise_options_row.add_child(_rise_label)
|
||||
rise_spin = _detail_spin(1.0, 0.1, 8.0, 0.001)
|
||||
_rise_options_row.add_child(rise_spin)
|
||||
_linear_options_row = HFlowContainer.new()
|
||||
dock.add_child(_linear_options_row)
|
||||
_stair_count_label = _label("Steps:")
|
||||
_linear_options_row.add_child(_stair_count_label)
|
||||
stair_count_spin = SpinBox.new()
|
||||
stair_count_spin.min_value = 1
|
||||
stair_count_spin.max_value = 100
|
||||
stair_count_spin.step = 1
|
||||
stair_count_spin.value = 10
|
||||
stair_count_spin.custom_minimum_size = Vector2(72, 0)
|
||||
_linear_options_row.add_child(stair_count_spin)
|
||||
_stair_width_label = _label("Width:")
|
||||
_linear_options_row.add_child(_stair_width_label)
|
||||
stair_width_spin = _detail_spin(1.0, 0.01, 8.0, 0.001)
|
||||
_linear_options_row.add_child(stair_width_spin)
|
||||
_fence_divisions_label = _label("Intermediate posts:")
|
||||
_linear_options_row.add_child(_fence_divisions_label)
|
||||
fence_divisions_spin = SpinBox.new()
|
||||
fence_divisions_spin.min_value = 0
|
||||
fence_divisions_spin.max_value = 100
|
||||
fence_divisions_spin.step = 1
|
||||
fence_divisions_spin.value = 1
|
||||
fence_divisions_spin.custom_minimum_size = Vector2(72, 0)
|
||||
_linear_options_row.add_child(fence_divisions_spin)
|
||||
_fence_height_label = _label("Height:")
|
||||
_linear_options_row.add_child(_fence_height_label)
|
||||
fence_height_spin = _detail_spin(0.7, 0.1, 8.0, 0.001)
|
||||
_linear_options_row.add_child(fence_height_spin)
|
||||
_linear_options_row.visible = false
|
||||
_gable_centralize_check = CheckBox.new()
|
||||
_gable_centralize_check.text = "Center"
|
||||
_gable_centralize_check.tooltip_text = "Align the gable with the center of the supporting wall thickness."
|
||||
_gable_centralize_check.visible = false
|
||||
_gable_centralize_check.toggled.connect(_on_gable_centralize_toggled)
|
||||
_rise_options_row.add_child(_gable_centralize_check)
|
||||
_rise_options_row.visible = false
|
||||
_detail_options_row = HFlowContainer.new()
|
||||
dock.add_child(_detail_options_row)
|
||||
_detail_width_label = _label("Width:")
|
||||
_detail_options_row.add_child(_detail_width_label)
|
||||
detail_width_spin = _detail_spin(1.0, 0.01, 5.0, 0.001)
|
||||
_detail_options_row.add_child(detail_width_spin)
|
||||
_detail_height_label = _label("Height:")
|
||||
_detail_options_row.add_child(_detail_height_label)
|
||||
detail_height_spin = _detail_spin(1.0, 0.2, 5.0, 0.001)
|
||||
_detail_options_row.add_child(detail_height_spin)
|
||||
_pillar_centralize_check = CheckBox.new()
|
||||
_pillar_centralize_check.text = "Center"
|
||||
_pillar_centralize_check.tooltip_text = "Align the pillar with the nearest axis of the first vertex’s piece while keeping it centered between the points."
|
||||
_pillar_centralize_check.visible = false
|
||||
_pillar_centralize_check.toggled.connect(_on_pillar_centralize_toggled)
|
||||
_detail_options_row.add_child(_pillar_centralize_check)
|
||||
_detail_height_offset_label = _label("Starting height:")
|
||||
_detail_options_row.add_child(_detail_height_offset_label)
|
||||
detail_height_offset_spin = _detail_spin(0.0, -2.0, 2.0, 0.001)
|
||||
detail_height_offset_spin.suffix = " m"
|
||||
_detail_options_row.add_child(detail_height_offset_spin)
|
||||
detail_mode_option = OptionButton.new()
|
||||
detail_mode_option.add_item("Opening", ModularBuilderDetail.DetailMode.OPENING)
|
||||
detail_mode_option.add_item("Opening + object", ModularBuilderDetail.DetailMode.SEPARATE_OBJECT)
|
||||
detail_mode_option.select(0)
|
||||
detail_mode_option.tooltip_text = "Choose an opening alone or an opening with a separately textured object."
|
||||
_detail_options_row.add_child(detail_mode_option)
|
||||
detail_style_option = OptionButton.new()
|
||||
detail_style_option.tooltip_text = "Window or door style."
|
||||
detail_style_option.item_selected.connect(_on_detail_style_selected)
|
||||
_detail_options_row.add_child(detail_style_option)
|
||||
detail_width_spin.value_changed.connect(Callable(self, "_refresh_detail_preview"))
|
||||
detail_height_spin.value_changed.connect(Callable(self, "_refresh_detail_preview"))
|
||||
detail_height_offset_spin.value_changed.connect(Callable(self, "_refresh_detail_preview"))
|
||||
detail_mode_option.item_selected.connect(_on_detail_mode_selected)
|
||||
_detail_options_row.visible = false
|
||||
_preview_material = StandardMaterial3D.new()
|
||||
_preview_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_preview_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
_preview_material.albedo_color = Color(1.0, 0.35, 0.72, 0.48)
|
||||
_save_module_dialog = FileDialog.new()
|
||||
_save_module_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
|
||||
_save_module_dialog.access = FileDialog.ACCESS_RESOURCES
|
||||
_save_module_dialog.add_filter("*.tscn", "Godot Scene")
|
||||
_save_module_dialog.file_selected.connect(Callable(self, "_save_module_to_path"))
|
||||
add_child(_save_module_dialog)
|
||||
_load_module_dialog = FileDialog.new()
|
||||
_load_module_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
_load_module_dialog.access = FileDialog.ACCESS_RESOURCES
|
||||
_load_module_dialog.add_filter("*.tscn", "Godot Scene")
|
||||
_load_module_dialog.file_selected.connect(Callable(self, "_load_module_from_path"))
|
||||
add_child(_load_module_dialog)
|
||||
_export_scene_dialog = FileDialog.new()
|
||||
_export_scene_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
|
||||
_export_scene_dialog.access = FileDialog.ACCESS_RESOURCES
|
||||
_export_scene_dialog.add_filter("*.tscn", "Godot Scene")
|
||||
_export_scene_dialog.file_selected.connect(Callable(self, "_export_baked_scene_to_path"))
|
||||
add_child(_export_scene_dialog)
|
||||
status_label = _label("Select a tool to show the vertices.")
|
||||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
dock.add_child(status_label)
|
||||
add_control_to_bottom_panel(dock, "Modular Builder")
|
||||
set_input_event_forwarding_always_enabled()
|
||||
get_editor_interface().get_selection().selection_changed.connect(Callable(self, "_on_editor_selection_changed"))
|
||||
var inspector := get_editor_interface().get_inspector()
|
||||
if not inspector.property_edited.is_connected(_on_inspector_property_edited):
|
||||
inspector.property_edited.connect(_on_inspector_property_edited)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
set_process(false)
|
||||
var editor_selection := get_editor_interface().get_selection()
|
||||
if editor_selection.selection_changed.is_connected(Callable(self, "_on_editor_selection_changed")):
|
||||
editor_selection.selection_changed.disconnect(Callable(self, "_on_editor_selection_changed"))
|
||||
var inspector := get_editor_interface().get_inspector()
|
||||
if inspector.property_edited.is_connected(_on_inspector_property_edited):
|
||||
inspector.property_edited.disconnect(_on_inspector_property_edited)
|
||||
if dock:
|
||||
remove_control_from_bottom_panel(dock)
|
||||
dock.queue_free()
|
||||
call("_clear_marker_container")
|
||||
call("_clear_detail_preview")
|
||||
update_overlays()
|
||||
|
||||
func _handles(object: Object) -> bool:
|
||||
return object is ModularBuilderPiece
|
||||
|
||||
func _label(text: String) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = text
|
||||
return label
|
||||
|
||||
func _detail_spin(value: float, minimum: float, maximum: float, step: float) -> SpinBox:
|
||||
var spin := SpinBox.new()
|
||||
spin.min_value = minimum
|
||||
spin.max_value = maximum
|
||||
spin.step = step
|
||||
spin.value = value
|
||||
spin.suffix = " m"
|
||||
spin.custom_minimum_size = Vector2(84, 0)
|
||||
return spin
|
||||
|
||||
func _make_icon_button(icon_path: String, tooltip: String, color: Color) -> Button:
|
||||
var button := Button.new()
|
||||
button.icon = load(icon_path) as Texture2D
|
||||
button.tooltip_text = tooltip
|
||||
button.custom_minimum_size = Vector2(48, 48)
|
||||
button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
button.expand_icon = true
|
||||
button.add_theme_constant_override("icon_max_width", 30)
|
||||
var normal := StyleBoxFlat.new()
|
||||
normal.bg_color = color
|
||||
normal.set_corner_radius_all(4)
|
||||
normal.set_border_width_all(1)
|
||||
normal.border_color = Color(0.02, 0.02, 0.02)
|
||||
button.add_theme_stylebox_override("normal", normal)
|
||||
var hover := normal.duplicate() as StyleBoxFlat
|
||||
hover.bg_color = color.lightened(0.16)
|
||||
button.add_theme_stylebox_override("hover", hover)
|
||||
var pressed := normal.duplicate() as StyleBoxFlat
|
||||
pressed.bg_color = color.lightened(0.28)
|
||||
pressed.border_width_bottom = 3
|
||||
button.add_theme_stylebox_override("pressed", pressed)
|
||||
return button
|
||||
|
||||
func _add_tool_button(icon_path: String, tooltip: String, tool: String, color: Color, target_row: Container = null) -> void:
|
||||
var button := _make_icon_button(icon_path, tooltip, color)
|
||||
button.toggle_mode = true
|
||||
button.button_group = _tool_button_group
|
||||
button.toggled.connect(_on_tool_button_toggled.bind(tool))
|
||||
(_tool_row if target_row == null else target_row).add_child(button)
|
||||
_tool_buttons[tool] = button
|
||||
|
||||
func _on_tool_button_toggled(pressed: bool, tool: String) -> void:
|
||||
var is_foundation := tool == TOOL_FOUNDATION_SQUARE or tool == TOOL_FOUNDATION_TRIANGLE
|
||||
if pressed:
|
||||
_active_button_key = tool
|
||||
if is_foundation:
|
||||
_foundation_type_selected = ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE if tool == TOOL_FOUNDATION_TRIANGLE else ModularBuilderPiece.PieceType.FOUNDATION_SQUARE
|
||||
call("_set_tool", TOOL_FOUNDATION)
|
||||
else:
|
||||
call("_set_tool", tool)
|
||||
_update_detail_options(tool)
|
||||
else:
|
||||
call_deferred("_finish_tool_button_toggle_off", tool)
|
||||
|
||||
func _finish_tool_button_toggle_off(tool: String) -> void:
|
||||
var button := _tool_buttons.get(tool) as Button
|
||||
if button != null and not button.button_pressed and _active_button_key == tool:
|
||||
call("_set_tool", TOOL_NONE)
|
||||
|
||||
func _update_detail_options(tool: String) -> void:
|
||||
var is_detail := _is_detail_tool(tool)
|
||||
var is_fence := tool == TOOL_FENCE or tool == TOOL_PARAPET
|
||||
_detail_options_row.visible = is_detail
|
||||
detail_width_spin.visible = is_detail and not is_fence
|
||||
_detail_width_label.visible = is_detail and not is_fence
|
||||
detail_height_spin.visible = is_detail and not is_fence
|
||||
_detail_height_label.visible = is_detail and not is_fence
|
||||
detail_height_offset_spin.visible = tool != TOOL_HATCH and tool != TOOL_PILLAR and not is_fence
|
||||
_detail_height_offset_label.visible = detail_height_offset_spin.visible
|
||||
detail_mode_option.visible = tool != TOOL_PILLAR and not is_fence
|
||||
_detail_width_label.text = "Side:" if tool == TOOL_PILLAR else "Width:"
|
||||
_detail_height_label.text = "Length:" if tool == TOOL_PILLAR else "Height:"
|
||||
_linear_options_row.visible = tool == TOOL_STAIRS or is_fence
|
||||
_stair_count_label.visible = tool == TOOL_STAIRS
|
||||
stair_count_spin.visible = tool == TOOL_STAIRS
|
||||
_stair_width_label.visible = tool == TOOL_STAIRS
|
||||
stair_width_spin.visible = tool == TOOL_STAIRS
|
||||
_fence_divisions_label.visible = is_fence
|
||||
fence_divisions_spin.visible = is_fence
|
||||
_fence_height_label.visible = is_fence
|
||||
fence_height_spin.visible = is_fence
|
||||
if not is_detail:
|
||||
return
|
||||
var width := 1.0
|
||||
var height := 1.0
|
||||
var bottom_offset := 0.5
|
||||
if tool == TOOL_PILLAR:
|
||||
width = 0.2
|
||||
height = 2.0
|
||||
bottom_offset = 0.0
|
||||
elif tool == TOOL_DOOR:
|
||||
width = DOOR_DEFAULT_WIDTH_METERS
|
||||
height = DOOR_DEFAULT_HEIGHT_METERS
|
||||
bottom_offset = 0.0
|
||||
elif tool == TOOL_HATCH:
|
||||
bottom_offset = 0.0
|
||||
detail_width_spin.set_value_no_signal(width)
|
||||
detail_height_spin.set_value_no_signal(height)
|
||||
detail_height_offset_spin.set_value_no_signal(bottom_offset)
|
||||
detail_style_option.clear()
|
||||
detail_style_option.visible = tool == TOOL_WINDOW or tool == TOOL_DOOR
|
||||
if tool == TOOL_WINDOW:
|
||||
detail_style_option.add_item("Single pane", ModularBuilderDetail.WindowStyle.SINGLE_PANE)
|
||||
detail_style_option.add_item("Cross pane", ModularBuilderDetail.WindowStyle.CROSS_PANE)
|
||||
detail_style_option.select(_selected_window_style)
|
||||
elif tool == TOOL_DOOR:
|
||||
detail_style_option.add_item("Flat", ModularBuilderDetail.DoorStyle.FLAT)
|
||||
detail_style_option.add_item("Paneled", ModularBuilderDetail.DoorStyle.PANELED)
|
||||
detail_style_option.select(_selected_door_style)
|
||||
call("_refresh_detail_preview")
|
||||
|
||||
func _on_detail_style_selected(index: int) -> void:
|
||||
if active_tool == TOOL_WINDOW:
|
||||
_selected_window_style = index
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.window_style = index
|
||||
elif active_tool == TOOL_DOOR:
|
||||
_selected_door_style = index
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.door_style = index
|
||||
call("_refresh_detail_preview")
|
||||
|
||||
func _on_detail_mode_selected(_index: int) -> void:
|
||||
call("_refresh_detail_preview")
|
||||
|
||||
func _on_pillar_centralize_toggled(enabled: bool) -> void:
|
||||
_pillar_centralize_default = enabled
|
||||
var selected_pillar := _get_selected_pillar_detail()
|
||||
if selected_pillar != null:
|
||||
selected_pillar.center_align = enabled
|
||||
if is_instance_valid(_detail_preview) and _detail_preview.detail_type == ModularBuilderDetail.DetailType.PILLAR:
|
||||
_detail_preview.center_align = enabled
|
||||
call("_refresh_detail_preview")
|
||||
|
||||
func _on_gable_centralize_toggled(enabled: bool) -> void:
|
||||
_gable_centralize_default = enabled
|
||||
var selected := _get_selected_gable_wall()
|
||||
if selected != null:
|
||||
selected.center_align = enabled
|
||||
|
||||
func _sync_gable_centralize_control() -> void:
|
||||
if _gable_centralize_check == null:
|
||||
return
|
||||
var selected := _get_selected_gable_wall()
|
||||
var show_option := active_tool == TOOL_WALL or active_tool == TOOL_GABLE_WALL or active_tool == TOOL_GABLE_WALL_RIGHT or selected != null
|
||||
var show_rise := active_tool == TOOL_GABLE_WALL or active_tool == TOOL_GABLE_WALL_RIGHT or active_tool == TOOL_ROOF_GABLE or active_tool == TOOL_ROOF_PYRAMID
|
||||
_rise_options_row.visible = show_rise or selected != null
|
||||
_rise_label.visible = show_rise
|
||||
rise_spin.visible = show_rise
|
||||
_gable_centralize_check.visible = show_option
|
||||
_gable_centralize_check.text = "Center walls" if active_tool == TOOL_WALL or (selected != null and selected.piece_type == ModularBuilderPiece.PieceType.WALL) else "Center"
|
||||
_gable_centralize_check.tooltip_text = "Align wall endpoints with the centerlines of supporting walls." if active_tool == TOOL_WALL or (selected != null and selected.piece_type == ModularBuilderPiece.PieceType.WALL) else "Align the gable with the center of the supporting wall thickness."
|
||||
if selected != null:
|
||||
_gable_centralize_default = selected.center_align
|
||||
_gable_centralize_check.set_pressed_no_signal(selected.center_align)
|
||||
else:
|
||||
_gable_centralize_check.set_pressed_no_signal(_gable_centralize_default)
|
||||
|
||||
func _get_selected_gable_wall() -> ModularBuilderPiece:
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected.size() == 1 and selected[0] is ModularBuilderPiece and (selected[0].piece_type == ModularBuilderPiece.PieceType.WALL or selected[0].piece_type == ModularBuilderPiece.PieceType.GABLE_WALL or selected[0].piece_type == ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT):
|
||||
return selected[0] as ModularBuilderPiece
|
||||
return null
|
||||
|
||||
func _sync_pillar_centralize_control() -> void:
|
||||
if _pillar_centralize_check == null:
|
||||
return
|
||||
var selected_pillar := _get_selected_pillar_detail()
|
||||
var show_pillar_options := active_tool == TOOL_PILLAR or selected_pillar != null
|
||||
_detail_options_row.visible = _is_detail_tool(active_tool) or show_pillar_options
|
||||
_pillar_centralize_check.visible = show_pillar_options
|
||||
if selected_pillar != null and active_tool != TOOL_PILLAR and not _is_detail_tool(active_tool):
|
||||
_detail_width_label.visible = false
|
||||
detail_width_spin.visible = false
|
||||
_detail_height_label.visible = false
|
||||
detail_height_spin.visible = false
|
||||
_detail_height_offset_label.visible = false
|
||||
detail_height_offset_spin.visible = false
|
||||
detail_mode_option.visible = false
|
||||
detail_style_option.visible = false
|
||||
elif active_tool == TOOL_PILLAR:
|
||||
_detail_width_label.visible = true
|
||||
detail_width_spin.visible = true
|
||||
_detail_height_label.visible = true
|
||||
detail_height_spin.visible = true
|
||||
if selected_pillar != null:
|
||||
_pillar_centralize_default = selected_pillar.center_align
|
||||
_pillar_centralize_check.set_pressed_no_signal(selected_pillar.center_align)
|
||||
else:
|
||||
_pillar_centralize_check.set_pressed_no_signal(_pillar_centralize_default)
|
||||
|
||||
func _get_selected_pillar_detail() -> ModularBuilderDetail:
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected.size() != 1 or not selected[0] is ModularBuilderDetail:
|
||||
return null
|
||||
var detail := selected[0] as ModularBuilderDetail
|
||||
return detail if detail.detail_type == ModularBuilderDetail.DetailType.PILLAR else null
|
||||
|
||||
func _on_inspector_property_edited(property: String) -> void:
|
||||
if property == "center_align":
|
||||
_sync_pillar_centralize_control()
|
||||
|
||||
func _is_detail_tool(tool: String) -> bool:
|
||||
return tool == TOOL_WINDOW or tool == TOOL_DOOR or tool == TOOL_HATCH or tool == TOOL_PILLAR or tool == TOOL_FENCE or tool == TOOL_PARAPET
|
||||
1
addons/modular_builder/modular_builder_editor_ui.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://duvk2kap6rjef
|
||||
727
addons/modular_builder/modular_builder_editor_vertices.gd
Normal file
|
|
@ -0,0 +1,727 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder/modular_builder_editor_details.gd"
|
||||
|
||||
const MESH_PICKER = preload("res://addons/modular_builder/modular_mesh_picker.gd")
|
||||
|
||||
func _sync_vertex_markers() -> void:
|
||||
var root := _get_selected_piece()
|
||||
var vertex_points: Array[Vector3] = []
|
||||
var side_points: Array[Dictionary] = []
|
||||
if root != null:
|
||||
if active_tool == TOOL_WALL or active_tool == TOOL_GABLE_WALL or active_tool == TOOL_GABLE_WALL_RIGHT or active_tool == TOOL_FLOOR_SQUARE or active_tool == TOOL_FLOOR_TRIANGLE or active_tool == TOOL_ROOF_GABLE or active_tool == TOOL_ROOF_SHED or active_tool == TOOL_ROOF_PYRAMID or active_tool == TOOL_STAIRS or active_tool == TOOL_FENCE or active_tool == TOOL_PILLAR or active_tool == TOOL_MERGE_VERTICES or active_tool == TOOL_CREATE_VERTEX or active_tool == TOOL_MOVE_VERTEX or active_tool == TOOL_REMOVE_VERTEX:
|
||||
vertex_points = _get_tool_vertices(root)
|
||||
elif active_tool == TOOL_FOUNDATION:
|
||||
side_points = _get_side_button_points(root)
|
||||
if root != _marker_root or (root == null and _marker_container != null) or vertex_points.size() != _marker_point_nodes.size() or side_points.size() != _side_button_nodes.size():
|
||||
_clear_marker_container()
|
||||
_marker_root = root
|
||||
if root == null:
|
||||
return
|
||||
_marker_container = Node3D.new()
|
||||
_marker_container.name = "ModularBuilderMarkers"
|
||||
root.add_child(_marker_container)
|
||||
for _point in vertex_points:
|
||||
var marker := Label3D.new()
|
||||
marker.text = "●"
|
||||
marker.font_size = 56
|
||||
marker.pixel_size = 0.004
|
||||
marker.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
marker.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
marker.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
marker.no_depth_test = true
|
||||
marker.modulate = Color(0.05, 0.58, 1.0)
|
||||
marker.outline_size = 8
|
||||
marker.outline_modulate = Color.BLACK
|
||||
_marker_container.add_child(marker)
|
||||
_marker_point_nodes.append(marker)
|
||||
for _button in side_points:
|
||||
var label := Label3D.new()
|
||||
label.text = "+"
|
||||
label.font_size = 56
|
||||
label.outline_size = 8
|
||||
label.outline_modulate = Color.BLACK
|
||||
label.pixel_size = 0.01
|
||||
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
label.no_depth_test = true
|
||||
label.modulate = Color(0.2, 1.0, 0.55)
|
||||
_marker_container.add_child(label)
|
||||
_side_button_nodes.append(label)
|
||||
for i in range(vertex_points.size()):
|
||||
_marker_point_nodes[i].position = root.to_local(vertex_points[i])
|
||||
var is_pending := false
|
||||
for pending_point in pending_points:
|
||||
if pending_point.distance_to(vertex_points[i]) < 0.025:
|
||||
is_pending = true
|
||||
break
|
||||
_marker_point_nodes[i].modulate = Color(1.0, 0.88, 0.08) if is_pending else Color(0.05, 0.58, 1.0)
|
||||
for i in range(side_points.size()):
|
||||
_side_button_nodes[i].position = root.to_local(side_points[i].position)
|
||||
if _marker_container:
|
||||
_marker_container.visible = active_tool != TOOL_NONE or not side_points.is_empty()
|
||||
|
||||
func _clear_marker_container() -> void:
|
||||
if is_instance_valid(_marker_container):
|
||||
_marker_container.queue_free()
|
||||
_marker_container = null
|
||||
_marker_root = null
|
||||
_marker_point_nodes.clear()
|
||||
_side_button_nodes.clear()
|
||||
|
||||
func _get_side_button_points(root: ModularBuilderPiece) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for foundation in _get_foundations(root):
|
||||
var top_y := foundation.thickness * 0.5 if foundation.centered_origin else foundation.thickness
|
||||
var y := top_y + 0.22
|
||||
var points := _foundation_polygon(foundation)
|
||||
var centroid := Vector2.ZERO
|
||||
for point in points:
|
||||
centroid += point
|
||||
centroid /= points.size()
|
||||
for i in range(points.size()):
|
||||
var a: Vector2 = points[i]
|
||||
var b: Vector2 = points[(i + 1) % points.size()]
|
||||
if _foundation_edge_is_occupied(root, foundation, a, b):
|
||||
continue
|
||||
var midpoint := (a + b) * 0.5
|
||||
var outward := (midpoint - centroid).normalized()
|
||||
var edge_length := a.distance_to(b)
|
||||
var neighbor_polygon: Array[Vector2] = []
|
||||
if _foundation_type_selected == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE:
|
||||
var square_side := minf(foundation.unit_size, edge_length)
|
||||
var edge_direction := (b - a).normalized()
|
||||
var square_a := midpoint - edge_direction * square_side * 0.5
|
||||
var square_b := midpoint + edge_direction * square_side * 0.5
|
||||
neighbor_polygon = [square_a, square_b, square_b + outward * square_side, square_a + outward * square_side]
|
||||
elif points.size() == 3:
|
||||
for source_point in points:
|
||||
neighbor_polygon.append(_reflect_point_across_edge(source_point, a, b))
|
||||
else:
|
||||
neighbor_polygon = [a, b, a + outward * edge_length]
|
||||
var custom_points := PackedVector3Array()
|
||||
for neighbor_point in neighbor_polygon:
|
||||
var base_y := -foundation.thickness * 0.5 if foundation.centered_origin else 0.0
|
||||
var foundation_local := Vector3(neighbor_point.x, base_y, neighbor_point.y)
|
||||
custom_points.append(root.to_local(foundation.to_global(foundation_local)))
|
||||
result.append({
|
||||
"position": foundation.to_global(Vector3(midpoint.x + outward.x * 0.55, y, midpoint.y + outward.y * 0.55)),
|
||||
"custom_points": custom_points,
|
||||
"foundation": foundation
|
||||
})
|
||||
return result
|
||||
|
||||
func _foundation_edge_is_occupied(root: ModularBuilderPiece, foundation: ModularBuilderPiece, edge_a: Vector2, edge_b: Vector2) -> bool:
|
||||
var foundation_base_y := -foundation.thickness * 0.5 if foundation.centered_origin else 0.0
|
||||
var edge_a_3d := root.to_local(foundation.to_global(Vector3(edge_a.x, foundation_base_y, edge_a.y)))
|
||||
var edge_b_3d := root.to_local(foundation.to_global(Vector3(edge_b.x, foundation_base_y, edge_b.y)))
|
||||
var target_a := Vector2(edge_a_3d.x, edge_a_3d.z)
|
||||
var target_b := Vector2(edge_b_3d.x, edge_b_3d.z)
|
||||
for other in _get_foundations(root):
|
||||
if other == foundation:
|
||||
continue
|
||||
var other_polygon := _foundation_polygon(other)
|
||||
for i in range(other_polygon.size()):
|
||||
var a := other_polygon[i]
|
||||
var b := other_polygon[(i + 1) % other_polygon.size()]
|
||||
var other_base_y := -other.thickness * 0.5 if other.centered_origin else 0.0
|
||||
var a_in_root := root.to_local(other.to_global(Vector3(a.x, other_base_y, a.y)))
|
||||
var b_in_root := root.to_local(other.to_global(Vector3(b.x, other_base_y, b.y)))
|
||||
var other_a := Vector2(a_in_root.x, a_in_root.z)
|
||||
var other_b := Vector2(b_in_root.x, b_in_root.z)
|
||||
if absf(a_in_root.y - edge_a_3d.y) < 0.02 and absf(b_in_root.y - edge_b_3d.y) < 0.02 and _segments_overlap(target_a, target_b, other_a, other_b):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _get_foundations(root: ModularBuilderPiece) -> Array[ModularBuilderPiece]:
|
||||
var result: Array[ModularBuilderPiece] = []
|
||||
if root.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or root.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
result.append(root)
|
||||
for child in root.get_children():
|
||||
_collect_foundations(child, result)
|
||||
return result
|
||||
|
||||
func _collect_foundations(node: Node, result: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and (node.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or node.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE):
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_foundations(child, result)
|
||||
|
||||
func _reflect_point_across_edge(point: Vector2, a: Vector2, b: Vector2) -> Vector2:
|
||||
var edge := b - a
|
||||
var projection := a + edge * ((point - a).dot(edge) / edge.length_squared())
|
||||
return projection * 2.0 - point
|
||||
|
||||
func _nearest_side_button(camera: Camera3D, screen_position: Vector2) -> int:
|
||||
var root := _get_selected_piece()
|
||||
if root == null:
|
||||
return -1
|
||||
var buttons := _get_side_button_points(root)
|
||||
var closest := MARKER_HIT_RADIUS + 5.0
|
||||
var result := -1
|
||||
for i in range(buttons.size()):
|
||||
var point: Vector3 = buttons[i].position
|
||||
if camera.is_position_behind(point):
|
||||
continue
|
||||
var distance := camera.unproject_position(point).distance_to(screen_position)
|
||||
if distance < closest:
|
||||
closest = distance
|
||||
result = i
|
||||
return result
|
||||
|
||||
func _add_foundation_at_side(root: ModularBuilderPiece, side: int) -> void:
|
||||
var buttons := _get_side_button_points(root)
|
||||
if side < 0 or side >= buttons.size():
|
||||
return
|
||||
var scene_root := get_editor_interface().get_edited_scene_root() as Node3D
|
||||
var piece := Node3D.new()
|
||||
piece.name = "Foundation"
|
||||
piece.set_script(PIECE_SCRIPT)
|
||||
var source_foundation: ModularBuilderPiece = buttons[side].foundation
|
||||
piece.set("unit_size", source_foundation.unit_size)
|
||||
piece.set("thickness", source_foundation.thickness)
|
||||
piece.set("centered_origin", true)
|
||||
piece.set("piece_type", _foundation_type_selected)
|
||||
var points_in_root: PackedVector3Array = buttons[side].custom_points
|
||||
var pivot := _packed_points_bounds_center(points_in_root)
|
||||
pivot.y += source_foundation.thickness * 0.5
|
||||
var custom_points := PackedVector3Array()
|
||||
for point in points_in_root:
|
||||
custom_points.append(point - pivot)
|
||||
piece.position = pivot
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Add foundation to selected side")
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "custom_points", custom_points)
|
||||
undo.add_do_property(piece, "owner", scene_root)
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
status_label.text = "Foundation added to structure."
|
||||
|
||||
func _packed_points_bounds_center(points: PackedVector3Array) -> Vector3:
|
||||
if points.is_empty():
|
||||
return Vector3.ZERO
|
||||
var minimum := points[0]
|
||||
var maximum := points[0]
|
||||
for point in points:
|
||||
minimum = minimum.min(point)
|
||||
maximum = maximum.max(point)
|
||||
return (minimum + maximum) * 0.5
|
||||
|
||||
func _get_tool_vertices(root: ModularBuilderPiece) -> Array[Vector3]:
|
||||
var result: Array[Vector3] = []
|
||||
if active_tool == TOOL_MERGE_VERTICES:
|
||||
_collect_all_connection_vertices(root, result)
|
||||
elif active_tool == TOOL_CREATE_VERTEX or active_tool == TOOL_MOVE_VERTEX or active_tool == TOOL_REMOVE_VERTEX:
|
||||
_collect_all_connection_vertices(root, result)
|
||||
elif active_tool == TOOL_STAIRS:
|
||||
_collect_stair_vertices(root, result)
|
||||
elif active_tool == TOOL_FENCE or active_tool == TOOL_PARAPET:
|
||||
_collect_fence_vertices(root, result)
|
||||
elif active_tool == TOOL_PILLAR:
|
||||
var surfaces: Array[ModularBuilderPiece] = []
|
||||
_collect_surface_pieces(root, surfaces)
|
||||
for piece in surfaces:
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.BUILDING:
|
||||
continue
|
||||
var polygon := _piece_polygon_xz(piece) if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR else _foundation_polygon(piece)
|
||||
var top_y := piece.custom_points[0].y if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR and not piece.custom_points.is_empty() else (piece.thickness * 0.5 if piece.centered_origin else piece.thickness)
|
||||
var bottom_y := top_y - piece.thickness
|
||||
for point in polygon:
|
||||
_add_unique_vertex(result, piece.to_global(Vector3(point.x, top_y, point.y)))
|
||||
_add_unique_vertex(result, piece.to_global(Vector3(point.x, bottom_y, point.y)))
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
for offset in [Vector3.ZERO, Vector3.UP * wall.unit_size]:
|
||||
_add_unique_vertex(result, wall.to_global(endpoint + offset + side))
|
||||
_add_unique_vertex(result, wall.to_global(endpoint + offset - side))
|
||||
_add_gable_wall_vertices(root, result)
|
||||
elif active_tool == TOOL_WALL:
|
||||
for foundation in _get_foundations(root):
|
||||
var top_y := foundation.thickness * 0.5 if foundation.centered_origin else foundation.thickness
|
||||
for point in _foundation_polygon(foundation):
|
||||
_add_unique_vertex(result, foundation.to_global(Vector3(point.x, top_y, point.y)))
|
||||
var surfaces: Array[ModularBuilderPiece] = []
|
||||
_collect_surface_pieces(root, surfaces)
|
||||
for surface in surfaces:
|
||||
if surface.piece_type != ModularBuilderPiece.PieceType.FLOOR:
|
||||
continue
|
||||
for point in surface.custom_points:
|
||||
_add_unique_vertex(result, surface.to_global(point))
|
||||
var existing_walls: Array[Node] = []
|
||||
_collect_walls(root, existing_walls)
|
||||
for wall_node in existing_walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x) * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
var top := endpoint + Vector3.UP * wall.unit_size
|
||||
_add_unique_vertex(result, wall.to_global(top + side))
|
||||
_add_unique_vertex(result, wall.to_global(top - side))
|
||||
_add_gable_wall_vertices(root, result)
|
||||
else:
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * float(wall.get("thickness")) * 0.5
|
||||
for endpoint in endpoints:
|
||||
if active_tool == TOOL_ROOF_GABLE or active_tool == TOOL_ROOF_SHED:
|
||||
_add_unique_vertex(result, wall.to_global(endpoint + side))
|
||||
_add_unique_vertex(result, wall.to_global(endpoint - side))
|
||||
var top := endpoint + Vector3.UP * float(wall.get("unit_size"))
|
||||
_add_unique_vertex(result, wall.to_global(top + side))
|
||||
_add_unique_vertex(result, wall.to_global(top - side))
|
||||
_add_gable_wall_vertices(root, result)
|
||||
_collect_detail_connection_vertices(root, result)
|
||||
for manual_vertex in _get_manual_vertices(root):
|
||||
_add_unique_vertex(result, manual_vertex)
|
||||
_collapse_merged_vertex_groups(root, result)
|
||||
if _marker_scope_check != null and _marker_scope_check.button_pressed:
|
||||
var scope_pieces := _get_marker_scope_pieces(root)
|
||||
var scope_vertices: Array[Vector3] = []
|
||||
for piece in scope_pieces:
|
||||
_collect_piece_geometry_vertices(piece, scope_vertices)
|
||||
for manual_vertex in _get_manual_vertices(piece):
|
||||
_add_unique_vertex(scope_vertices, manual_vertex)
|
||||
for selected_node in get_editor_interface().get_selection().get_selected_nodes():
|
||||
if selected_node is ModularBuilderDetail:
|
||||
_collect_detail_connection_vertices(selected_node, scope_vertices)
|
||||
_collapse_merged_vertex_groups(root, scope_vertices)
|
||||
var filtered: Array[Vector3] = []
|
||||
for vertex in result:
|
||||
for scope_vertex in scope_vertices:
|
||||
if vertex.distance_to(scope_vertex) < 0.025:
|
||||
_add_unique_vertex(filtered, vertex)
|
||||
break
|
||||
result = filtered
|
||||
if _simplify_vertices_check != null and _simplify_vertices_check.button_pressed and active_tool != TOOL_MERGE_VERTICES and active_tool != TOOL_CREATE_VERTEX and active_tool != TOOL_MOVE_VERTEX and active_tool != TOOL_REMOVE_VERTEX:
|
||||
result = _simplify_tool_vertices(root, result)
|
||||
return result
|
||||
|
||||
func _simplify_tool_vertices(root: ModularBuilderPiece, vertices: Array[Vector3]) -> Array[Vector3]:
|
||||
var replacements := {}
|
||||
var pieces: Array[ModularBuilderPiece] = []
|
||||
_collect_all_modular_pieces(root, pieces)
|
||||
for piece in pieces:
|
||||
if piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE and piece.piece_type != ModularBuilderPiece.PieceType.FLOOR and piece.piece_type != ModularBuilderPiece.PieceType.WALL and piece.piece_type != ModularBuilderPiece.PieceType.GABLE_WALL and piece.piece_type != ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT:
|
||||
continue
|
||||
var piece_vertices: Array[Vector3] = []
|
||||
_collect_piece_geometry_vertices(piece, piece_vertices)
|
||||
for original in piece_vertices:
|
||||
var key := _vertex_position_key(original)
|
||||
if not replacements.has(key):
|
||||
replacements[key] = VERTEX_SIMPLIFIER.piece_vertex(piece, original)
|
||||
_add_pillar_simplification_candidates(root, replacements)
|
||||
var result: Array[Vector3] = []
|
||||
for original in vertices:
|
||||
_add_unique_vertex(result, replacements.get(_vertex_position_key(original), original))
|
||||
return result
|
||||
|
||||
func _add_pillar_simplification_candidates(node: Node, replacements: Dictionary) -> void:
|
||||
if node is ModularBuilderDetail:
|
||||
var detail := node as ModularBuilderDetail
|
||||
if detail.detail_type == ModularBuilderDetail.DetailType.PILLAR:
|
||||
var half := detail.width * 0.5
|
||||
for x in [-half, half]:
|
||||
for y in [-detail.height * 0.5, detail.height * 0.5]:
|
||||
for z in [-half, half]:
|
||||
var original := detail.to_global(Vector3(x, y, z))
|
||||
var key := _vertex_position_key(original)
|
||||
if not replacements.has(key):
|
||||
replacements[key] = VERTEX_SIMPLIFIER.pillar_vertex(detail, original)
|
||||
for child in node.get_children():
|
||||
_add_pillar_simplification_candidates(child, replacements)
|
||||
|
||||
func _vertex_position_key(point: Vector3) -> Vector3i:
|
||||
return Vector3i(roundi(point.x * 1000.0), roundi(point.y * 1000.0), roundi(point.z * 1000.0))
|
||||
|
||||
func _collect_stair_vertices(root: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
var surfaces: Array[ModularBuilderPiece] = []
|
||||
_collect_surface_pieces(root, surfaces)
|
||||
for surface in surfaces:
|
||||
var polygon := _piece_polygon_xz(surface) if surface.piece_type == ModularBuilderPiece.PieceType.FLOOR else _foundation_polygon(surface)
|
||||
var y := surface.thickness * 0.5 if surface.centered_origin else surface.thickness
|
||||
for point in polygon:
|
||||
_add_unique_vertex(vertices, surface.to_global(Vector3(point.x, y, point.y)))
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x) * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
var top := endpoint + Vector3.UP * wall.unit_size
|
||||
_add_unique_vertex(vertices, wall.to_global(top + side))
|
||||
_add_unique_vertex(vertices, wall.to_global(top - side))
|
||||
_add_gable_wall_vertices(root, vertices)
|
||||
|
||||
func _collect_fence_vertices(root: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
var surfaces: Array[ModularBuilderPiece] = []
|
||||
_collect_surface_pieces(root, surfaces)
|
||||
for surface in surfaces:
|
||||
var polygon := _piece_polygon_xz(surface) if surface.piece_type == ModularBuilderPiece.PieceType.FLOOR else _foundation_polygon(surface)
|
||||
var top_y := surface.thickness * 0.5 if surface.centered_origin else surface.thickness
|
||||
for point in polygon:
|
||||
_add_unique_vertex(vertices, surface.to_global(Vector3(point.x, top_y, point.y)))
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x) * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
for height in [0.0, wall.unit_size]:
|
||||
_add_unique_vertex(vertices, wall.to_global(endpoint + Vector3.UP * height + side))
|
||||
_add_unique_vertex(vertices, wall.to_global(endpoint + Vector3.UP * height - side))
|
||||
_add_gable_wall_vertices(root, vertices)
|
||||
|
||||
func _get_marker_scope_pieces(root: ModularBuilderPiece) -> Array[ModularBuilderPiece]:
|
||||
var result: Array[ModularBuilderPiece] = []
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
for selected_node in selected:
|
||||
var piece := selected_node as ModularBuilderPiece
|
||||
if piece == null and selected_node is ModularBuilderDetail:
|
||||
var parent := selected_node.get_parent()
|
||||
while parent != null and not parent is ModularBuilderPiece:
|
||||
parent = parent.get_parent()
|
||||
piece = parent as ModularBuilderPiece
|
||||
if piece == root:
|
||||
_collect_all_modular_pieces(root, result)
|
||||
return result
|
||||
if piece != null and _is_descendant_of_piece(piece, root):
|
||||
if not result.has(piece):
|
||||
result.append(piece)
|
||||
return result
|
||||
|
||||
func _collect_all_modular_pieces(node: Node, result: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and not result.has(node):
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_all_modular_pieces(child, result)
|
||||
|
||||
func _is_descendant_of_piece(node: Node, ancestor: Node) -> bool:
|
||||
var current := node.get_parent()
|
||||
while current != null:
|
||||
if current == ancestor:
|
||||
return true
|
||||
current = current.get_parent()
|
||||
return false
|
||||
|
||||
func _collect_piece_geometry_vertices(piece: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or piece.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
var top_y := piece.thickness * 0.5 if piece.centered_origin else piece.thickness
|
||||
var bottom_y := -piece.thickness * 0.5 if piece.centered_origin else 0.0
|
||||
for point in _foundation_polygon(piece):
|
||||
_add_unique_vertex(vertices, piece.to_global(Vector3(point.x, top_y, point.y)))
|
||||
_add_unique_vertex(vertices, piece.to_global(Vector3(point.x, bottom_y, point.y)))
|
||||
elif piece.piece_type == ModularBuilderPiece.PieceType.FLOOR:
|
||||
for point in piece.custom_points:
|
||||
_add_unique_vertex(vertices, piece.to_global(point))
|
||||
_add_unique_vertex(vertices, piece.to_global(point - Vector3.UP * piece.thickness))
|
||||
elif piece.piece_type == ModularBuilderPiece.PieceType.WALL:
|
||||
var endpoints: PackedVector3Array = piece.call("_get_centered_wall_points") if piece.center_align else piece.custom_points
|
||||
if endpoints.size() < 2:
|
||||
return
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x) * piece.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
for height in [0.0, piece.unit_size]:
|
||||
_add_unique_vertex(vertices, piece.to_global(endpoint + Vector3.UP * height + side))
|
||||
_add_unique_vertex(vertices, piece.to_global(endpoint + Vector3.UP * height - side))
|
||||
elif piece.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL or piece.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT:
|
||||
_add_single_gable_wall_vertices(piece, vertices)
|
||||
elif piece.piece_type == ModularBuilderPiece.PieceType.ROOF_GABLE or piece.piece_type == ModularBuilderPiece.PieceType.ROOF_SHED or piece.piece_type == ModularBuilderPiece.PieceType.ROOF_PYRAMID:
|
||||
for point in piece.custom_points:
|
||||
_add_unique_vertex(vertices, piece.to_global(point))
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.ROOF_PYRAMID and piece.custom_points.size() >= 4:
|
||||
var base_center := (piece.custom_points[0] + piece.custom_points[1] + piece.custom_points[2] + piece.custom_points[3]) * 0.25
|
||||
_add_unique_vertex(vertices, piece.to_global(base_center + Vector3.UP * piece.rise))
|
||||
|
||||
func _collect_all_connection_vertices(root: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
var surfaces: Array[ModularBuilderPiece] = []
|
||||
_collect_surface_pieces(root, surfaces)
|
||||
for piece in surfaces:
|
||||
var polygon := _piece_polygon_xz(piece) if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR else _foundation_polygon(piece)
|
||||
var top_y := piece.thickness * 0.5 if piece.centered_origin else piece.thickness
|
||||
var bottom_y := -piece.thickness * 0.5 if piece.centered_origin else 0.0
|
||||
for point in polygon:
|
||||
_add_unique_vertex(vertices, piece.to_global(Vector3(point.x, top_y, point.y)))
|
||||
_add_unique_vertex(vertices, piece.to_global(Vector3(point.x, bottom_y, point.y)))
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x) * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
for height in [0.0, wall.unit_size]:
|
||||
_add_unique_vertex(vertices, wall.to_global(endpoint + Vector3.UP * height + side))
|
||||
_add_unique_vertex(vertices, wall.to_global(endpoint + Vector3.UP * height - side))
|
||||
_add_gable_wall_vertices(root, vertices)
|
||||
_collect_detail_connection_vertices(root, vertices)
|
||||
var roofs: Array[ModularBuilderPiece] = []
|
||||
_collect_roof_pieces(root, roofs)
|
||||
for roof in roofs:
|
||||
for point in roof.custom_points:
|
||||
_add_unique_vertex(vertices, roof.to_global(point))
|
||||
if roof.piece_type == ModularBuilderPiece.PieceType.ROOF_PYRAMID and roof.custom_points.size() >= 4:
|
||||
var base_center := (roof.custom_points[0] + roof.custom_points[1] + roof.custom_points[2] + roof.custom_points[3]) * 0.25
|
||||
_add_unique_vertex(vertices, roof.to_global(base_center + Vector3.UP * roof.rise))
|
||||
|
||||
func _collect_roof_pieces(node: Node, roofs: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and (node.piece_type == ModularBuilderPiece.PieceType.ROOF_GABLE or node.piece_type == ModularBuilderPiece.PieceType.ROOF_SHED or node.piece_type == ModularBuilderPiece.PieceType.ROOF_PYRAMID):
|
||||
roofs.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_roof_pieces(child, roofs)
|
||||
|
||||
func _collect_detail_connection_vertices(node: Node, vertices: Array[Vector3]) -> void:
|
||||
if node is ModularBuilderDetail:
|
||||
var detail := node as ModularBuilderDetail
|
||||
if detail.detail_type == ModularBuilderDetail.DetailType.PILLAR:
|
||||
var half := detail.width * 0.5
|
||||
for x in [-half, half]:
|
||||
for y in [-detail.height * 0.5, detail.height * 0.5]:
|
||||
for z in [-half, half]:
|
||||
_add_unique_vertex(vertices, detail.to_global(Vector3(x, y, z)))
|
||||
elif (detail.detail_type == ModularBuilderDetail.DetailType.FENCE or detail.detail_type == ModularBuilderDetail.DetailType.PARAPET) and detail.path_points.size() >= 2:
|
||||
for point in detail.path_points:
|
||||
_add_unique_vertex(vertices, detail.to_global(point))
|
||||
for local_point in detail.get_meta(CUSTOM_VERTICES_META, []):
|
||||
_add_unique_vertex(vertices, detail.to_global(local_point))
|
||||
for child in node.get_children():
|
||||
_collect_detail_connection_vertices(child, vertices)
|
||||
|
||||
func _collapse_merged_vertex_groups(root: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
if not root.has_meta(MERGED_VERTEX_GROUPS_META):
|
||||
return
|
||||
var groups: Array = root.get_meta(MERGED_VERTEX_GROUPS_META, [])
|
||||
for group_value in groups:
|
||||
if not group_value is Dictionary:
|
||||
continue
|
||||
var group: Dictionary = group_value
|
||||
var members: PackedVector3Array = group.get("members", PackedVector3Array())
|
||||
var anchor: Vector3 = group.get("position", Vector3.ZERO)
|
||||
var had_member := false
|
||||
for i in range(vertices.size() - 1, -1, -1):
|
||||
var local_vertex := root.to_local(vertices[i])
|
||||
for member in members:
|
||||
if local_vertex.distance_to(member) <= 0.025:
|
||||
vertices.remove_at(i)
|
||||
had_member = true
|
||||
break
|
||||
if had_member:
|
||||
_add_unique_vertex(vertices, root.to_global(anchor))
|
||||
|
||||
func _merge_members_for_point(root: ModularBuilderPiece, world_point: Vector3) -> PackedVector3Array:
|
||||
var local_point := root.to_local(world_point)
|
||||
var groups: Array = root.get_meta(MERGED_VERTEX_GROUPS_META, [])
|
||||
for group_value in groups:
|
||||
if not group_value is Dictionary:
|
||||
continue
|
||||
var group: Dictionary = group_value
|
||||
var anchor: Vector3 = group.get("position", Vector3.ZERO)
|
||||
if local_point.distance_to(anchor) <= 0.04:
|
||||
return group.get("members", PackedVector3Array())
|
||||
return PackedVector3Array([local_point])
|
||||
|
||||
func _finish_merge_vertices(root: ModularBuilderPiece) -> void:
|
||||
if pending_points.size() < 2:
|
||||
return
|
||||
var selected_members := _merge_members_for_point(root, pending_points[0])
|
||||
for member in _merge_members_for_point(root, pending_points[1]):
|
||||
var found := false
|
||||
for existing in selected_members:
|
||||
if existing.distance_to(member) <= 0.025:
|
||||
found = true
|
||||
break
|
||||
if not found:
|
||||
selected_members.append(member)
|
||||
if selected_members.size() < 2:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose two different vertices to merge."
|
||||
update_overlays()
|
||||
return
|
||||
var old_groups: Array = root.get_meta(MERGED_VERTEX_GROUPS_META, []).duplicate(true)
|
||||
var new_groups: Array = []
|
||||
for group_value in old_groups:
|
||||
if not group_value is Dictionary:
|
||||
continue
|
||||
var group: Dictionary = group_value
|
||||
var members: PackedVector3Array = group.get("members", PackedVector3Array())
|
||||
var overlaps := false
|
||||
for member in members:
|
||||
for selected in selected_members:
|
||||
if member.distance_to(selected) <= 0.025:
|
||||
overlaps = true
|
||||
break
|
||||
if overlaps:
|
||||
break
|
||||
if not overlaps:
|
||||
new_groups.append(group)
|
||||
var center := Vector3.ZERO
|
||||
for member in selected_members:
|
||||
center += member
|
||||
center /= selected_members.size()
|
||||
new_groups.append({"members": selected_members, "position": center})
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Merge vertex markers")
|
||||
undo.add_do_method(root, "set_meta", MERGED_VERTEX_GROUPS_META, new_groups)
|
||||
undo.add_undo_method(root, "set_meta", MERGED_VERTEX_GROUPS_META, old_groups)
|
||||
undo.commit_action()
|
||||
pending_points.clear()
|
||||
status_label.text = "Markers merged. Ctrl+Z undoes the merge."
|
||||
update_overlays()
|
||||
|
||||
func _add_gable_wall_vertices(root: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
var gable_walls: Array[Node] = []
|
||||
_collect_gable_walls(root, gable_walls)
|
||||
for node in gable_walls:
|
||||
_add_single_gable_wall_vertices(node as ModularBuilderPiece, vertices)
|
||||
|
||||
func _add_single_gable_wall_vertices(gable: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
if gable.custom_points.size() < 2:
|
||||
return
|
||||
var base_points: PackedVector3Array = gable.call("_get_centered_gable_base") if gable.center_align else gable.custom_points
|
||||
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() * gable.thickness * 0.5
|
||||
var peak := (a + b) * 0.5 + Vector3.UP * gable.rise if gable.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL else b + Vector3.UP * gable.rise
|
||||
for endpoint in [a, b]:
|
||||
_add_unique_vertex(vertices, gable.to_global(endpoint + side))
|
||||
_add_unique_vertex(vertices, gable.to_global(endpoint - side))
|
||||
_add_unique_vertex(vertices, gable.to_global(peak + side))
|
||||
_add_unique_vertex(vertices, gable.to_global(peak - side))
|
||||
|
||||
func _collect_gable_walls(node: Node, gable_walls: Array[Node]) -> void:
|
||||
if node is ModularBuilderPiece and (node.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL or node.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT) and not gable_walls.has(node):
|
||||
gable_walls.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_gable_walls(child, gable_walls)
|
||||
|
||||
func _collect_walls(node: Node, walls: Array[Node]) -> void:
|
||||
if node is ModularBuilderPiece and node.piece_type == ModularBuilderPiece.PieceType.WALL and not walls.has(node):
|
||||
walls.append(node)
|
||||
for child in node.get_children():
|
||||
if child is ModularBuilderPiece and child.piece_type == ModularBuilderPiece.PieceType.WALL:
|
||||
if not walls.has(child):
|
||||
walls.append(child)
|
||||
_collect_walls(child, walls)
|
||||
|
||||
func _add_unique_vertex(vertices: Array[Vector3], point: Vector3) -> void:
|
||||
for existing in vertices:
|
||||
if existing.distance_to(point) < 0.02:
|
||||
return
|
||||
vertices.append(point)
|
||||
|
||||
func _get_manual_vertices(piece: ModularBuilderPiece) -> Array[Vector3]:
|
||||
var result: Array[Vector3] = []
|
||||
var owners: Array[Node3D] = []
|
||||
_collect_manual_vertex_owners(piece, owners)
|
||||
for owner_node in owners:
|
||||
for local_point_value in owner_node.get_meta(CUSTOM_VERTICES_META, []):
|
||||
var local_point: Vector3 = local_point_value
|
||||
result.append(owner_node.to_global(local_point))
|
||||
return result
|
||||
|
||||
func _collect_manual_vertex_owners(node: Node, owners: Array[Node3D]) -> void:
|
||||
if node is ModularBuilderPiece or node is ModularBuilderDetail:
|
||||
owners.append(node as Node3D)
|
||||
for child in node.get_children():
|
||||
_collect_manual_vertex_owners(child, owners)
|
||||
|
||||
func _raycast_piece_mesh(root: ModularBuilderPiece, camera: Camera3D, screen_position: Vector2) -> Variant:
|
||||
return MESH_PICKER.raycast(root, camera, screen_position)
|
||||
|
||||
func _commit_manual_vertices(owner_node: Node3D, new_vertices: Array, action_name: String) -> void:
|
||||
var old_vertices: Array = owner_node.get_meta(CUSTOM_VERTICES_META, []).duplicate()
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action(action_name)
|
||||
undo.add_do_method(owner_node, "set_meta", CUSTOM_VERTICES_META, new_vertices)
|
||||
undo.add_undo_method(owner_node, "set_meta", CUSTOM_VERTICES_META, old_vertices)
|
||||
undo.commit_action()
|
||||
|
||||
func _create_manual_vertex(root: ModularBuilderPiece, world_position: Vector3) -> void:
|
||||
var owner_node := _get_vertex_attachment_node(root)
|
||||
var local_position := owner_node.to_local(world_position)
|
||||
var vertices: Array = owner_node.get_meta(CUSTOM_VERTICES_META, []).duplicate()
|
||||
for existing in vertices:
|
||||
var existing_position: Vector3 = existing
|
||||
if existing_position.distance_to(local_position) < 0.025:
|
||||
status_label.text = "A manual vertex already exists there."
|
||||
return
|
||||
vertices.append(local_position)
|
||||
_commit_manual_vertices(owner_node, vertices, "Create connection vertex")
|
||||
status_label.text = "Connection vertex created and linked to the selected object."
|
||||
|
||||
func _move_manual_vertex(root: ModularBuilderPiece, old_world_position: Vector3, new_world_position: Vector3) -> void:
|
||||
var owner_record := _find_manual_vertex_record(root, old_world_position)
|
||||
if owner_record.is_empty():
|
||||
pending_points.clear()
|
||||
status_label.text = "The selected vertex is no longer available."
|
||||
return
|
||||
var owner_node: Node3D = owner_record.node
|
||||
var vertices: Array = owner_node.get_meta(CUSTOM_VERTICES_META, []).duplicate()
|
||||
vertices[owner_record.index] = owner_node.to_local(new_world_position)
|
||||
pending_points.clear()
|
||||
_commit_manual_vertices(owner_node, vertices, "Move connection vertex")
|
||||
status_label.text = "Vertex moved."
|
||||
|
||||
func _remove_manual_vertex(root: ModularBuilderPiece, world_position: Vector3) -> void:
|
||||
var owner_record := _find_manual_vertex_record(root, world_position)
|
||||
if owner_record.is_empty():
|
||||
return
|
||||
var owner_node: Node3D = owner_record.node
|
||||
var vertices: Array = owner_node.get_meta(CUSTOM_VERTICES_META, []).duplicate()
|
||||
vertices.remove_at(owner_record.index)
|
||||
_commit_manual_vertices(owner_node, vertices, "Remove connection vertex")
|
||||
status_label.text = "Vertex removed."
|
||||
|
||||
func _get_vertex_attachment_node(root: ModularBuilderPiece) -> Node3D:
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected.size() == 1:
|
||||
var selected_piece := selected[0] as ModularBuilderPiece
|
||||
if selected_piece != null and (selected_piece == root or _is_descendant_of_piece(selected_piece, root)):
|
||||
return selected_piece
|
||||
if selected[0] is ModularBuilderDetail and _is_descendant_of_piece(selected[0], root):
|
||||
return selected[0] as ModularBuilderDetail
|
||||
return root
|
||||
|
||||
func _find_manual_vertex_record(root: ModularBuilderPiece, world_position: Vector3) -> Dictionary:
|
||||
var owners: Array[Node3D] = []
|
||||
_collect_manual_vertex_owners(root, owners)
|
||||
var nearest_record := {}
|
||||
var nearest_distance := 0.05
|
||||
for owner_node in owners:
|
||||
var vertices: Array = owner_node.get_meta(CUSTOM_VERTICES_META, [])
|
||||
for i in range(vertices.size()):
|
||||
var local_point: Vector3 = vertices[i]
|
||||
var distance := owner_node.to_global(local_point).distance_to(world_position)
|
||||
if distance < nearest_distance:
|
||||
nearest_distance = distance
|
||||
nearest_record = {"node": owner_node, "index": i}
|
||||
return nearest_record
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://ghgythev6vkt
|
||||
637
addons/modular_builder/modular_builder_plugin.gd
Normal file
|
|
@ -0,0 +1,637 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder/modular_builder_editor_vertices.gd"
|
||||
|
||||
const ROOF_SUPPORT = preload("res://addons/modular_builder/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:
|
||||
pending_points.clear()
|
||||
_editor_sync_elapsed = 1.0
|
||||
status_label.text = "Current points cleared."
|
||||
update_overlays()
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if _is_detail_tool(active_tool) and event is InputEventMouseMotion:
|
||||
_preview_camera = camera
|
||||
_preview_screen_position = event.position
|
||||
_update_detail_preview()
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
if not (event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT):
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
_editor_sync_elapsed = 1.0
|
||||
var root := _get_selected_piece()
|
||||
if root == null:
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
if active_tool == TOOL_FOUNDATION:
|
||||
var side := _nearest_side_button(camera, event.position)
|
||||
if side >= 0:
|
||||
_add_foundation_at_side(root, side)
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
if active_tool == TOOL_NONE:
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
if active_tool == TOOL_CREATE_VERTEX:
|
||||
var create_position := _raycast_piece_mesh(root, camera, event.position)
|
||||
if create_position == null:
|
||||
status_label.text = "Click a structure surface to create a connection vertex."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
_create_manual_vertex(root, create_position)
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if active_tool == TOOL_MOVE_VERTEX:
|
||||
if pending_points.is_empty():
|
||||
var selected_vertex := _nearest_vertex(camera, event.position, _get_manual_vertices(root))
|
||||
if selected_vertex == null:
|
||||
status_label.text = "Click a blue manual vertex to select it."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
pending_points.append(selected_vertex)
|
||||
status_label.text = "Vertex selected. Click the surface where you want to move it."
|
||||
else:
|
||||
var move_position := _raycast_piece_mesh(root, camera, event.position)
|
||||
if move_position == null:
|
||||
status_label.text = "Click a structure surface to move the vertex."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
_move_manual_vertex(root, pending_points[0], move_position)
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if active_tool == TOOL_REMOVE_VERTEX:
|
||||
var remove_position := _nearest_vertex(camera, event.position, _get_manual_vertices(root))
|
||||
if remove_position == null:
|
||||
status_label.text = "Click a blue manual vertex to remove it."
|
||||
else:
|
||||
_remove_manual_vertex(root, remove_position)
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if active_tool == TOOL_PILLAR:
|
||||
var pillar_vertex := _nearest_vertex(camera, event.position, _get_tool_vertices(root))
|
||||
if pillar_vertex == null:
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
if event.alt_pressed:
|
||||
_create_vertical_pillar_from_vertex(root, pillar_vertex)
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
for previous_point in pending_points:
|
||||
if previous_point.distance_to(pillar_vertex) < 0.02:
|
||||
status_label.text = "Choose a different second vertex."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
pending_points.append(pillar_vertex)
|
||||
if pending_points.size() >= 2:
|
||||
_create_pillar_from_vertices(root, pending_points[0], pending_points[1])
|
||||
else:
|
||||
status_label.text = "First vertex marked. Click another vertex to place the pillar between them."
|
||||
update_overlays()
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if active_tool == TOOL_FENCE or active_tool == TOOL_PARAPET:
|
||||
var fence_vertex := _nearest_vertex(camera, event.position, _get_tool_vertices(root))
|
||||
if fence_vertex == null:
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
for previous_point in pending_points:
|
||||
if previous_point.distance_to(fence_vertex) < 0.02:
|
||||
status_label.text = "Choose a different second vertex for the path."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
pending_points.append(fence_vertex)
|
||||
if pending_points.size() >= 2:
|
||||
_create_fence_decoration(root, pending_points[0], pending_points[1])
|
||||
else:
|
||||
status_label.text = "Start marked. Click the final fence vertex."
|
||||
update_overlays()
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if _is_detail_tool(active_tool):
|
||||
if _create_detail_from_click(camera, event.position):
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
status_label.text = "Click a surface compatible with the selected tool."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
var point := _nearest_vertex(camera, event.position, _get_tool_vertices(root))
|
||||
if point == null:
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
for previous_point in pending_points:
|
||||
if previous_point.distance_to(point) < 0.02:
|
||||
status_label.text = "Choose a different vertex."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if active_tool == TOOL_FLOOR_SQUARE and pending_points.size() == 1:
|
||||
var first_local := root.to_local(pending_points[0])
|
||||
var second_local := root.to_local(point)
|
||||
if absf(first_local.x - second_local.x) < 0.02 or absf(first_local.z - second_local.z) < 0.02:
|
||||
status_label.text = "To complete a square floor, choose the opposite diagonal vertex."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
pending_points.append(point)
|
||||
var needed := _required_clicks()
|
||||
if pending_points.size() >= needed:
|
||||
_finish_shape(root)
|
||||
elif active_tool == TOOL_ROOF_SHED:
|
||||
status_label.text = "Low point marked. Click the high point on the other side of the roof."
|
||||
elif active_tool == TOOL_MERGE_VERTICES:
|
||||
status_label.text = "First vertex marked. Click the second to merge the markers."
|
||||
else:
|
||||
status_label.text = "Point %d of %d marked. Click the next vertex." % [pending_points.size(), needed]
|
||||
update_overlays()
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
|
||||
func _forward_3d_draw_over_viewport(overlay: Control) -> void:
|
||||
pass
|
||||
|
||||
func _required_clicks() -> int:
|
||||
if active_tool == TOOL_WALL or active_tool == TOOL_GABLE_WALL or active_tool == TOOL_GABLE_WALL_RIGHT or active_tool == TOOL_MERGE_VERTICES or active_tool == TOOL_FLOOR_SQUARE or active_tool == TOOL_ROOF_GABLE or active_tool == TOOL_ROOF_SHED or active_tool == TOOL_ROOF_PYRAMID or active_tool == TOOL_STAIRS or active_tool == TOOL_FENCE:
|
||||
return 2
|
||||
return 3
|
||||
|
||||
func _finish_shape(root: ModularBuilderPiece) -> void:
|
||||
if active_tool == TOOL_MERGE_VERTICES:
|
||||
_finish_merge_vertices(root)
|
||||
return
|
||||
if active_tool == TOOL_WALL:
|
||||
_finish_wall_segments(root)
|
||||
return
|
||||
if active_tool == TOOL_GABLE_WALL or active_tool == TOOL_GABLE_WALL_RIGHT:
|
||||
_finish_gable_wall(root)
|
||||
return
|
||||
if active_tool == TOOL_ROOF_GABLE or active_tool == TOOL_ROOF_SHED or active_tool == TOOL_ROOF_PYRAMID:
|
||||
_finish_roof(root)
|
||||
return
|
||||
if active_tool == TOOL_STAIRS:
|
||||
_finish_stairs(root)
|
||||
return
|
||||
if active_tool == TOOL_FENCE:
|
||||
_finish_fence(root)
|
||||
return
|
||||
var local_points := PackedVector3Array()
|
||||
for world_point in pending_points:
|
||||
local_points.append(root.to_local(world_point))
|
||||
var piece := Node3D.new()
|
||||
piece.set_script(PIECE_SCRIPT)
|
||||
piece.set("centered_origin", true)
|
||||
piece.name = "FloorTriangle" if active_tool == TOOL_FLOOR_TRIANGLE else "FloorSquare"
|
||||
piece.set("piece_type", ModularBuilderPiece.PieceType.FLOOR)
|
||||
piece.set("thickness", root.thickness)
|
||||
if active_tool == TOOL_FLOOR_SQUARE:
|
||||
var a := local_points[0]
|
||||
var b := local_points[1]
|
||||
var left := minf(a.x, b.x)
|
||||
var right := maxf(a.x, b.x)
|
||||
var near_z := minf(a.z, b.z)
|
||||
var far_z := maxf(a.z, b.z)
|
||||
var y := (a.y + b.y) * 0.5
|
||||
local_points = PackedVector3Array([
|
||||
Vector3(left, y, near_z), Vector3(right, y, near_z),
|
||||
Vector3(right, y, far_z), Vector3(left, y, far_z)
|
||||
])
|
||||
var floor_pivot := _packed_points_bounds_center(local_points)
|
||||
floor_pivot.y -= piece.thickness * 0.5
|
||||
piece.position = floor_pivot
|
||||
for i in range(local_points.size()):
|
||||
local_points[i] -= floor_pivot
|
||||
piece.set("custom_points", local_points)
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Create %s from vertices" % piece.name)
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
pending_points.clear()
|
||||
status_label.text = "%s created inside ModularBuilderPiece. The tool remains active." % piece.name
|
||||
update_overlays()
|
||||
|
||||
func _finish_wall_segments(root: ModularBuilderPiece) -> void:
|
||||
var start_world := pending_points[0]
|
||||
var finish_world := pending_points[1]
|
||||
var path := finish_world - start_world
|
||||
var path_length_squared := path.length_squared()
|
||||
if path_length_squared < 0.0001:
|
||||
pending_points.clear()
|
||||
status_label.text = "Wall points are too close together."
|
||||
update_overlays()
|
||||
return
|
||||
var parameters: Array[float] = [0.0, 1.0]
|
||||
for vertex in _get_tool_vertices(root):
|
||||
var t := (vertex - start_world).dot(path) / path_length_squared
|
||||
if t <= 0.001 or t >= 0.999:
|
||||
continue
|
||||
var projected := start_world + path * t
|
||||
if vertex.distance_to(projected) > 0.02:
|
||||
continue
|
||||
var already_added := false
|
||||
for existing_t in parameters:
|
||||
if absf(existing_t - t) < 0.005:
|
||||
already_added = true
|
||||
break
|
||||
if not already_added:
|
||||
parameters.append(t)
|
||||
parameters.sort()
|
||||
var pieces: Array[ModularBuilderPiece] = []
|
||||
var centralize_wall := _gable_centralize_check.button_pressed
|
||||
for i in range(parameters.size() - 1):
|
||||
var a_world := start_world.lerp(finish_world, parameters[i])
|
||||
var b_world := start_world.lerp(finish_world, parameters[i + 1])
|
||||
pieces.append(_make_wall_segment(root, a_world, b_world, i + 1, centralize_wall))
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Create wall in %d segments" % pieces.size())
|
||||
for piece in pieces:
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", scene_root)
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = pieces[-1]
|
||||
pending_points.clear()
|
||||
status_label.text = "Wall split into %d segments at foundation vertices." % pieces.size()
|
||||
update_overlays()
|
||||
|
||||
func _finish_gable_wall(root: ModularBuilderPiece) -> void:
|
||||
var start := root.to_local(pending_points[0])
|
||||
var finish := root.to_local(pending_points[1])
|
||||
if Vector2(start.x - finish.x, start.z - finish.z).length() < 0.02 or absf(start.y - finish.y) > 0.05:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose two separate vertices at the same height for the gable base."
|
||||
update_overlays()
|
||||
return
|
||||
var piece_node := Node3D.new()
|
||||
piece_node.set_script(PIECE_SCRIPT)
|
||||
var piece := piece_node as ModularBuilderPiece
|
||||
var is_right_triangle := active_tool == TOOL_GABLE_WALL_RIGHT
|
||||
piece.name = "GableWallRight" if is_right_triangle else "GableWall"
|
||||
piece.piece_type = ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT if is_right_triangle else ModularBuilderPiece.PieceType.GABLE_WALL
|
||||
piece.unit_size = root.unit_size
|
||||
piece.thickness = root.thickness
|
||||
piece.centered_origin = true
|
||||
piece.center_align = _gable_centralize_check.button_pressed
|
||||
var pivot := (start + finish) * 0.5 + Vector3.UP * rise_spin.value * 0.5
|
||||
piece.position = pivot
|
||||
piece.rise = rise_spin.value
|
||||
piece.custom_points = PackedVector3Array([start - pivot, finish - pivot])
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Create gable wall")
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
pending_points.clear()
|
||||
status_label.text = "Gable wall created. The tool remains active."
|
||||
update_overlays()
|
||||
|
||||
func _finish_roof(root: ModularBuilderPiece) -> void:
|
||||
var first := root.to_local(pending_points[0])
|
||||
var second := root.to_local(pending_points[1])
|
||||
var is_gable := active_tool == TOOL_ROOF_GABLE
|
||||
var is_pyramid := active_tool == TOOL_ROOF_PYRAMID
|
||||
var is_shed := active_tool == TOOL_ROOF_SHED
|
||||
if (is_gable or is_pyramid) and absf(first.y - second.y) > 0.05:
|
||||
pending_points.clear()
|
||||
status_label.text = "For this roof, choose two diagonal points at the same height."
|
||||
update_overlays()
|
||||
return
|
||||
if is_shed and second.y <= first.y + 0.05:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose a low point first, then a higher point on the opposite side."
|
||||
update_overlays()
|
||||
return
|
||||
if is_shed and not _shed_points_are_on_opposite_sides(root, pending_points[0], pending_points[1]):
|
||||
pending_points.clear()
|
||||
status_label.text = "Roof points must be on opposite, parallel walls."
|
||||
update_overlays()
|
||||
return
|
||||
var roof_rise := rise_spin.value
|
||||
var roof_width := 0.0
|
||||
var corners := PackedVector3Array()
|
||||
if is_gable:
|
||||
var gable_result := _find_roof_between_gables(root, pending_points[0], pending_points[1])
|
||||
if not gable_result.is_empty():
|
||||
corners = gable_result.corners
|
||||
roof_rise = gable_result.rise
|
||||
elif is_pyramid:
|
||||
corners = _find_roof_corners(root, first, second)
|
||||
else:
|
||||
corners = _find_shed_roof_corners(root, first, second)
|
||||
if corners.size() == 4:
|
||||
roof_width = corners[0].distance_to(corners[1])
|
||||
if is_gable and corners.is_empty():
|
||||
corners = ROOF_SUPPORT.find_corners(root, pending_points[0], pending_points[1], _simplify_vertices_check.button_pressed)
|
||||
if corners.is_empty():
|
||||
corners = _find_roof_corners(root, first, second)
|
||||
if corners.size() != 4:
|
||||
pending_points.clear()
|
||||
status_label.text = "Could not find the other roof corners. Choose two diagonal vertices on the top edges of opposite walls."
|
||||
update_overlays()
|
||||
return
|
||||
var piece_node := Node3D.new()
|
||||
piece_node.set_script(PIECE_SCRIPT)
|
||||
var piece := piece_node as ModularBuilderPiece
|
||||
piece.name = "RoofGable" if is_gable else ("RoofPyramid" if is_pyramid else "RoofShed")
|
||||
piece.piece_type = ModularBuilderPiece.PieceType.ROOF_GABLE if is_gable else (ModularBuilderPiece.PieceType.ROOF_PYRAMID if is_pyramid else ModularBuilderPiece.PieceType.ROOF_SHED)
|
||||
piece.unit_size = root.unit_size
|
||||
piece.thickness = 0.05
|
||||
piece.centered_origin = true
|
||||
piece.rise = roof_rise
|
||||
if is_shed:
|
||||
piece.roof_width = roof_width
|
||||
var pivot := Vector3.ZERO
|
||||
for corner in corners:
|
||||
pivot += corner
|
||||
pivot /= corners.size()
|
||||
if is_gable or is_pyramid:
|
||||
pivot += Vector3.UP * roof_rise * 0.5
|
||||
piece.position = pivot
|
||||
if is_gable or is_pyramid:
|
||||
var local_corners := PackedVector3Array()
|
||||
for corner in corners:
|
||||
var local_corner: Vector3 = corner - pivot
|
||||
local_corner.y = -roof_rise * 0.5
|
||||
local_corners.append(local_corner)
|
||||
piece.custom_points = local_corners
|
||||
else:
|
||||
var local_corners := PackedVector3Array()
|
||||
for corner in corners:
|
||||
local_corners.append(corner - pivot)
|
||||
piece.custom_points = local_corners
|
||||
var undo := get_undo_redo()
|
||||
var roof_title := "gable roof" if is_gable else ("pyramid roof" if is_pyramid else "shed roof")
|
||||
undo.create_action("Create %s" % roof_title)
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
pending_points.clear()
|
||||
status_label.text = "%s created. The tool remains active." % piece.name
|
||||
update_overlays()
|
||||
|
||||
func _finish_stairs(root: ModularBuilderPiece) -> void:
|
||||
if pending_points.size() < 2:
|
||||
return
|
||||
var low := root.to_local(pending_points[0])
|
||||
var high := root.to_local(pending_points[1])
|
||||
if high.y <= low.y + 0.05:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose the low point first, then a point at least 5 cm higher."
|
||||
update_overlays()
|
||||
return
|
||||
if Vector2(high.x - low.x, high.z - low.z).length() < 0.1:
|
||||
pending_points.clear()
|
||||
status_label.text = "Stair points need horizontal separation."
|
||||
update_overlays()
|
||||
return
|
||||
var piece_node := Node3D.new()
|
||||
piece_node.set_script(PIECE_SCRIPT)
|
||||
var piece := piece_node as ModularBuilderPiece
|
||||
var pivot := (low + high) * 0.5
|
||||
piece.name = "Stairs"
|
||||
piece.piece_type = ModularBuilderPiece.PieceType.STAIRS
|
||||
piece.position = pivot
|
||||
piece.unit_size = root.unit_size
|
||||
piece.thickness = 0.1
|
||||
piece.centered_origin = true
|
||||
piece.stair_count = int(stair_count_spin.value)
|
||||
piece.stair_width = stair_width_spin.value
|
||||
piece.custom_points = PackedVector3Array([low - pivot, high - pivot])
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Create stairs")
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
pending_points.clear()
|
||||
status_label.text = "Stairs created with %d steps. The tool remains active." % piece.stair_count
|
||||
update_overlays()
|
||||
|
||||
func _finish_fence(root: ModularBuilderPiece) -> void:
|
||||
if pending_points.size() < 2:
|
||||
return
|
||||
var start := root.to_local(pending_points[0])
|
||||
var finish := root.to_local(pending_points[1])
|
||||
if start.distance_to(finish) < 0.1:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose two points far enough apart for the fence."
|
||||
update_overlays()
|
||||
return
|
||||
var piece_node := Node3D.new()
|
||||
piece_node.set_script(PIECE_SCRIPT)
|
||||
var piece := piece_node as ModularBuilderPiece
|
||||
var pivot := (start + finish) * 0.5
|
||||
piece.name = "Fence"
|
||||
piece.piece_type = ModularBuilderPiece.PieceType.FENCE
|
||||
piece.position = pivot
|
||||
piece.unit_size = root.unit_size
|
||||
piece.thickness = 0.08
|
||||
piece.centered_origin = true
|
||||
piece.fence_divisions = int(fence_divisions_spin.value)
|
||||
piece.fence_height = 1.0
|
||||
piece.custom_points = PackedVector3Array([start - pivot, finish - pivot])
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Create fence")
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
pending_points.clear()
|
||||
status_label.text = "Fence created with %d intermediate post(s). The tool remains active." % piece.fence_divisions
|
||||
update_overlays()
|
||||
|
||||
func _find_roof_corners(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> PackedVector3Array:
|
||||
var diagonal_center := (first + second) * 0.5
|
||||
var diagonal_length := first.distance_to(second)
|
||||
var candidates: Array[Vector3] = []
|
||||
for world_point in _get_tool_vertices(root):
|
||||
var local_point := root.to_local(world_point)
|
||||
if absf(local_point.y - first.y) > 0.04:
|
||||
continue
|
||||
_add_unique_vertex(candidates, local_point)
|
||||
for adjacent in candidates:
|
||||
if adjacent.distance_to(first) < 0.02 or adjacent.distance_to(second) < 0.02:
|
||||
continue
|
||||
var opposite := diagonal_center * 2.0 - adjacent
|
||||
for other_adjacent in candidates:
|
||||
if other_adjacent.distance_to(first) < 0.02 or other_adjacent.distance_to(second) < 0.02 or other_adjacent.distance_to(adjacent) < 0.02:
|
||||
continue
|
||||
if other_adjacent.distance_to(opposite) > 0.04:
|
||||
continue
|
||||
if absf(adjacent.distance_to(other_adjacent) - diagonal_length) > 0.06:
|
||||
continue
|
||||
return PackedVector3Array([first, adjacent, second, other_adjacent])
|
||||
return PackedVector3Array()
|
||||
|
||||
func _find_shed_roof_corners(root: ModularBuilderPiece, low: Vector3, high: Vector3) -> PackedVector3Array:
|
||||
var candidates: Array[Vector3] = []
|
||||
for world_point in _get_tool_vertices(root):
|
||||
_add_unique_vertex(candidates, root.to_local(world_point))
|
||||
var match_tolerance := maxf(0.08, root.thickness * 1.5)
|
||||
var best := PackedVector3Array()
|
||||
var best_score := INF
|
||||
for low_adjacent in candidates:
|
||||
if absf(low_adjacent.y - low.y) > 0.04 or low_adjacent.distance_to(low) < 0.4:
|
||||
continue
|
||||
var target_high_adjacent := low + high - low_adjacent
|
||||
for high_adjacent in candidates:
|
||||
if absf(high_adjacent.y - high.y) > 0.04 or high_adjacent.distance_to(high) < 0.4:
|
||||
continue
|
||||
var corner_error := high_adjacent.distance_to(target_high_adjacent)
|
||||
if corner_error > match_tolerance:
|
||||
continue
|
||||
var low_edge := Vector2(low_adjacent.x - low.x, low_adjacent.z - low.z)
|
||||
var high_edge := Vector2(high.x - high_adjacent.x, high.z - high_adjacent.z)
|
||||
if low_edge.length() < 0.4 or high_edge.length() < 0.4:
|
||||
continue
|
||||
if absf(low_edge.normalized().dot(high_edge.normalized())) < 0.94:
|
||||
continue
|
||||
var edge_error := absf(low_edge.length() - high_edge.length())
|
||||
if edge_error > match_tolerance * 2.0:
|
||||
continue
|
||||
var score := corner_error + edge_error
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
best = PackedVector3Array([low, low_adjacent, high_adjacent, high])
|
||||
return best
|
||||
|
||||
func _shed_points_are_on_opposite_sides(root: ModularBuilderPiece, low_world: Vector3, high_world: Vector3) -> bool:
|
||||
var low_supports := _get_roof_supports_at_point(root, low_world)
|
||||
var high_supports := _get_roof_supports_at_point(root, high_world)
|
||||
var across := root.to_local(high_world) - root.to_local(low_world)
|
||||
var across_horizontal := Vector2(across.x, across.z)
|
||||
if across_horizontal.length() < 0.1:
|
||||
return false
|
||||
across_horizontal = across_horizontal.normalized()
|
||||
for low_support in low_supports:
|
||||
for high_support in high_supports:
|
||||
if low_support.piece == high_support.piece:
|
||||
continue
|
||||
var low_direction: Vector2 = low_support.direction
|
||||
var high_direction: Vector2 = high_support.direction
|
||||
if absf(low_direction.dot(high_direction)) < 0.85:
|
||||
continue
|
||||
var side_axis := Vector2(-low_direction.y, low_direction.x)
|
||||
var center_delta: Vector3 = high_support.center - low_support.center
|
||||
var side_separation := absf(Vector2(center_delta.x, center_delta.z).dot(side_axis))
|
||||
if side_separation >= 0.4:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _get_roof_supports_at_point(root: ModularBuilderPiece, world_point: Vector3) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for node in walls:
|
||||
var wall := node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var world_a := wall.to_global(endpoints[0])
|
||||
var world_b := wall.to_global(endpoints[1])
|
||||
var local_direction := root.to_local(world_b) - root.to_local(world_a)
|
||||
var direction := Vector2(local_direction.x, local_direction.z).normalized()
|
||||
var wall_local_direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-wall_local_direction.z, 0.0, wall_local_direction.x).normalized() * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
for height in [0.0, wall.unit_size]:
|
||||
var center := wall.to_global(endpoint + Vector3.UP * height)
|
||||
var support_points := [center + side, center - side]
|
||||
if _simplify_vertices_check.button_pressed:
|
||||
support_points.append(center)
|
||||
for point in support_points:
|
||||
if point.distance_to(world_point) <= 0.04:
|
||||
result.append({"piece": wall, "direction": direction, "center": root.to_local(center)})
|
||||
var gables: Array[Node] = []
|
||||
_collect_gable_walls(root, gables)
|
||||
for node in gables:
|
||||
var gable := node as ModularBuilderPiece
|
||||
if gable.custom_points.size() < 2:
|
||||
continue
|
||||
var base: PackedVector3Array = gable.call("_get_centered_gable_base") if gable.center_align else gable.custom_points
|
||||
var a: Vector3 = base[0]
|
||||
var b: Vector3 = base[1]
|
||||
var local_direction := b - a
|
||||
var direction := Vector2(local_direction.x, local_direction.z).normalized()
|
||||
var side := Vector3(-local_direction.z, 0.0, local_direction.x).normalized() * gable.thickness * 0.5
|
||||
var peak := (a + b) * 0.5 + Vector3.UP * gable.rise if gable.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL else b + Vector3.UP * gable.rise
|
||||
var support_points := [a + side, a - side, b + side, b - side, peak + side, peak - side]
|
||||
if _simplify_vertices_check.button_pressed:
|
||||
support_points.append_array([a, b, peak])
|
||||
for point_local in support_points:
|
||||
var point := gable.to_global(point_local)
|
||||
if point.distance_to(world_point) <= 0.04:
|
||||
result.append({"piece": gable, "direction": direction, "center": root.to_local(gable.to_global((a + b) * 0.5))})
|
||||
if result.is_empty() and root.has_meta(MERGED_VERTEX_GROUPS_META):
|
||||
for group_value in root.get_meta(MERGED_VERTEX_GROUPS_META, []):
|
||||
if not group_value is Dictionary:
|
||||
continue
|
||||
var group: Dictionary = group_value
|
||||
var anchor: Vector3 = group.get("position", Vector3.ZERO)
|
||||
if root.to_global(anchor).distance_to(world_point) > 0.04:
|
||||
continue
|
||||
var members: PackedVector3Array = group.get("members", PackedVector3Array())
|
||||
for member in members:
|
||||
result.append_array(_get_roof_supports_at_point(root, root.to_global(member)))
|
||||
return result
|
||||
|
||||
func _find_roof_between_gables(root: ModularBuilderPiece, first_world: Vector3, second_world: Vector3) -> Dictionary:
|
||||
var gables: Array[Node] = []
|
||||
_collect_gable_walls(root, gables)
|
||||
var first_gable: ModularBuilderPiece
|
||||
var second_gable: ModularBuilderPiece
|
||||
var first_base := PackedVector3Array()
|
||||
var second_base := PackedVector3Array()
|
||||
for node in gables:
|
||||
var gable := node as ModularBuilderPiece
|
||||
if gable.custom_points.size() < 2:
|
||||
continue
|
||||
var base: PackedVector3Array = gable.call("_get_centered_gable_base") if gable.center_align else gable.custom_points
|
||||
var a: Vector3 = base[0]
|
||||
var b: Vector3 = base[1]
|
||||
var direction := (b - a).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * gable.thickness * 0.5
|
||||
var peak := (a + b) * 0.5 + Vector3.UP * gable.rise if gable.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL else b + Vector3.UP * gable.rise
|
||||
var peak_a := gable.to_global(peak + side)
|
||||
var peak_b := gable.to_global(peak - side)
|
||||
var peak_center := gable.to_global(peak)
|
||||
if first_gable == null and (peak_a.distance_to(first_world) < 0.12 or peak_b.distance_to(first_world) < 0.12 or (_simplify_vertices_check.button_pressed and peak_center.distance_to(first_world) < 0.12)):
|
||||
first_gable = gable
|
||||
first_base = base
|
||||
elif second_gable == null and gable != first_gable and (peak_a.distance_to(second_world) < 0.12 or peak_b.distance_to(second_world) < 0.12 or (_simplify_vertices_check.button_pressed and peak_center.distance_to(second_world) < 0.12)):
|
||||
second_gable = gable
|
||||
second_base = base
|
||||
if first_gable == null or second_gable == null:
|
||||
return {}
|
||||
var a0 := root.to_local(first_gable.to_global(first_base[0]))
|
||||
var a1 := root.to_local(first_gable.to_global(first_base[1]))
|
||||
var b0 := root.to_local(second_gable.to_global(second_base[0]))
|
||||
var b1 := root.to_local(second_gable.to_global(second_base[1]))
|
||||
var direct_score := a0.distance_to(b0) + a1.distance_to(b1)
|
||||
var crossed_score := a0.distance_to(b1) + a1.distance_to(b0)
|
||||
var corners := PackedVector3Array([a0, a1, b1, b0]) if direct_score <= crossed_score else PackedVector3Array([a0, a1, b0, b1])
|
||||
return {"corners": corners, "rise": (first_gable.rise + second_gable.rise) * 0.5}
|
||||
|
||||
func _make_wall_segment(root: ModularBuilderPiece, start_world: Vector3, finish_world: Vector3, index: int, centralize_wall: bool) -> ModularBuilderPiece:
|
||||
var centered_start := _center_wall_anchor(root, start_world) if centralize_wall else start_world
|
||||
var centered_finish := _center_wall_anchor(root, finish_world) if centralize_wall else finish_world
|
||||
var start := root.to_local(centered_start)
|
||||
var finish := root.to_local(centered_finish)
|
||||
var piece_node := Node3D.new()
|
||||
piece_node.set_script(PIECE_SCRIPT)
|
||||
var piece := piece_node as ModularBuilderPiece
|
||||
piece.name = "Wall_%02d" % index
|
||||
piece.set("piece_type", ModularBuilderPiece.PieceType.WALL)
|
||||
piece.set("unit_size", root.unit_size)
|
||||
piece.set("thickness", root.thickness)
|
||||
piece.set("centered_origin", true)
|
||||
piece.set("center_align", centralize_wall)
|
||||
var pivot := (start + finish) * 0.5 + Vector3.UP * root.unit_size * 0.5
|
||||
piece.position = pivot
|
||||
piece.set("custom_points", PackedVector3Array([start - pivot, finish - pivot]))
|
||||
return piece
|
||||
|
||||
func _center_wall_anchor(root: ModularBuilderPiece, world_point: Vector3) -> Vector3:
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
var best_point := world_point
|
||||
var best_distance := INF
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
var center := wall.to_global(endpoint + Vector3.UP * wall.unit_size)
|
||||
for surface_corner in [center + side, center - side]:
|
||||
var distance := world_point.distance_to(surface_corner)
|
||||
if distance < best_distance and distance <= wall.thickness + 0.04:
|
||||
best_distance = distance
|
||||
best_point = center
|
||||
return best_point
|
||||
1
addons/modular_builder/modular_builder_plugin.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b065m6e7ey5n0
|
||||
166
addons/modular_builder/modular_mesh_baker.gd
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
func bake(building: ModularBuilderPiece) -> ArrayMesh:
|
||||
var mesh_instances: Array[MeshInstance3D] = []
|
||||
_collect_mesh_instances(building, mesh_instances)
|
||||
var groups: Array[Dictionary] = []
|
||||
var projected_materials: Dictionary = {}
|
||||
for mesh_instance in mesh_instances:
|
||||
if not is_instance_valid(mesh_instance) or not mesh_instance.is_visible_in_tree() or _is_export_excluded(mesh_instance, building):
|
||||
continue
|
||||
var source_mesh := mesh_instance.mesh
|
||||
if source_mesh == null:
|
||||
continue
|
||||
var relative_transform := building.global_transform.affine_inverse() * mesh_instance.global_transform
|
||||
if absf(relative_transform.basis.determinant()) < 0.000001:
|
||||
continue
|
||||
var normal_transform := relative_transform.basis.inverse().transposed()
|
||||
var mirrored := relative_transform.basis.determinant() < 0.0
|
||||
for surface_index in range(source_mesh.get_surface_count()):
|
||||
var primitive_type := Mesh.PRIMITIVE_TRIANGLES
|
||||
var surface_arrays: Array
|
||||
if source_mesh is PrimitiveMesh:
|
||||
# PrimitiveMesh provides mesh arrays directly; surface_get_* methods
|
||||
# are used for other meshes.
|
||||
if surface_index > 0:
|
||||
continue
|
||||
surface_arrays = source_mesh.get_mesh_arrays()
|
||||
else:
|
||||
primitive_type = source_mesh.surface_get_primitive_type(surface_index)
|
||||
surface_arrays = source_mesh.surface_get_arrays(surface_index)
|
||||
if primitive_type != Mesh.PRIMITIVE_TRIANGLES:
|
||||
continue
|
||||
if surface_arrays.is_empty() or not surface_arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array:
|
||||
continue
|
||||
var source_vertices: PackedVector3Array = surface_arrays[Mesh.ARRAY_VERTEX]
|
||||
if source_vertices.size() < 3:
|
||||
continue
|
||||
var source_normals := PackedVector3Array()
|
||||
var source_uvs := PackedVector2Array()
|
||||
var source_colors := PackedColorArray()
|
||||
if surface_arrays[Mesh.ARRAY_NORMAL] is PackedVector3Array:
|
||||
source_normals = surface_arrays[Mesh.ARRAY_NORMAL]
|
||||
if surface_arrays[Mesh.ARRAY_TEX_UV] is PackedVector2Array:
|
||||
source_uvs = surface_arrays[Mesh.ARRAY_TEX_UV]
|
||||
if surface_arrays[Mesh.ARRAY_COLOR] is PackedColorArray:
|
||||
source_colors = surface_arrays[Mesh.ARRAY_COLOR]
|
||||
var source_indices := PackedInt32Array()
|
||||
if surface_arrays[Mesh.ARRAY_INDEX] is PackedInt32Array:
|
||||
source_indices = surface_arrays[Mesh.ARRAY_INDEX]
|
||||
var material := mesh_instance.get_active_material(surface_index)
|
||||
var force_projected_uvs := false
|
||||
var bake_material := material
|
||||
if material is StandardMaterial3D and material.uv1_triplanar:
|
||||
force_projected_uvs = true
|
||||
var material_id := material.get_instance_id()
|
||||
if not projected_materials.has(material_id):
|
||||
var material_copy := material.duplicate() as StandardMaterial3D
|
||||
material_copy.uv1_triplanar = false
|
||||
material_copy.uv1_world_triplanar = false
|
||||
projected_materials[material_id] = material_copy
|
||||
bake_material = projected_materials[material_id] as Material
|
||||
var group_index := _find_or_create_group(groups, bake_material)
|
||||
var group: Dictionary = groups[group_index]
|
||||
var group_vertices: Array = group["vertices"]
|
||||
var group_normals: Array = group["normals"]
|
||||
var group_uvs: Array = group["uvs"]
|
||||
var group_colors: Array = group["colors"]
|
||||
var has_indices := not source_indices.is_empty()
|
||||
var triangle_count := source_indices.size() if has_indices else source_vertices.size()
|
||||
for triangle_start in range(0, triangle_count - 2, 3):
|
||||
var index_a := source_indices[triangle_start] if has_indices else triangle_start
|
||||
var index_b := source_indices[triangle_start + 1] if has_indices else triangle_start + 1
|
||||
var index_c := source_indices[triangle_start + 2] if has_indices else triangle_start + 2
|
||||
if index_a >= source_vertices.size() or index_b >= source_vertices.size() or index_c >= source_vertices.size():
|
||||
continue
|
||||
if mirrored:
|
||||
var swap_index := index_b
|
||||
index_b = index_c
|
||||
index_c = swap_index
|
||||
var local_a: Vector3 = source_vertices[index_a]
|
||||
var local_b: Vector3 = source_vertices[index_b]
|
||||
var local_c: Vector3 = source_vertices[index_c]
|
||||
var baked_a: Vector3 = relative_transform * local_a
|
||||
var baked_b: Vector3 = relative_transform * local_b
|
||||
var baked_c: Vector3 = relative_transform * local_c
|
||||
var face_normal := (baked_b - baked_a).cross(baked_c - baked_a).normalized()
|
||||
if face_normal.length_squared() < 0.5:
|
||||
continue
|
||||
var baked_points := [baked_a, baked_b, baked_c]
|
||||
var source_point_indices := [index_a, index_b, index_c]
|
||||
var source_face_normal := (local_b - local_a).cross(local_c - local_a).normalized()
|
||||
for corner in range(3):
|
||||
var source_index: int = source_point_indices[corner]
|
||||
var baked_point: Vector3 = baked_points[corner]
|
||||
var baked_normal := face_normal
|
||||
if source_normals.size() == source_vertices.size():
|
||||
baked_normal = (normal_transform * source_normals[source_index]).normalized()
|
||||
if baked_normal.length_squared() < 0.5:
|
||||
baked_normal = face_normal
|
||||
var uv: Vector2
|
||||
if not force_projected_uvs and source_uvs.size() == source_vertices.size():
|
||||
uv = source_uvs[source_index]
|
||||
else:
|
||||
uv = _project_uv(source_vertices[source_index], source_face_normal)
|
||||
var color := source_colors[source_index] if source_colors.size() == source_vertices.size() else Color.WHITE
|
||||
group_vertices.append(baked_point)
|
||||
group_normals.append(baked_normal)
|
||||
group_uvs.append(uv)
|
||||
group_colors.append(color)
|
||||
groups[group_index] = group
|
||||
if groups.is_empty():
|
||||
return null
|
||||
var combined := ArrayMesh.new()
|
||||
combined.resource_name = "%s_Mesh" % building.name
|
||||
for group in groups:
|
||||
var vertices := PackedVector3Array(group["vertices"])
|
||||
if vertices.is_empty():
|
||||
continue
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = PackedVector3Array(group["normals"])
|
||||
arrays[Mesh.ARRAY_TEX_UV] = PackedVector2Array(group["uvs"])
|
||||
arrays[Mesh.ARRAY_COLOR] = PackedColorArray(group["colors"])
|
||||
var surface_tool := SurfaceTool.new()
|
||||
surface_tool.create_from_arrays(arrays, Mesh.PRIMITIVE_TRIANGLES)
|
||||
if group["material"] is Material:
|
||||
surface_tool.set_material(group["material"])
|
||||
surface_tool.generate_tangents()
|
||||
var temp_mesh := surface_tool.commit()
|
||||
if temp_mesh == null or temp_mesh.get_surface_count() == 0:
|
||||
continue
|
||||
combined.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, temp_mesh.surface_get_arrays(0))
|
||||
if group["material"] is Material:
|
||||
combined.surface_set_material(combined.get_surface_count() - 1, group["material"])
|
||||
return combined if combined.get_surface_count() > 0 else null
|
||||
|
||||
func _find_or_create_group(groups: Array[Dictionary], material: Material) -> int:
|
||||
for i in range(groups.size()):
|
||||
if groups[i]["material"] == material:
|
||||
return i
|
||||
groups.append({"material": material, "vertices": [], "normals": [], "uvs": [], "colors": []})
|
||||
return groups.size() - 1
|
||||
|
||||
func _project_uv(point: Vector3, normal: Vector3) -> Vector2:
|
||||
var absolute_normal := normal.abs()
|
||||
if absolute_normal.y >= absolute_normal.x and absolute_normal.y >= absolute_normal.z:
|
||||
return Vector2(point.x, point.z)
|
||||
if absolute_normal.x >= absolute_normal.z:
|
||||
return Vector2(point.z, point.y)
|
||||
return Vector2(point.x, point.y)
|
||||
|
||||
func _is_export_excluded(mesh_instance: MeshInstance3D, building: ModularBuilderPiece) -> bool:
|
||||
var current: Node = mesh_instance
|
||||
while current != null and current != building:
|
||||
if current.has_meta("modular_builder_preview") or current.name == "ModularBuilderMarkers":
|
||||
return true
|
||||
current = current.get_parent()
|
||||
return false
|
||||
|
||||
func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
|
||||
if node is MeshInstance3D:
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_mesh_instances(child, result)
|
||||
1
addons/modular_builder/modular_mesh_baker.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bd52re1amu4r2
|
||||
55
addons/modular_builder/modular_mesh_picker.gd
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
static func raycast(root: Node3D, camera: Camera3D, screen_position: Vector2) -> Variant:
|
||||
var ray_origin := camera.project_ray_origin(screen_position)
|
||||
var ray_direction := camera.project_ray_normal(screen_position).normalized()
|
||||
var nearest_point: Variant = null
|
||||
var nearest_distance := INF
|
||||
var mesh_nodes: Array[MeshInstance3D] = []
|
||||
_collect_mesh_instances(root, mesh_nodes)
|
||||
for mesh_node in mesh_nodes:
|
||||
var source_mesh := mesh_node.mesh
|
||||
if source_mesh == null or not mesh_node.is_visible_in_tree():
|
||||
continue
|
||||
for surface_index in range(source_mesh.get_surface_count()):
|
||||
var arrays: Array
|
||||
if source_mesh is PrimitiveMesh:
|
||||
if surface_index > 0:
|
||||
continue
|
||||
arrays = source_mesh.get_mesh_arrays()
|
||||
else:
|
||||
if source_mesh.surface_get_primitive_type(surface_index) != Mesh.PRIMITIVE_TRIANGLES:
|
||||
continue
|
||||
arrays = source_mesh.surface_get_arrays(surface_index)
|
||||
if arrays.is_empty() or not arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array:
|
||||
continue
|
||||
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
var indices := PackedInt32Array()
|
||||
if arrays[Mesh.ARRAY_INDEX] is PackedInt32Array:
|
||||
indices = arrays[Mesh.ARRAY_INDEX]
|
||||
var has_indices := not indices.is_empty()
|
||||
var triangle_count := indices.size() if has_indices else vertices.size()
|
||||
for triangle_start in range(0, triangle_count - 2, 3):
|
||||
var index_a := indices[triangle_start] if has_indices else triangle_start
|
||||
var index_b := indices[triangle_start + 1] if has_indices else triangle_start + 1
|
||||
var index_c := indices[triangle_start + 2] if has_indices else triangle_start + 2
|
||||
if index_a < 0 or index_b < 0 or index_c < 0 or index_a >= vertices.size() or index_b >= vertices.size() or index_c >= vertices.size():
|
||||
continue
|
||||
var vertex_a := mesh_node.to_global(vertices[index_a])
|
||||
var vertex_b := mesh_node.to_global(vertices[index_b])
|
||||
var vertex_c := mesh_node.to_global(vertices[index_c])
|
||||
var hit: Variant = Geometry3D.ray_intersects_triangle(ray_origin, ray_direction, vertex_a, vertex_b, vertex_c)
|
||||
if hit == null:
|
||||
continue
|
||||
var distance := ray_origin.distance_to(hit)
|
||||
if distance < nearest_distance:
|
||||
nearest_distance = distance
|
||||
nearest_point = hit
|
||||
return nearest_point
|
||||
|
||||
static func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
|
||||
if node is MeshInstance3D:
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_mesh_instances(child, result)
|
||||
1
addons/modular_builder/modular_mesh_picker.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cg71aq1smh1q3
|
||||
61
addons/modular_builder/modular_piece.gd
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
@tool
|
||||
class_name ModularBuilderPiece
|
||||
extends "res://core/vendor/modular_builder/modular_piece_geometry.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
_rebuild()
|
||||
|
||||
func _validate_property(property: Dictionary) -> void:
|
||||
if property.name == &"symmetric_dimensions" and piece_type == PieceType.BUILDING:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"dimension_height" and (piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE or piece_type == PieceType.FLOOR):
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"dimension_depth" and piece_type == PieceType.WALL:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"dimension_width" or property.name == &"dimension_height" or property.name == &"dimension_depth":
|
||||
if symmetric_dimensions:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"rise" and piece_type != PieceType.GABLE_WALL and piece_type != PieceType.GABLE_WALL_RIGHT and piece_type != PieceType.ROOF_GABLE and piece_type != PieceType.ROOF_PYRAMID:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"roof_width" and piece_type != PieceType.ROOF_SHED:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"stair_count" and piece_type != PieceType.STAIRS:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"stair_width" and piece_type != PieceType.STAIRS:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"fence_divisions" and piece_type != PieceType.FENCE:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"fence_height" and piece_type != PieceType.FENCE:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"center_align" and piece_type != PieceType.GABLE_WALL and piece_type != PieceType.GABLE_WALL_RIGHT and piece_type != PieceType.WALL:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
|
||||
func _rebuild() -> void:
|
||||
if not is_inside_tree():
|
||||
return
|
||||
var old_collision := get_node_or_null("Collision")
|
||||
if old_collision != null:
|
||||
remove_child(old_collision)
|
||||
old_collision.queue_free()
|
||||
var mesh_instance := get_node_or_null("Geometry") as MeshInstance3D
|
||||
if piece_type == PieceType.BUILDING:
|
||||
if mesh_instance != null:
|
||||
remove_child(mesh_instance)
|
||||
mesh_instance.queue_free()
|
||||
return
|
||||
if mesh_instance == null:
|
||||
mesh_instance = MeshInstance3D.new()
|
||||
mesh_instance.name = "Geometry"
|
||||
add_child(mesh_instance)
|
||||
_set_scene_owner(mesh_instance)
|
||||
mesh_instance.mesh = _make_mesh()
|
||||
mesh_instance.material_override = surface_material
|
||||
if mesh_instance.mesh != null and mesh_instance.mesh.get_surface_count() > 0:
|
||||
_dimension_reference = mesh_instance.mesh.get_aabb().size
|
||||
_apply_dimension_scale()
|
||||
|
||||
func _set_scene_owner(node: Node) -> void:
|
||||
if Engine.is_editor_hint():
|
||||
var scene_root := get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
node.owner = scene_root
|
||||
1
addons/modular_builder/modular_piece.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://coqicbv0jfksq
|
||||
34
addons/modular_builder/modular_pillar_support.gd
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
static func vertical_direction_for_piece(piece: ModularBuilderPiece, world_anchor: Vector3) -> Vector3:
|
||||
var local_anchor := piece.to_local(world_anchor)
|
||||
var low_y := 0.0
|
||||
var high_y := piece.unit_size
|
||||
match piece.piece_type:
|
||||
ModularBuilderPiece.PieceType.FOUNDATION_SQUARE, ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
low_y = -piece.thickness * 0.5 if piece.centered_origin else 0.0
|
||||
high_y = piece.thickness * 0.5 if piece.centered_origin else piece.thickness
|
||||
ModularBuilderPiece.PieceType.FLOOR:
|
||||
high_y = piece.custom_points[0].y if not piece.custom_points.is_empty() else (piece.thickness * 0.5 if piece.centered_origin else piece.thickness)
|
||||
low_y = high_y - piece.thickness
|
||||
ModularBuilderPiece.PieceType.GABLE_WALL, ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT:
|
||||
if piece.custom_points.size() >= 2:
|
||||
var base_points: PackedVector3Array = piece.call("_get_centered_gable_base") if piece.center_align else piece.custom_points
|
||||
low_y = base_points[0].y
|
||||
high_y = low_y + piece.rise
|
||||
_:
|
||||
if piece.custom_points.size() >= 2:
|
||||
var nearest_endpoint := piece.custom_points[0]
|
||||
var nearest_horizontal := INF
|
||||
for endpoint in piece.custom_points:
|
||||
var horizontal := Vector2(endpoint.x - local_anchor.x, endpoint.z - local_anchor.z).length_squared()
|
||||
if horizontal < nearest_horizontal:
|
||||
nearest_horizontal = horizontal
|
||||
nearest_endpoint = endpoint
|
||||
low_y = nearest_endpoint.y
|
||||
high_y = nearest_endpoint.y + piece.unit_size
|
||||
return Vector3.DOWN if absf(local_anchor.y - low_y) < absf(local_anchor.y - high_y) else Vector3.UP
|
||||
|
||||
static func vertical_direction_for_pillar(pillar: ModularBuilderDetail, world_anchor: Vector3) -> Vector3:
|
||||
return Vector3.DOWN if pillar.to_local(world_anchor).y < 0.0 else Vector3.UP
|
||||
1
addons/modular_builder/modular_pillar_support.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://df6psamkqmyvg
|
||||
72
addons/modular_builder/modular_roof_support.gd
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
# Find the roof outline from two diagonal corners at the top
|
||||
# of parallel walls, even when the structure is rotated around Y.
|
||||
static func find_corners(root: ModularBuilderPiece, first_world: Vector3, second_world: Vector3, allow_center: bool = false) -> PackedVector3Array:
|
||||
var first_edges := _matching_top_edges(root, first_world, allow_center)
|
||||
var second_edges := _matching_top_edges(root, second_world, allow_center)
|
||||
var best_corners := PackedVector3Array()
|
||||
var best_score := INF
|
||||
var best_length := 0.0
|
||||
for first_edge in first_edges:
|
||||
for second_edge in second_edges:
|
||||
if first_edge["wall"] == second_edge["wall"]:
|
||||
continue
|
||||
var a: Vector3 = first_edge["selected"]
|
||||
var adjacent_a: Vector3 = first_edge["other"]
|
||||
var c: Vector3 = second_edge["selected"]
|
||||
var adjacent_c: Vector3 = second_edge["other"]
|
||||
var edge_a := Vector2(adjacent_a.x - a.x, adjacent_a.z - a.z)
|
||||
var edge_c := Vector2(adjacent_c.x - c.x, adjacent_c.z - c.z)
|
||||
var length_a := edge_a.length()
|
||||
var length_c := edge_c.length()
|
||||
if length_a < 0.2 or length_c < 0.2:
|
||||
continue
|
||||
if edge_a.normalized().dot(edge_c.normalized()) > -0.9:
|
||||
continue
|
||||
var center_a := (a + adjacent_a) * 0.5
|
||||
var center_c := (c + adjacent_c) * 0.5
|
||||
if Vector2(center_c.x - center_a.x, center_c.z - center_a.z).length() < 0.2:
|
||||
continue
|
||||
var diagonal_error := (a + c - adjacent_a - adjacent_c).length()
|
||||
var length_error := absf(length_a - length_c)
|
||||
var tolerance := maxf(0.12, maxf(first_edge["thickness"], second_edge["thickness"]) * 0.5)
|
||||
if diagonal_error > tolerance or length_error > tolerance:
|
||||
continue
|
||||
var score := diagonal_error + length_error
|
||||
if score < best_score - 0.01 or (absf(score - best_score) <= 0.01 and length_a > best_length):
|
||||
best_score = score
|
||||
best_length = length_a
|
||||
best_corners = PackedVector3Array([a, adjacent_a, c, adjacent_c])
|
||||
return best_corners
|
||||
|
||||
static func _matching_top_edges(root: ModularBuilderPiece, world_point: Vector3, allow_center: bool) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
var walls: Array[ModularBuilderPiece] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall in walls:
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := endpoints[1] - endpoints[0]
|
||||
var horizontal := Vector2(direction.x, direction.z)
|
||||
if horizontal.length() < 0.2:
|
||||
continue
|
||||
var side := Vector3(-horizontal.y, 0.0, horizontal.x).normalized() * wall.thickness * 0.5
|
||||
for offset in [side, -side]:
|
||||
var top_a := wall.to_global(endpoints[0] + Vector3.UP * wall.unit_size + offset)
|
||||
var top_b := wall.to_global(endpoints[1] + Vector3.UP * wall.unit_size + offset)
|
||||
var center_a := wall.to_global(endpoints[0] + Vector3.UP * wall.unit_size)
|
||||
var center_b := wall.to_global(endpoints[1] + Vector3.UP * wall.unit_size)
|
||||
if top_a.distance_to(world_point) <= 0.08 or (allow_center and center_a.distance_to(world_point) <= 0.08):
|
||||
result.append({"wall": wall, "selected": root.to_local(top_a), "other": root.to_local(top_b), "thickness": wall.thickness})
|
||||
if top_b.distance_to(world_point) <= 0.08 or (allow_center and center_b.distance_to(world_point) <= 0.08):
|
||||
result.append({"wall": wall, "selected": root.to_local(top_b), "other": root.to_local(top_a), "thickness": wall.thickness})
|
||||
return result
|
||||
|
||||
static func _collect_walls(node: Node, result: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and node.piece_type == ModularBuilderPiece.PieceType.WALL:
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_walls(child, result)
|
||||
1
addons/modular_builder/modular_roof_support.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c3m5254irja4m
|
||||
32
addons/modular_builder/modular_vertex_simplifier.gd
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
static func piece_vertex(piece: ModularBuilderPiece, world_point: Vector3) -> Vector3:
|
||||
var local := piece.to_local(world_point)
|
||||
match piece.piece_type:
|
||||
ModularBuilderPiece.PieceType.FOUNDATION_SQUARE, ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
local.y = piece.thickness * 0.5 if piece.centered_origin else piece.thickness
|
||||
ModularBuilderPiece.PieceType.FLOOR:
|
||||
if not piece.custom_points.is_empty():
|
||||
local.y = piece.custom_points[0].y
|
||||
ModularBuilderPiece.PieceType.WALL, ModularBuilderPiece.PieceType.GABLE_WALL, ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT:
|
||||
var endpoints: PackedVector3Array
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.WALL:
|
||||
endpoints = piece.call("_get_centered_wall_points") if piece.center_align else piece.custom_points
|
||||
else:
|
||||
endpoints = piece.call("_get_centered_gable_base") if piece.center_align else piece.custom_points
|
||||
if endpoints.size() >= 2:
|
||||
var start := Vector2(endpoints[0].x, endpoints[0].z)
|
||||
var edge := Vector2(endpoints[1].x, endpoints[1].z) - start
|
||||
if edge.length_squared() > 0.000001:
|
||||
var fraction := clampf((Vector2(local.x, local.z) - start).dot(edge) / edge.length_squared(), 0.0, 1.0)
|
||||
var center := start + edge * fraction
|
||||
local.x = center.x
|
||||
local.z = center.y
|
||||
return piece.to_global(local)
|
||||
|
||||
static func pillar_vertex(pillar: ModularBuilderDetail, world_point: Vector3) -> Vector3:
|
||||
var local := pillar.to_local(world_point)
|
||||
local.x = 0.0
|
||||
local.z = 0.0
|
||||
return pillar.to_global(local)
|
||||
1
addons/modular_builder/modular_vertex_simplifier.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dpixdrfnwnj5m
|
||||
7
addons/modular_builder/plugin.cfg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="Modular Builder (English)"
|
||||
description="Create snap-together modular building pieces directly in the Godot editor."
|
||||
author="Michel Savadim"
|
||||
version="0.2.0"
|
||||
script="modular_builder_plugin.gd"
|
||||
162
addons/modular_builder/scopedvr_export.gd
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
## ScopedVR-Bibliothek-Export für den Modular Builder (Karte t_82d2a8dd).
|
||||
##
|
||||
## Kern-Logik des „Struktur exportieren → ScopedVR-Bibliothek"-Flows — bewusst
|
||||
## ALLES statisch und ohne Editor-UI, damit tests/test_builder_export.gd sie
|
||||
## headless prüfen kann. Der UI-Ablauf (Name-Dialog, Pipeline-Aufrufe, Status)
|
||||
## lebt in scopedvr_export_ui.gd; der Editor-Button hängt in
|
||||
## modular_builder_editor_actions.gd (_on_export_pressed → EXPORT_LIBRARY).
|
||||
##
|
||||
## Pipeline (wie Fass, docs/object_pipeline.md):
|
||||
## 1. Mesh normalisieren (Fußpunkt = y 0, XZ zentriert)
|
||||
## 2. GLB schreiben (GLTFDocument, Texturen eingebettet)
|
||||
## 3. Trimesh-Vertices (Dreiecks-Suppe, dedupliziert, 1 mm quantisiert)
|
||||
## 4. tools/objekt mesh → registry (trimesh, --budget) → client → editor
|
||||
## Die Registry bekommt "shape": "trimesh" + "vertices" + "size" (AABB) —
|
||||
## der Match-Server-Lader baut daraus ein ConcavePolygonShape3D (t_c949ebf2).
|
||||
|
||||
const QUANT := 0.001 # 1 mm — Kollision darf ~1 mm vom visuellen Mesh abweichen
|
||||
|
||||
|
||||
## Name → Objekt-ID: kleinbuchstaben, ä/ö/ü/ß aufgelöst, alles Ungültige → '_',
|
||||
## führende Ziffern weg, max. 48 Zeichen. Rückgabe "" = Name unbrauchbar.
|
||||
static func slugify(name: String) -> String:
|
||||
var s := name.strip_edges().to_lower()
|
||||
s = s.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
var out := ""
|
||||
for c in s:
|
||||
var ok := (c >= "a" and c <= "z") or (c >= "0" and c <= "9") or c == "_"
|
||||
out += c if ok else "_"
|
||||
# Führende Unterstriche/Ziffern abstreifen (IDs müssen mit Buchstabe beginnen).
|
||||
while out.length() > 0 and not (out[0] >= "a" and out[0] <= "z"):
|
||||
out = out.substr(1)
|
||||
while out.contains("__"):
|
||||
out = out.replace("__", "_")
|
||||
out = out.strip_edges()
|
||||
while out.ends_with("_"):
|
||||
out = out.substr(0, out.length() - 1)
|
||||
if out.length() > 48:
|
||||
out = out.substr(0, 48)
|
||||
if out.length() == 0 or not (out[0] >= "a" and out[0] <= "z"):
|
||||
return ""
|
||||
return out
|
||||
|
||||
|
||||
## Mesh normalisieren: Kopie, in der der Boden auf y = 0 liegt und das Haus
|
||||
## um seinen XZ-Mittelpunkt steht — der svmap-Fußpunkt-Vertrag (pos = Boden).
|
||||
## Rückgabe {mesh: ArrayMesh, offset: Vector3 (angewandter Versatz), size: Vector3 (AABB)}.
|
||||
## null im Ergebnis-Mesh = nichts zu holen (leeres Mesh).
|
||||
static func normalisiere(mesh: ArrayMesh) -> Dictionary:
|
||||
var aabb := mesh.get_aabb()
|
||||
if aabb.size.length() <= 0.0:
|
||||
return {"mesh": null, "offset": Vector3.ZERO, "size": Vector3.ZERO}
|
||||
var offset := Vector3(-aabb.get_center().x, -aabb.position.y, -aabb.get_center().z)
|
||||
var out := ArrayMesh.new()
|
||||
out.resource_name = mesh.resource_name
|
||||
for s in mesh.get_surface_count():
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays.is_empty() or not (arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array):
|
||||
continue
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
for i in verts.size():
|
||||
verts[i] = verts[i] + offset
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
var src_mat := mesh.surface_get_material(s)
|
||||
if src_mat != null:
|
||||
out.surface_set_material(out.get_surface_count() - 1, src_mat)
|
||||
return {"mesh": out, "offset": offset, "size": aabb.size}
|
||||
|
||||
|
||||
## Trimesh-Vertices (Dreiecks-Suppe, Loader-Vertrag svmap_loader._trimesh_faces):
|
||||
## je 3 Vertices ein Dreieck, aus ALLEN Oberflächen, exakte Doppel-Dreiecke
|
||||
## (quantisiert) fliegen raus — Decken/Wände aus aneinander liegenden Teilen
|
||||
## dürfen die Kollisions-Suppe nicht unnötig aufblähen.
|
||||
static func trimesh_vertices(mesh: ArrayMesh) -> PackedVector3Array:
|
||||
var seen := {}
|
||||
var out := PackedVector3Array()
|
||||
for s in mesh.get_surface_count():
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays.is_empty() or not (arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array):
|
||||
continue
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
# ARRAY_INDEX ist null (nicht PackedInt32Array), wenn das Mesh nicht
|
||||
# indiziert ist — der direkte Cast bricht sonst die ganze Funktion ab.
|
||||
var idx := PackedInt32Array()
|
||||
if arrays[Mesh.ARRAY_INDEX] is PackedInt32Array:
|
||||
idx = arrays[Mesh.ARRAY_INDEX]
|
||||
var tri_count := 0
|
||||
if not idx.is_empty():
|
||||
tri_count = idx.size() / 3
|
||||
else:
|
||||
tri_count = verts.size() / 3
|
||||
for t in tri_count:
|
||||
var a: Vector3
|
||||
var b: Vector3
|
||||
var c: Vector3
|
||||
if not idx.is_empty():
|
||||
a = verts[idx[t * 3]]
|
||||
b = verts[idx[t * 3 + 1]]
|
||||
c = verts[idx[t * 3 + 2]]
|
||||
else:
|
||||
a = verts[t * 3]
|
||||
b = verts[t * 3 + 1]
|
||||
c = verts[t * 3 + 2]
|
||||
var key := _dreieck_schluessel(a, b, c)
|
||||
if key.is_empty():
|
||||
continue # Entartet (Fläche 0 in min. einer Komponente).
|
||||
if seen.has(key):
|
||||
continue
|
||||
seen[key] = true
|
||||
out.append_array(PackedVector3Array([a, b, c]))
|
||||
return out
|
||||
|
||||
|
||||
## Sortierter Quantisierungs-Schlüssel eines Dreiecks (unabhängig von der
|
||||
## Vertex-Reihenfolge) — "" bei entarteten Dreiecken (zwei identische Punkte).
|
||||
static func _dreieck_schluessel(a: Vector3, b: Vector3, c: Vector3) -> String:
|
||||
var pts := [_quant(a), _quant(b), _quant(c)]
|
||||
if pts[0] == pts[1] or pts[1] == pts[2] or pts[0] == pts[2]:
|
||||
return ""
|
||||
pts.sort()
|
||||
return "%s|%s|%s" % [pts[0], pts[1], pts[2]]
|
||||
|
||||
|
||||
static func _quant(v: Vector3) -> String:
|
||||
return "%d,%d,%d" % [roundi(v.x / QUANT), roundi(v.y / QUANT), roundi(v.z / QUANT)]
|
||||
|
||||
|
||||
## Mesh als GLB-Bytes (Texturen eingebettet, image/png) — über GLTFDocument,
|
||||
## funktioniert headless und im Editor (Probe: tests/test_builder_export.gd).
|
||||
## Leere Bytes = Export fehlgeschlagen.
|
||||
static func glb_bytes(mesh: ArrayMesh, name_hint := "Struktur") -> PackedByteArray:
|
||||
if mesh == null:
|
||||
return PackedByteArray() # Kein Mesh → Export fehlschlagen, kein Engine-Error.
|
||||
var root := Node3D.new()
|
||||
root.name = name_hint
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.name = "BakedMesh"
|
||||
mi.mesh = mesh
|
||||
root.add_child(mi)
|
||||
var state := GLTFState.new()
|
||||
var doc := GLTFDocument.new()
|
||||
var err := doc.append_from_scene(root, state)
|
||||
if err != OK:
|
||||
return PackedByteArray()
|
||||
var bytes := doc.generate_buffer(state)
|
||||
root.free()
|
||||
return bytes
|
||||
|
||||
|
||||
## GLB-Bytes grob prüfen (glTF-Magic + Version + JSON-Chunk) — reicht für die
|
||||
## Selbst-Verifikation VOR dem Pipeline-Aufruf; die Dreiecks-Zählung macht
|
||||
## tools/objekt eh noch mal selbst.
|
||||
static func glb_gueltig(bytes: PackedByteArray) -> bool:
|
||||
if bytes.size() < 20:
|
||||
return false
|
||||
if bytes.decode_u32(0) != 0x46546C67 or bytes.decode_u32(4) != 2:
|
||||
return false
|
||||
if bytes.decode_u32(16) != 0x4E4F534A:
|
||||
return false
|
||||
return 20 + bytes.decode_u32(12) <= bytes.size()
|
||||
1
addons/modular_builder/scopedvr_export.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://du56x33pwyb4s
|
||||
250
addons/modular_builder/scopedvr_export_ui.gd
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
## UI-Ablauf des ScopedVR-Bibliothek-Exports (Karte t_82d2a8dd): Name-Dialog →
|
||||
## Mesh normalisieren → GLB + Trimesh-Vertices → tools/objekt (Match-Server-Repo)
|
||||
## → Thumbnail → Status.
|
||||
##
|
||||
## Bewusst getrennt von scopedvr_export.gd (Kern, statisch/testbar) — dieses
|
||||
## 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 BUDGET := 20000 # Export-Strukturen (match-server BUILDER_BUDGET)
|
||||
const THUMB_SIZE := 128
|
||||
|
||||
var _plugin: EditorPlugin
|
||||
var _status: Callable
|
||||
var _building: Node3D
|
||||
var _mesh: ArrayMesh
|
||||
var _dialog: AcceptDialog
|
||||
var _name_edit: LineEdit
|
||||
|
||||
|
||||
## Startet den Flow. building = gewählte Struktur, mesh = gebackenes Mesh
|
||||
## (aus modular_mesh_baker), status = Callable(label: String) für das Dock-Statusfeld.
|
||||
func starten(plugin: EditorPlugin, building: Node3D, mesh: ArrayMesh, status: Callable) -> void:
|
||||
_plugin = plugin
|
||||
_building = building
|
||||
_mesh = mesh
|
||||
_status = status
|
||||
if _dialog != null:
|
||||
_dialog.queue_free()
|
||||
_dialog = AcceptDialog.new()
|
||||
_dialog.title = "Struktur exportieren — ScopedVR-Bibliothek"
|
||||
_dialog.ok_button_text = "Exportieren"
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.custom_minimum_size = Vector2(380, 0)
|
||||
var hinweis := Label.new()
|
||||
hinweis.text = "Name der Struktur (wird zur Objekt-ID: kleine Buchstaben,\nZahlen, Unterstrich — z. B. „Bauernhaus Rot\" → haus_bauernhaus_rot... \nrichtig: „Bauernhaus rot\" → bauernhaus_rot).\n\nDer Export durchläuft die Objekt-Pipeline (Mesh, Registry,\nClient, Editor) — der Client-Import dauert einen Moment."
|
||||
hinweis.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
vbox.add_child(hinweis)
|
||||
_name_edit = LineEdit.new()
|
||||
_name_edit.text = str(building.name)
|
||||
vbox.add_child(_name_edit)
|
||||
_dialog.add_child(vbox)
|
||||
_dialog.confirmed.connect(_on_name_ok)
|
||||
plugin.add_child(_dialog)
|
||||
_name_edit.text_submitted.connect(func(_t: String) -> void:
|
||||
_dialog.hide()
|
||||
_on_name_ok())
|
||||
_name_edit.call_deferred("grab_focus")
|
||||
_dialog.popup_centered()
|
||||
|
||||
|
||||
func _on_name_ok() -> void:
|
||||
var name_roh := _name_edit.text
|
||||
var oid := KERN.slugify(name_roh)
|
||||
if oid.is_empty():
|
||||
_status.call("FEHLER: Aus dem Namen „%s\" wird keine gültige Objekt-ID — bitte Buchstaben verwenden." % name_roh)
|
||||
return
|
||||
_status.call("Export läuft: „%s\" → %s (Pipeline: Mesh → Registry → Client → Editor) ..." % [name_roh, oid])
|
||||
# Einen Frame abwarten, damit der Status sichtbar wird, bevor OS.execute blockiert.
|
||||
await _plugin.get_tree().process_frame
|
||||
await _export(oid, name_roh)
|
||||
|
||||
|
||||
func _export(oid: String, name_roh: String) -> void:
|
||||
var server_dir := _match_server_dir()
|
||||
if server_dir.is_empty():
|
||||
_status.call("FEHLER: Match-Server-Repo nicht gefunden — erwartet ../scopedvr-match-server neben dem Map-Editor (oder SCOPEDVR_MATCH_SERVER_DIR).")
|
||||
return
|
||||
var objekt_tool := server_dir + "/tools/objekt"
|
||||
|
||||
# 1) Normalisieren (Fußpunkt y=0, XZ zentriert).
|
||||
var norm: Dictionary = KERN.normalisiere(_mesh)
|
||||
var mesh: ArrayMesh = norm.get("mesh")
|
||||
if mesh == null:
|
||||
_status.call("FEHLER: Das gebackene Mesh ist leer — nichts zu exportieren.")
|
||||
return
|
||||
var size: Vector3 = norm["size"]
|
||||
var faces := KERN.trimesh_vertices(mesh)
|
||||
if faces.is_empty():
|
||||
_status.call("FEHLER: Keine Kollisions-Dreiecke im gebackenen Mesh.")
|
||||
return
|
||||
|
||||
# 2) GLB + Vertices temporär schreiben.
|
||||
var tmp_dir := _tmp_dir()
|
||||
var glb_path := tmp_dir + "/scopedvr_export_%s.glb" % oid
|
||||
var verts_path := tmp_dir + "/scopedvr_export_%s.json" % oid
|
||||
var glb := KERN.glb_bytes(mesh, oid)
|
||||
if glb.is_empty() or not KERN.glb_gueltig(glb):
|
||||
_status.call("FEHLER: GLB-Export fehlgeschlagen (GLTFDocument).")
|
||||
return
|
||||
var gf := FileAccess.open(glb_path, FileAccess.WRITE)
|
||||
if gf == null:
|
||||
_status.call("FEHLER: Konnte %s nicht schreiben." % glb_path)
|
||||
return
|
||||
gf.store_buffer(glb)
|
||||
gf.close()
|
||||
var vertices_liste: Array = []
|
||||
for v in faces:
|
||||
vertices_liste.append([_r(v.x), _r(v.y), _r(v.z)])
|
||||
var verts_json := JSON.stringify({
|
||||
"vertices": vertices_liste,
|
||||
"size": [_r(size.x), _r(size.y), _r(size.z)],
|
||||
})
|
||||
var vf := FileAccess.open(verts_path, FileAccess.WRITE)
|
||||
if vf == null:
|
||||
_status.call("FEHLER: Konnte %s nicht schreiben." % verts_path)
|
||||
return
|
||||
vf.store_string(verts_json)
|
||||
vf.close()
|
||||
|
||||
# 3) Pipeline-Schritte 1–2 (Mesh + Registry). Fehler = sofort raus, klare Meldung.
|
||||
var schritt1 := _lauf(objekt_tool, ["mesh", oid, glb_path, "--budget", str(BUDGET)])
|
||||
if schritt1 != "":
|
||||
_status.call(schritt1)
|
||||
return
|
||||
var schritt2 := _lauf(objekt_tool, ["registry", oid, name_roh.strip_edges(), "trimesh", "--vertices", verts_path, "--budget", str(BUDGET)])
|
||||
if schritt2 != "":
|
||||
_status.call(schritt2)
|
||||
return
|
||||
|
||||
# 4) Client-Sync (kopiert GLB + Registry in den Client, Import-Probe).
|
||||
_status.call("Export läuft: Client-Import (dauert einen Moment) ...")
|
||||
var schritt3 := _lauf(objekt_tool, ["client", oid])
|
||||
if schritt3 != "":
|
||||
_status.call(schritt3)
|
||||
return
|
||||
|
||||
# 5) Thumbnail AUS DEM ECHTEN MESH rendern (bevor der Editor-Sync kommt —
|
||||
# gen_thumbs überspringt Trimesh-Objekte mit vorhandener Datei).
|
||||
var editor_dir := _editor_dir()
|
||||
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")
|
||||
|
||||
# 6) Editor-Sync (Registry-Kopie; vorhandene Thumbnails bleiben stehen).
|
||||
var schritt4 := _lauf(objekt_tool, ["editor", oid])
|
||||
if schritt4 != "":
|
||||
_status.call(schritt4)
|
||||
return
|
||||
|
||||
var vis_tris := 0
|
||||
var tris := int(faces.size() / 3)
|
||||
for s in mesh.get_surface_count():
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays.is_empty():
|
||||
continue
|
||||
# ARRAY_INDEX ist null bei nicht-indizierten Oberflächen (kein Cast!).
|
||||
var idx := PackedInt32Array()
|
||||
if arrays[Mesh.ARRAY_INDEX] is PackedInt32Array:
|
||||
idx = arrays[Mesh.ARRAY_INDEX]
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
vis_tris += (idx.size() / 3) if not idx.is_empty() else (verts.size() / 3)
|
||||
_status.call("OK: „%s\" als '%s' exportiert (poly %d, Trimesh %d Dreiecke, Budget %d). Objekt liegt in Bibliothek + Client — Einchecken nicht vergessen (git add in 3 Repos)." % [name_roh, oid, vis_tris, tris, BUDGET])
|
||||
_aufraeumen([glb_path, verts_path])
|
||||
|
||||
|
||||
func _aufraeumen(pfade: Array) -> void:
|
||||
for p in pfade:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
|
||||
|
||||
func _lauf(objekt_tool: String, args: PackedStringArray) -> String:
|
||||
var out := []
|
||||
var code: int = OS.execute(objekt_tool, args, out, true)
|
||||
var text := "\n".join(out)
|
||||
if code == 0:
|
||||
return ""
|
||||
# Die FEHLER-Zeile des Werkzeugs durchreichen (letzte gewinnt).
|
||||
var fehler := "Pipeline fehlgeschlagen (Exit %d)." % code
|
||||
for zeile in text.split("\n"):
|
||||
if str(zeile).begins_with("FEHLER: "):
|
||||
fehler = str(zeile)
|
||||
elif str(zeile).begins_with("FEHLER:"):
|
||||
fehler = str(zeile)
|
||||
return fehler
|
||||
|
||||
|
||||
func _match_server_dir() -> String:
|
||||
var env := OS.get_environment("SCOPEDVR_MATCH_SERVER_DIR")
|
||||
if not env.is_empty() and DirAccess.dir_exists_absolute(env):
|
||||
return env.rstrip("/")
|
||||
var root := ProjectSettings.globalize_path("res://").rstrip("/")
|
||||
var cand := root.path_join("../scopedvr-match-server")
|
||||
if DirAccess.dir_exists_absolute(cand):
|
||||
return cand
|
||||
return ""
|
||||
|
||||
|
||||
func _editor_dir() -> String:
|
||||
var env := OS.get_environment("SCOPEDVR_EDITOR_DIR")
|
||||
if not env.is_empty() and DirAccess.dir_exists_absolute(env):
|
||||
return env.rstrip("/")
|
||||
return ProjectSettings.globalize_path("res://").rstrip("/")
|
||||
|
||||
|
||||
func _tmp_dir() -> String:
|
||||
var t := OS.get_environment("TMPDIR")
|
||||
if t.is_empty():
|
||||
t = "/tmp"
|
||||
return t.rstrip("/")
|
||||
|
||||
|
||||
static func _r(v: float) -> float:
|
||||
return snappedf(v, 0.0001)
|
||||
|
||||
|
||||
## Thumbnail aus dem Mesh rendern (Editor-Rendering vorausgesetzt) —
|
||||
## gleicher Stil wie tools/gen_thumbs.gd (128 px, dunkler Hintergrund).
|
||||
func _thumbnail_bilden(mesh: ArrayMesh) -> Image:
|
||||
var vp := SubViewport.new()
|
||||
vp.size = Vector2i(THUMB_SIZE, THUMB_SIZE)
|
||||
vp.own_world_3d = true
|
||||
vp.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
_plugin.add_child(vp)
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_COLOR
|
||||
env.background_color = Color(0.16, 0.18, 0.21)
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
env.ambient_light_color = Color(1, 1, 1)
|
||||
env.ambient_light_energy = 0.7
|
||||
var we := WorldEnvironment.new()
|
||||
we.environment = env
|
||||
vp.add_child(we)
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.rotation_degrees = Vector3(-55.0, 35.0, 0.0)
|
||||
sun.light_energy = 1.1
|
||||
vp.add_child(sun)
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
vp.add_child(mi)
|
||||
var cam := Camera3D.new()
|
||||
cam.current = true
|
||||
vp.add_child(cam)
|
||||
var aabb := mesh.get_aabb()
|
||||
var center := aabb.get_center()
|
||||
var radius := aabb.size.length() * 0.5
|
||||
var dir := Vector3(1.0, 0.65, 1.0).normalized()
|
||||
cam.position = center + dir * (radius * 2.3 + 0.3)
|
||||
cam.look_at(center, Vector3.UP)
|
||||
# Ein Frame Rendering abwarten, dann auslesen und aufräumen.
|
||||
await _plugin.get_tree().process_frame
|
||||
await _plugin.get_tree().process_frame
|
||||
var img := vp.get_texture().get_image()
|
||||
vp.queue_free()
|
||||
return img
|
||||
1
addons/modular_builder/scopedvr_export_ui.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dx8fw7ovdhua0
|
||||
0
assets/textures/terrain/.gdignore
Normal file
BIN
assets/textures/terrain/terrain_arrays.res
Normal file
1
core
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 5e98535a0afc83af4e4017806686a76af4913afd
|
||||
76
docs/bauen.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# 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.
|
||||
51
docs/server-sektion.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# 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
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
# 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.
|
||||
29
export_presets.cfg
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[preset.0]
|
||||
|
||||
name="Linux"
|
||||
platform="Linux"
|
||||
runnable=true
|
||||
advanced_options=false
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter="models/*"
|
||||
export_path="export/MapEditor.x86_64"
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.0.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=0
|
||||
binary_format/embed_pck=false
|
||||
texture_format/s3tc_bptc=true
|
||||
texture_format/etc2_astc=false
|
||||
binary_format/architecture="x86_64"
|
||||
ssh_remote_deploy/enabled=false
|
||||