Compare commits

...

156 commits

Author SHA1 Message Date
slyphax
313500b988 Kern-Stand nachgezogen, Test für Terrain in Stücken, Loadout-Sync-Pfad
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:27:35 +01:00
slyphax
8f01775a92 Editor-Code liegt jetzt komplett im Kern (res://core), PC-App startet core/scenes/main.tscn; Addon nutzt Bau-Geometrie aus dem Kern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 19:34:05 +01:00
slyphax
295f79011f Werkzeuge hängen nicht mehr an der Maus: PointerEvent, MousePointer, PointerRouter; Test Maus gegen Zeiger
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 19:26:17 +01:00
slyphax
949d73dfdd Map-Kern ausgelagert: scopedvr-map-core als Submodul unter res://core, Update holt ihn mit; Test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:37:09 +01:00
slyphax
e8ab954876 Editor: Blickrichtung für Spawn-Punkte und Conquest-Zonen (yaw_deg, optional): Pfeil im Bild, Spitze ziehen oder Zahl im Panel, Standard-Knopf, Undo, Symmetrie spiegelt mit, Playtest nutzt sie; Prüfung; Test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:00:02 +01:00
slyphax
ff1c0e345e Datei-Menü: Hand-Haltung der Waffen startet das Hand-Haltungs-Tool aus dem Client-Ordner (importiert nach neuem Client-Stand im Hintergrund); Test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:02:36 +01:00
slyphax
91c66a0b98 Loadout-Bereich: Granaten und Minen pro Karte festlegen (V18, Undo, auch im Spieler-Modus, gilt für Bots); Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkB4i3mQaxfg6aEncovy2Q
2026-10-31 01:55:45 +01:00
slyphax
330365e59b Loadout-Auswahl: Holo- und Schrägvisier klarer benannt (Ring mit Punkt, seitlich) neben dem Rotpunktvisier
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:52:21 +01:00
slyphax
9589703f7c Loadout-Auswahl: Rotpunktvisier und andere Visiere mit deutschem Namen statt Teil-ID; Tests für Auswahl, Speichern und V18
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:00:32 +01:00
slyphax
b04a953190 Tab Server: Server neu starten mit Bestätigung und Warten auf den neuen Prozess, Bot-Warnung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:08:06 +01:00
slyphax
48db200d3e Server-Sektion im Editor: Client der Admin-API (Server speichern mit verschluesseltem Token, Uebersicht, Einstellungen, Spieler kicken/sperren, Map laden/loeschen, Rotation, Logs) und 'Aktuelle Map auf Server pushen' mit verstaendlicher 422-Fehlerliste; SvmapIO.to_signed_text; Tests inkl. E2E gegen echten Server
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 00:04:11 +01:00
slyphax
92e2c688c1 Spielmodi im Editor: Checkpoints → Conquest (Alt-Maps werden beim Laden umgestellt), King of the Hill mit Hill-Liste + Reihenfolge, Deathmatch, eigene Spawns je Modus (mode_spawns, Spawn-Satz-Auswahl, Undo), V21–V24
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 20:38:54 +01:00
slyphax
f42e133bae Spielmodus-Auswahl beim Neu-Erstellen + Checkpoint-Zonen im Map-Tab (V21/V22, Undo, 3D-Anzeige)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 09:21:13 +01:00
slyphax
bb9eb71770 Map-Loadout: Loadout-Bereich im Map-Tab (Spieler-Loadout oder festes Map-Loadout mit Aufsaetzen), V18, Undo/Redo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 09:10:16 +01:00
slyphax
8345903ce7 Container: offene Varianten (eine Tür offen / beide Türen offen / ohne Türen), 8 Farben, Innenseiten mit Paletten-Textur + Trimesh-Kollision
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 23:42:09 +01:00
slyphax
902181500a Drag&Drop aus der Bibliothek: Ablege-Fläche über der 3D-Ansicht (echter Maus-Drag getestet), Drag-Vorschau, Doku
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 22:50:37 +01:00
slyphax
bcbd416960 Bauen: Geschoss-Wahl (Decken mitten im Raum), 1 cm Wand-Überstand gegen Z-Fighting, Dach aus Einzelteilen, größeres Einstellungen-Fenster
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 22:47:15 +01:00
slyphax
5e32c771fc Revert "Bauen: begehbare Vorlagen (Kaserne, Wachhaus, Lagerraum) per Klick, ein Undo-Schritt"
This reverts commit 961e8c6ff6.
2026-10-25 22:37:24 +01:00
slyphax
961e8c6ff6 Bauen: begehbare Vorlagen (Kaserne, Wachhaus, Lagerraum) per Klick, ein Undo-Schritt
Pack-Kaserne/-Checkpoint/-Container sind innen geschlossen; die Vorlagen sind
begehbare Gebäude aus bau_*-Teilen. Test beweist per Strahl durch die echte Kollision.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 22:00:49 +01:00
slyphax
075735f426 Laden: gelöschte Objekte (Pilze) beim Öffnen ersetzen/überspringen statt Abbruch (test.svmap wieder ladbar)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 21:53:40 +01:00
slyphax
fa2feee780 Thumbnails für die großen Dächer/Giebelwände (4 m, 6 m)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:32:53 +01:00
slyphax
3f0d0f3f33 Map-Editor: UI aufgeräumt, Auswahl-Sync, Drag&Drop, Bauen (Wände bündig, Geschosshöhe, große Dächer)
- Tabs Auswahl/Prüfen/Test-Start/Einstellungen/Symmetrie weg; Spawn-Zonen, Test-Start, Symmetrie im Map-Tab; Einstellungen-Fenster (Gitter, Höhenlinien, Symmetrie-Modus, UI-Größe, Raster-Snap, Gizmo-Schritt, Karte prüfen)
- Bauen-Tab nur Bauteile; Objekt-Werkzeuge entfernt
- Klick in der Map-Inhalt-Liste wählt in der Szene aus (SELECT_MULTI feuert nur multi_selected)
- Objekt-Kachel per Ziehen in die Szene setzen
- Wände bündig in der Zelle, gleiche Geschosshöhe mit/ohne Decke, Dächer/Giebelwände 4 m und 6 m

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:32:32 +01:00
slyphax
ab9a798343 Thumb-Importdateien der bau_*-Bauteile nachgetragen 2026-10-25 16:32:21 +01:00
slyphax
491a47d3f1 Editor-Bibliothek auf das Military-Desert-Pack umgestellt
- 423 Objekte in 19 deutschen Kategorien (Panzer, Radfahrzeuge, Flugzeuge, Gebaeude,
  Zelte, Tuerme, Mauern, Sperren, Kisten, Technik, Felsen, Boden, Decals ...);
  Gruppen "Bauwerke" und "Kisten & Barrieren" samt Test-Asset-Namenslisten entfernt
- LibraryBrowser: Gruppen/Namen aus dem Manifest (kategorie, name_de), unbekannte
  Kategorien bekommen automatisch eine eigene Gruppe
- Thumbs neu, library/ synchronisiert (inkl. neuer Kollision)
- Neue Identitaetsprobe: Kollision ALLER Objekte Editor = Client = Server
- Tests auf Pack-Objekte umgestellt

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-25 16:31:28 +01:00
slyphax
a76c3ecae7 Bauen-Modus neu: Bauteile als normale Objekte (bau_*), Snap an Kante/Zelle/Ebene, Gebäude = Gruppe
- BauKatalog: 12 Teile (Fundament, Boden, Wände mit Tür/Fenster/Scharte, Brüstung, Pfeiler, Giebel, Dächer, Treppe) auf Basis der CC0-Geometrie des Modular Builder 0.2.0, Wandhöhe 3 m
- BuildSnap: Wände auf Zellkanten, Böden in Zellmitte, Höhe aus Ebenen statt Klickhöhe, Ersetzen (Tür in Wand)
- Gebäude kopieren/auswählen/löschen im Bauen-Tab, Kopien bekommen eigene Gruppe
- Kollision serverkonsistent über Trimesh-Dateien (Türen/Fenster echte Löcher), tools/gen_bauteile.gd erzeugt GLB, Kollision, Registry-Fragment
- Alte gebaeude-Sektion wird beim Laden migriert, altes Bauen-Werkzeug entfernt, Resize behält Gruppennamen
- Tests: test_bauen (258 Checks), Playtest läuft Treppe/Tür

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 16:30:15 +01:00
slyphax
fcc7f16f1c Skybox-Auswahl im Map-Panel: 13 neue Himmel, Vorschau, Undo/Redo, V17 über Katalog
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 15:30:38 +01:00
slyphax
e3a6b2ad21 UI-Nachbesserung: Statuszeile eine Zeile, Ring-Drehen (Variablen-Schatten im Gizmo), Tab-Klick aktiviert Werkzeug, Panels offen 2026-10-25 11:20:24 +01:00
slyphax
35a047cc2a Ringe dicker, neuer Test in run_tests 2026-10-25 10:34:37 +01:00
slyphax
93cdcb0cd4 3D-Werkzeugleiste, Dreh-/Kipp-Gizmo um alle Achsen mit Undo, Test 2026-10-25 10:33:37 +01:00
slyphax
59c932dc35 UI: Info-Zeile kompakt, kurze Viewport-Hinweise 2026-10-25 10:32:01 +01:00
slyphax
3094395444 UI: Erklärtexte raus, Kontrast-Fix (Theme-Farben), größere Bibliothek-Kacheln 2026-10-25 10:31:37 +01:00
slyphax
3d083648d8 UI: Terrain/Malen/Bauen als eigene Tabs, Material-Vorschau, neue Hilfe nach Werkzeugen, Hilfe-Knopf 2026-10-25 10:30:06 +01:00
slyphax
8cde37b85c Updater: Klon vor/auf anderem Branch ist kein Update mehr, Pull gezielt origin main 2026-10-25 10:25:55 +01:00
slyphax
26bcbe0cdd Terrain-Pinsel: Kante-Regler (Falloff), Einebnen, Rampe, Druck-Stärke, Mal-Slot-Ersatz
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 09:52:03 +01:00
slyphax
b1d8f7626d Terrain-Shader: detail_kontrast (1,3) gegen blasse Büschel nach Hex-Tiling
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 09:50:20 +01:00
slyphax
a1f386f5e6 Terrain-Shader: Hex-Tiling + Makro-Variation statt Raster-Kachel (Büschel nicht mehr im Raster, keine Diagonalstreifen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 09:42:40 +01:00
slyphax
99a06c6cb8 Boden-Texturen: 3-m-Kachel + Anti-Wiederholung, „Gras“ heißt jetzt „Steppe“
Sly 25.10.: Texturen wiederholen sich zu krass, „Gras“ sieht nicht nach Gras aus.
- Kachel 3 m statt 1 m; zweite Abtastung gedreht + 2,3× größer, großflächiges
  Rauschen mischt beide, zweites Rauschen variiert die Helligkeit (Spiegel Client).
- Material 0 (gras_steppe) heißt im Editor „Steppe“ (Format unverändert);
  Hilfetexte nachgezogen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 01:05:07 +02:00
slyphax
9f2a32df70 Terrain-Shader: Ganzzahl-Hash statt sin()-Hash im Kanten-Warp (gerade harte Kanten weg)
Sly 25.10.: nah gezoomt gerade, rechteckige harte Kanten in den Verläufen.
Ursache: sin() großer Argumente ist auf der GPU ungenau, die Warp-Noise
bekam rasterförmige Sprünge. Mit malentest.svmap reproduziert (ohne Warp
weg, mit Ganzzahl-Hash weg, Warp bleibt organisch).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 00:58:08 +02:00
slyphax
5a70824dd5 Malen mit Symmetrie: Überlappung beider Seiten nimmt den stärkeren Anteil (stetig statt Mittelsenkrechten-Kante)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 00:55:18 +02:00
slyphax
7f06e28472 Malen mit Symmetrie: nur wirklich bemalte Punkte spiegeln (harte gerade Kanten weg)
Sly 25.10.: „teilweise sehr harte Kanten in den Übergängen“. Ursache: bei
aktiver Symmetrie kopierte jeder Mal-Schritt das GANZE Snapshot-Quadrat um
den Pinsel auf die Gegenseite — dort vorhandene weiche Verläufe wurden an
den Quadratkanten gerade abgeschnitten. Jetzt werden nur Punkte gespiegelt,
die der Schritt verändert hat; überlappen beide Seiten, gewinnt wie bisher
der näher am Strichpunkt liegende Wert.
Test: test_terrain_mirror 109/109 (neuer Fall schlägt mit altem Code fehl).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 00:50:09 +02:00
slyphax
4e973795f6 Malen: echter weicher Übergang am Pinselrand (Misch-Schicht statt Streuung)
Sly 25.10.: „weich“ soll ein Textur-Verlauf am Pinselrand sein, nicht fleckig;
gerade Strichkanten wirkten weiter hart (1-m-Übergang).
- Format (optional, alte Karten gültig): materials_b_r8_b64 (zweites Material
  je Punkt) + materials_w_r8_b64 (Anteil 0–255); ohne Mischung fallen die
  Felder weg. V15 prüft Länge/IDs/Paarigkeit.
- TerrainField.apply_material_brush_mix: innen voll, außen Smoothstep-Anteil;
  nur verstärken, nie abschwächen; Gras drübermalen mischt zurück.
- Shader: Top-2 aus 4 Nachbarpunkten × (a, b) mit Anteilen → nur noch 2
  Albedo-Fetches; Warp bleibt.
- Pinsel/Live-Vorschau, Spiegeln, Symmetrie-Analyse/-Anwenden, Resize,
  Anheben, Undo (terrain-Snapshot) tragen die Misch-Schicht mit.
- Streu-Rand (vorheriger Commit) entfernt.
- Tests: test_mal_fein 30/30 (Verlauf fällt weich, nie abschwächen,
  zurückmischen, Kodierung), Editor-Suite unverändert grün bis auf die
  bekannten Umgebungs-Tests; Kette Editor → Server → Client per
  run_map_download_e2e 15/15 mit gemischter Karte.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 00:43:26 +02:00
slyphax
4a7b99ee3e Malen: Regler „Kante hart ↔ weich“ + Pinsel ab 1 m
Sly 25.10.: harte Kanten an Strichrändern, Pinsel auf 1 m, Regler für die
Kantenschärfe.
- apply_material_brush(…, weichheit): im äußeren Anteil des Radius wird
  gestreut ausgedünnt (fester Hash je Punkt → deterministisch, Nachmalen
  füllt nicht auf); der Shader macht daraus einen ausfransenden Übergang.
  Kein Formatwechsel (ein Material je Punkt bleibt).
- Panel: Regler 0–100 % (Default 40 %), nur im Malen-Modus; Pinselgröße ab 1 m.
- test_mal_fein 26/26 (Dichte fällt nach außen, deterministisch, 1-m-Pinsel),
  test_terrain_mirror an die Tool-Weichheit angepasst.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 00:30:16 +02:00
slyphax
40ca5ab6ba Terrain-Malen: weiche, organische Kanten statt 1-m-Pixelblöcke
Sly 25.10.: „immer noch harte Kanten beim Malen“. Ursache: die Splat-Textur
hatte beim feinen 1-m-Raster genau einen Texel je Materialpunkt und wird
nearest gesampelt → jede Zelle ein harter Block.

- Shader liest die Material-ID-Karte (R8, ein Texel je Punkt, texelFetch)
  und mischt PRO PIXEL bilinear über die 4 Nachbarpunkte.
- Leichter Domain-Warp (Value-Noise in Rasterzellen, eine Oktave) verwackelt
  die Abtaststelle → keine Rastertreppen, natürliche Kante.
- Malen aktualisiert nur noch die Bytes der Karte (kein Splat-Neuberechnen).
- TerrainArrays.material_image (Spiegel-Datei, identisch im Client).
- Render-Probe tests/probe_mal_weich_render.gd (vorher/nachher geprüft).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 00:23:18 +02:00
slyphax
69668eb250 Feines Material-Raster: Malen auf ~1 m statt Höhenraster (bei 1024 m vorher 8 m)
Sly: „feiner malen“. Die Material-Schicht bekommt ein eigenes Raster
(terrain.material_resolution, optional; Default = resolution → alte Karten
unverändert gültig): ~1 m pro Punkt, auf dem Höhenraster ausgerichtet,
max. 1025 (1024 m → 1025, 256 m → 257, 2048 m → 1025 = 2 m).

- SvmapIO: material_resolution / material_resolution_for / resample /
  upgrade_material_layer / material_at_height_point; V15 prüft Raster + Länge.
- Neue Karten starten fein; beim Öffnen werden alte Karten optisch identisch
  angehoben (Datei ändert sich erst beim Speichern); Resize rechnet die
  Schicht auf ihrem eigenen Raster an der Weltposition um.
- Pinsel, Spiegeln (Symmetrie live + „Symmetrie anwenden“) und Live-Splat
  arbeiten auf dem Material-Raster; Vertex-Farben lesen den passenden Punkt.
- Malen-Hinweis zeigt die echte feinste Einheit.
- Tests: test_mal_fein 22/22 (Raster, Anheben, 2-m-Pinsel ≈ 13 Punkte,
  V15, Roundtrip, Resize); test_terrain/test_terrain_mirror/
  test_symmetry_onclick auf das neue Raster nachgezogen; Identitäts-Runner OK.
  Kette Editor → Server (Prüfsumme) → Client per run_map_download_e2e 15/15.

Format-Spiegel: Match-Server svmap_loader (V15) + Client svmap_terrain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 00:07:25 +02:00
slyphax
66df3bdc1e Editor: Klick wählt das exakt getroffene Objekt (Mauer statt Nachbar-Gebäude) + Malen-Hinweis statt Stärke-Regler
- _pick_object: Kugel-Näherung nur noch Vorfilter, entschieden wird über den
  echten Mesh-Treffer (SelectionMath.ray_mesh_distance, Faces gecacht).
  Ohne Mesh-Treffer bleibt die alte Kugel-Wahl als Fallback.
- Terrain-Panel: im Malen-Modus ist der Stärke-Regler weg (Malen setzt das
  Material zellenweise, Stärke hatte nie eine Wirkung); stattdessen steht die
  feinste Mal-Einheit der Karte da (size / (resolution - 1)).
- Tests: test_klick_exakt 6/6 (reproduziert Gebäude-Kugel vor Mauer),
  test_mal_hinweis 4/4. Vorhandene Suiten unverändert (4 schon vorher rote
  Umgebungs-Tests: assets_panel, identitaet_client, model_sources,
  playtest_identitaet).

Ersetzt die nie gepushten Arbeitskopie-Änderungen der Karten t_12feca66 /
t_c487a430 / t_271a77bd (Material-IDs per lerp gemischt, Sub-Zellen ohne
Client/Server-Unterstützung).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 23:27:48 +02:00
Hermes (scopedvr-dev)
1d8763b881 Karte t_5c94d098 Terrain-Blending v2: Splat-Texel-Dichte folgt der KARTENGROESSE
Gleiche Ursache wie im Client (BEWEISE.md in work/terrain_blending_v2_t_5c94d098_bilder/):
splat_side_for(129) = 512 px = 2 m/Texel auf der 1024-m-Standardmap (8 m/Zelle)
-> Gewichtsrampe in 2-m-Stufen, sichtbar als Baender/Stufen an der Materialgrenze.

Aenderung Editor:
- library/terrain_arrays.gd (Spiegel-Datei, byte-identisch zum Client):
  splat_side_for(res, map_size_m = 0.0) = MAX(4 Texel/Zelle, 1 m/Texel aus der
  Karten-Kantenlaenge), Zweierpotenz, geklemmt [256, 1024]; build_splat_bytes
  ohne Allokation je Texel + Schnellpfad (gleiche ID in allen vier Nachbarzellen)
  -> unveraenderte Bytes, deutlich schneller.
- src/viewport/terrain_mesh.gd: _build_splat bekommt size_x/size_z und gibt sie an
  splat_side_for; der Live-Sub-Rect-Pfad greift nur noch, wenn auch die
  Kantengroesse stimmt (Size-Aenderung ohne res-Aenderung baut die Splat neu).
- tests/test_terrain_splat.gd (Spiegel, identisch im Client): +6 Kartenmass-Checks
  (31/31).

Suiten (frischer Forgejo-Klon): test_terrain_splat 31/31, test_terrain 155/155,
test_terrain_mirror 106/106.
2026-10-22 19:37:30 +00:00
Hermes (scopedvr-dev)
04c6c67ba8 Karte t_7a0a6b79 Terrain-Qualitaet: Splat-Top-2-Blend (weiche Uebergaenge), Live-Sub-Rect beim Malen, Ausbrennen-Fix (Shading-Normalisierung) 2026-10-21 22:12:04 +00:00
Hermes (scopedvr-dev)
f5a4ab54d0 Wasser pro Map (t_28fdef03): V20-Validator, Panel-Regler, Vorschau-Renderer
- src/svmap/svmap_io.gd: _validate_wasser (V20) — optional, endliche Zahl,
  Terrain-Hoehenbereich +/-10 m; identisch zum Match-Server
- src/ui/panels/map_panel.gd: Regler 'Wasserhoehe' mit Sofort-Rebuild;
  ganz links (-10) = Feld ENTFERNEN (Legacy-sauber, keine Migration)
- src/viewport/map_view_builder.gd: _build_wasser nach _build_terrain
  (Senken fuellen sich); Legacy ohne Feld = kein Wasser-Node
- src/viewport/terrain_mesh.gd: Matsch-Ring in relief_color (Ufer-Faktor,
  dieselbe Formel wie der Client)
- library/svmap_wasser.gd + src/viewport/wasser.gdshader: Spiegel-Modul,
  byteidentisch zum Client (Identitaets-Vertrag der Vorschau)
- tests/test_wasser.gd: 33 Checks (W1 V20, W2 Panel, W3 Vorschau + Matsch-Mathe, W4 Spiegel)

Verifiziert: Editor-Suite 33/33, test_editor_svmap 31, test_terrain 155,
test_gui_smoke 92, test_skybox 23 — alle OK
2026-10-21 06:13:59 +00:00
Hermes (scopedvr-dev)
8468b89456 Terrain-Textur-Panel + Editor-Spiegel (t_ab408127): Terrain-Panel zeigt die 20 CC0-Materialien; terrain_mesh bindet terrain_arrays.res (Spiegel aus Client via tools/sync_terrain_tex.sh, Ordner OHNE .gdignore — ResourceLoader braucht den Index); TerrainArrays-Klasse spiegelgleich in library/ (identischer Pfad wie im Client — .res-Script-Pfad löst in beiden Repos auf); Kontur-Shader bekommt Textur-Sampling + Höhenlinien; V15-Test auf neue Palette (ID 20 abgelehnt, 3 gültig); test_terrain 155/155 2026-10-21 00:48:41 +00:00
Hermes (scopedvr-dev)
338e7152a0 ALLES Trimesh (t_ff7fc919): Registry + 250 Kollisions-Suppen synchron; KolliderBau.hindernis_aabb aus der Suppe (Spiegel Client/Server); Identitäts-Probe um Trimesh-Fälle erweitert (12 IDENT-Zeilen, ALL OK) 2026-10-20 22:58:42 +00:00
Hermes (scopedvr-dev)
42a205b6ff Platzierungsfreiheit (t_e166e656): Scale-Zeile entfernt (Groesse fix), Kippen X/Z (rot-Feld, Strg+R/Alt+R, Ghost + Platzierung), Versenken unter Terrain erlaubt + Tiefe-Anzeige, Spiegel-Mathe 3 Achsen, KolliderBau rot + hindernis_aabb, Suiten + Identitaets-Runner 2026-10-20 22:32:10 +00:00
Hermes (scopedvr-dev)
78d311c75f Atmosphaere & Skybox (t_f72913d4): Render-Probe Editor-Vorschau (Panorama vs. Prozedural, xvfb/opengl3) 2026-10-20 22:11:30 +00:00
Hermes (scopedvr-dev)
0505463ace Atmosphaere & Skybox (t_f72913d4): Map-Option „Himmel: Pack-Skybox“ (weather_sky=sky_panorama, checksum-neutral), Vorschau = 1:1-Spiegel Client-Wetter (ACES/Fog/sun_deg/Panorama via Nachbar-Klon), V17-Validator; Suite test_skybox 23/23 2026-10-20 22:10:36 +00:00
Hermes (scopedvr-dev)
35a1d80d3e svmap: Prüfsummen-Repair-Dialog „Trotzdem laden“ (t_9814f18f) 2026-10-20 22:05:53 +00:00
Hermes (scopedvr-dev)
65b33a207a svmap: Prüfsummen-Schema v2 (t_9814f18f)
- sign_map schreibt schema=v2, Hash über die quantisierte Payload
  (12 signifikante Stellen, Snap über die Godot-Textform → idempotent)
- save_to_file quantisiert VOR dem Schreiben → Dateien ohne Rausch-Floats
- Laden: schema=v2 → v2-Hash; fehlt → Legacy-Fallback (Bestandskarten)
- FirstFight-Fall: Python-repr-Floats (15+ sig) hashen formatunabhängig
- Beweise: tests/test_svmap_checksum_v2.gd (18 Checks, ALL OK);
  echte FirstFight.svmap lädt nach v2-Resave, cross-validiert im Server
2026-10-20 21:33:28 +00:00
Hermes (scopedvr-dev)
7a1b5037e7 Playtest: Splitter-Lagen bleiben erhalten (Sly-Befund 20.10.)
Beim Start des Test-Modus versteckt der Editor die Panels — Godot
klappt dabei die Splitter zusammen (Lage springt z. B. [-320,0] →
[160]), beim Wiedereinblenden baut er die Trenner mit Defaults wieder
auf → die fein gezogenen Seitenbereiche waren weg („riesen groß").

Fix in main.gd (besitzt die UI, überlebt den Playtest-Node): beim
Start alle split_offsets von MainSplit/BottomSplit sichern, im
Exit-Handler deferred UND im nächsten Frame zurücksetzen (das Layout
muss die Panels erst neu vermessen haben). Suite test_playtest 52/52
(mit Splitter-Regression: merken → verbiegen → Exit → unverändert),
test_ui_layout 79, test_gui_smoke 92 grün.
2026-10-20 21:21:05 +00:00
Hermes (scopedvr-dev)
812da1a261 Playtest-Avatar (t_17a35c59): Kommentar-Harmonisierung nach Rebase auf 52919b3
Beide WASD-Yaw-Fixes koexistieren (52919b3: Bewegung aus yaw_deg;
t_17a35c59: Koerper-Rotation fuer Avatar-Front + Third-Person-Kamera).
Die Kommentare behaupteten gegensaetzliche Fakten — jetzt konsistent.
Suiten: test_playtest_avatar 84/84, test_playtest 39/39.
2026-10-20 21:19:20 +00:00
Hermes (scopedvr-dev)
6f00737dd3 Playtest-Avatar (t_17a35c59): Spieler-Modell = Bot-Rig + First/Third-Person (Taste V)
- playtest_avatar.gd (NEU): Spiegel-Modul des Bot-Avatars (Quelle
  bot_avatar.gd/MatchRemoteAvatar) — UAL-GLB + m40-Rifle aus dem
  Nachbar-Klon ../scopedvr-client (GLTFDocument, kein Duplikat),
  AnimationTree-Pipeline 1:1 (BlendSpace1D Idle/Walk/Jog auf den
  Clip-Naturgeschwindigkeiten 0/0,979/2,620 m/s, sync, TimeScale-Kadenz
  nach BOT2: Schrittlängen 0,95/0,85 m, Klammern 1,0–4,5 / 0,6–2,0,
  Blend-Glättung 0,25 s), Gait-Schwellen 0,3/2,6 m/s aus der ECHTEN
  PlaytestBody-Geschwindigkeit, Frontachse +Z → MODEL_YAW_OFFSET = PI.
  Importer-Semantik nachgebildet: "_loop"-Endungen gestrichen +
  LOOP_LINEAR (GLTFDocument liefert rohe GLTF-Namen).
- playtest_body.gd: Körper trägt den Blick-Yaw (1:1 TestmapPlayer
  rotate_y — vorher weltachsen-fixierte WASD-Bewegung, Beweis-Probe),
  Avatar-Halterung (Kopf-Anker 1,65 m, GLB-Füße am Boden), Third-Person-
  SpringArm3D (3,5 m + 0,3 m Margin, eigener Body ausgeschlossen),
  toggle_perspektive/set_perspektive, horizontale_geschwindigkeit().
- playtest_settings.gd: persistenter Eintrag dritte_person (Default First).
- playtest_mode.gd: Avatar-Bau in enter() (graceful ohne Client-Klon:
  First Person + Klartext-HUD-Hinweis), Taste V, Gait-Speisung pro Frame,
  HUD-Hinweis + F1-Hilfe erweitert.
- settings_panel.gd: Perspektive-Auswahl (Erste/Dritte) im Einstellungen-
  Panel, live wirksam + persistiert.
- tests/test_playtest_avatar.gd (NEU, 84 checks): Identitäts-Vertrag gegen
  die Client-Quelle (Konstanten aus bot_avatar/match_remote_avatar/
  testmap_player gelesen), GLB-Ladung, Anim-Pipeline, Kadenz-Mathe,
  Modellhöhe == Kapsel == Client (1,83 m), Rifle-Mount, V-Umschalter +
  Persistenz, SpringArm (Blick zum Körper, kein Clipping durch Wand),
  Blickrichtung == Bewegungsrichtung, Playtest-Integration (enter + Gait).
2026-10-20 21:18:25 +00:00
Hermes (scopedvr-dev)
52919b36b4 Playtest: WASD folgt der Blickrichtung (Sly-Befund 20.10.)
Bug: Die Laufrichtung kam aus transform.basis des Körperknotens — die
Kamera-Drehung steckt aber im look_cam-Kindknoten, der Körper rotiert
nicht → WASD lief stur in eine Himmelsrichtung. Auf der Quest rotiert
der ganze Origin mit dem Kopf, dort trat der Bug nicht auf.

Fix: forward/right aus yaw_deg bauen (identisch zu view_direction(),
identisch zum Kopf-Yaw des Clients). Regression in test_playtest:
yaw 0° + W → -Z, yaw 90° + W → -X. Suite 39/39 grün.
2026-10-20 21:10:09 +00:00
Hermes (scopedvr-dev)
0cdf68622c Playtest-Physik = Quest 3 identisch (t_1da96275): CharacterBody3D + echte Kollider statt AABB-Blocking — KolliderBau (1:1-Spiegel svmap_world), Testmap-Player-Parameter (Kapsel 0.35x1.8, 6/14 m/s, Jump 4.5, Step-Up 0.45), Bauteil-Trimeshe (Treppe/Tür), Identitäts-Vertrag Suite (Editor vs. Client, 8 Entscheidungen identisch), FPS-Probe (Physik-Overhead 7%) 2026-10-20 21:01:37 +00:00
Hermes (scopedvr-dev)
fabbbcce20 Playtest-Kollision: echter Registry-Fußabdruck statt Pick-Radius-Kugel
Sly-Befund 20.10.: Im Editor-Testmodus hing man an unsichtbaren Wänden weit
VOR den Gebäuden. Ursache: _collect_obstacles nutzte die Viewport-Metas
pick_radius (Kugel-Näherung = volle Raumdiagonale + Zuschlag) — bei der
Kaserne (17×35 m) blockte das ~40×40 m. Die Trimesh-Fixes aus t_9558038d
wirken nur im Spiel-Client, der Editor-Playtest baut seine Blockierung
selbst (keine Physik-Welt, nur AABBs).

Fix: Hindernis-AABB aus der Registry (collision.size × scale, um rot_y_deg
konservativ gedreht, Y vom Fußpunkt bis Fußpunkt + Höhe — gleiche
Konvention wie der Client-Kollider). Fallback: Kugel-Näherung bleibt für
Objekte ohne Registry-Eintrag.

Regression in test_playtest.gd: gedreht/ungedreht, Scale, Fallback
(20 checks, alle grün). Suiten: playtest_comfort 51, playtest_spawn 56,
resize_dialog 44, terrain 153 — ALL OK.
2026-10-20 20:11:11 +00:00
Hermes (scopedvr-dev)
1f1a413065 Begehbare Gebäude (t_9558038d): Registry + Kollisionsdateien gezogen
- library/objects.json vom Match-Server (dc9432b): 37 Gebäude jetzt
  shape=trimesh mit collision.quelle -> library/kollisions/<oid>.json
  (36 038 Dreiecke gesamt, objects.json bleibt klein).
- library/kollisions/ (37 Dateien) im Editor-Repo. Editor-Platzhalter nutzt
  weiterhin collision.size — Optik/Anzeige unverändert.
- Suiten nach dem Sync grün: test_library 101, test_object_mirror 74,
  test_library_browser_ui 30, test_editor_svmap 31, test_map_tint 31/0.
2026-10-20 19:50:36 +00:00
Hermes (scopedvr-dev)
d637314b82 Update-Check: sichtbares Ergebnis-Fenster für „🔄 Updates prüfen“ (Sly-Wunsch 20.10.).
Befund: der Check funktionierte die ganze Zeit — die Rückmeldung kam aber
NUR in der Statuszeile („Du bist auf dem neusten Stand (…).“) und verblasst
dort schnell → las sich wie „tut nichts“. Probe (echter Menü-Klick): Check
läuft, ls-remote ok, Status korrekt.

Fix: nach dem MANUELLEN Check (report=true) öffnet ein kleines AcceptDialog
„Update-Check“ mit Klartext-Ergebnis (Stil wie Map-Check-Dialog):
- up_to_date: „Du bist auf dem neusten Stand (<sha>).“
- update_available: Zusätzlich zum Banner das Fenster mit local → remote
  und Hinweis auf „Jetzt updaten“.
- unreachable / auth_failed / error: gleicher Klartext wie Statuszeile.
Der Still-Start-Check (report=false) zeigt bewusst KEIN Fenster (nur Banner
bei „Update verfügbar“) — Probe belegt: Popup sichtbar=false.
Probe: probe_update_check (echter Menü-Klick → Popup sichtbar + Text).
Suiten: test_updater 75 OK, test_resize_dialog 44 OK, test_theme 35 OK,
test_gui_smoke 92 OK, test_ux 137 OK, test_ui_layout 79 OK.
2026-10-20 18:37:38 +00:00
Hermes (scopedvr-dev)
ae551f387a Fix: Resize-Dialog 5930 px hoch + OK/Abbrechen unterhalb des Bildschirms (Sly-Befund 20.10. „viel zu lang, kann nichts bestätigen").
Ursache (Proben im Container, xvfb): autowrap-Labels OHNE Mindestbreite
melden VOR dem ersten Layout eine explosionsartige Mindesthöhe (Wrap bei
~1 px → jedes Wort eine Zeile). popup_centered() übernimmt genau diesen
Wert als Fensterhöhe: Resize-Dialog 5930 px statt ~470, Neue-Map-Dialog
4781 px statt ~390 — beide waren betroffen. Nach dem Layout fällt der
Mindestwert zurück, die Fenstergröße bleibt aber.
Zusatz-Befund: wächst die Live-Vorschau NACH dem Öffnen (Preset 256 →
Warnband), bleibt das Fenster oben hängen → Buttons bei y≈940 in einem
900-px-Sichtbereich = unerreichbar.

Fix:
- resize_map_dialog + new_map_dialog: autowrap-Labels bekommen
  Mindestbreite ~ Dialogbreite → Umbruch rechnet mit echter Breite,
  Fenster normal hoch (Probe: 481 px).
- resize_map_dialog: Inhalt in ScrollContainer (horizontal aus), Höhe
  dynamisch = min(Inhalt, sichtbarer Platz); nach jeder Vorschau-Änderung
  wird das Fenster gedeckelt und neu zentriert (_fit_dialog/
  _fit_after_layout) → OK/Abbrechen IMMER erreichbar. unresizable.
- tests: test_resize_dialog prüft jetzt Mindesthöhe VOR dem Popup
  (absolut, < 1500 px) für beide Dialoge + echten Menü-Klick;
  Preset-Button-Suche robust über find_children.
Proben (xvfb): Dialog 662×481, nach Preset 256 → 662×631, unten 765 < 900,
Buttons sichtbar (Beweisbild work/dialog_beweis/resize_dialog_fix.png).
Suiten: test_resize_dialog 44 OK, test_theme 35 OK, test_gui_smoke 92 OK,
test_ux 137 OK, test_ui_layout 79 OK.
2026-10-20 18:33:07 +00:00
Hermes (scopedvr-dev)
f4cfe17594 Fix: Datei-Menü komplett tot — id_pressed landete als Argument VOR dem Methodennamen (Sly-Befund 20.10. „Kartengröße ändern tut nichts“).
popup.id_pressed.connect(main.call.bind("_on_menu_action")) rief effektiv
call(6, "_on_menu_action") → „Cannot convert argument 1 from int to
StringName“ (map-builder.log 20.10., 2 Treffer) — KEIN Menü-Eintrag tat
etwas (Neue Map/Öffnen/Speichern/Kartengröße/Updates). Alle anderen
.call.bind-Stellen hängen an arg-losen Signalen (pressed/timeout) und
waren unbeeinträchtigt. Fix: Lambda mit expliziter Reihenfolge
(Stil wie grid_btn/contours_btn). Regression: test_resize_dialog stellt
jetzt den echten Klick nach (id_pressed.emit(6) am Datei-Popup) — vorher
rief die Suite _open_resize_dialog() direkt auf und sah den Bruch nicht.
Suiten: test_resize_dialog 42 OK, test_theme 35 OK, test_gui_smoke 92 OK, test_ux 137 OK.
2026-10-20 18:19:42 +00:00
Hermes (scopedvr-dev)
f6c754115a U7 (t_2336c58e): Feinschliff — Theme konsequent (Disabled/Fokus/Hover, LineEdit, Tab-Hover, Splitter-Anfassen, Scrollbars, Farb-Token warning/badge_outline), Abstands-Raster 4/8 über UIScale, Typografie-Raster (heading/body/small via ui_scale.gd), Tooltips für alle Knöpfe inkl. Dialoge, Leerzustand in der Bibliothek, Fenstertitel mit „(ungespeichert)“. Neue Suite tests/test_theme.gd (35 Checks). Keine Funktionsänderung. 2026-10-20 12:08:10 +00:00
Slyphax
0e1406a63e M5 (t_60014669): Ein-Klick „Karte symmetrisch machen“ + Symmetrie-Check mit Klartext-Bericht (Spec vc27). Neu: symmetry_ops.gd (analyze/apply/check — Paarung über Spiegel-Position + sym-Links, Ergänzen ohne Löschen, Widersprüche zählen und der gewählten Quelle zusprechen, Terrain zellgenau Höhen+Material, Spawn-Zwillinge im Gegner-Team via M4-Mechanik, frische sym-Kopplungen ohne V16-Waisen) + Symmetrie-Panel über Panel-Registry (main.gd unberührt; Widerspruchs-Dialog nennt die Zahlen VOR dem Anwenden, Quell-Hälfte links/rechts bzw. vorn/hinten, EIN Undo-Schritt). Suite test_symmetry_onclick (66 Checks: 0 Abweichungen nach Klick, Zellgleichheit, Quell-Flip, Legacy ohne sym, EIN Undo stellt alles her, Modus AUS no-op) + Renderprobe probe_m5_symmetry_render (Vorher/Nachher). 2026-10-20 10:55:06 +00:00
Hermes (scopedvr-dev)
835a7f7363 U3 t_a4bf9b8c: Objekt-Gruppen — GroupState (group_state.gd, fuell-/datei-agnostisch ueber get_meta/set_meta), scene_tree_panel: Gruppen-Ebene im Baum (Name + (N Objekte), eingerueckte Mitglieder, Klick waehlt alle Mitglieder, Gruppen-Knoten markiert wenn alle Mitglieder gewaehlt), zwei-Wege mit Auswahl, Rendering bleibt Gruppen-unabhaengig. Neue Suite tests/test_scene_groups.gd + Renderprobe probe_u3_groups_render.gd. 2026-10-20 10:36:04 +00:00
Hermes (scopedvr-dev)
5dca5513da U6 t_639135dd: dezente Auswahl — Kontur statt Vollorange (Textur bleibt sichtbar).
Neu shaders/selection_outline.gdshader: Inverted-Hull-Außenhülle (unshaded,
cull_front, Grow entlang der Normalen; Streifen-Uniform für den Zwilling).

Markierung jetzt über Outline-Kind am Mesh (Auswahl/Hover/Partner); Farben +
Breiten aus theme/editor_theme.tres (selection_outline*, *_width_mm) — eine
Quelle. material_override wird von der Markierung nicht mehr angefasst:
GLB-Texturen bleiben im Auswahl-Zustand voll sichtbar, der Tint bleibt beim
Markieren sichtbar (kein "Orange gewinnt" mehr).

Spawn-Zonen: konstante Disc-Deckkraft + dezenter Kontur-Ring (Torus) statt
Aufhellung/Emission; Label-Farbe aus dem Theme statt hartkodiertem Gelb.
Bau-Teile: Kontur um das Bauteil-Mesh statt halbtransparenter Vollbox.
Neu: set_hover()-API (schwächste Stufe) — Anschluss an die Mausbewegung
liegt in interaction.gd (Grenze dieser Karte) und folgt als eigene Karte.

Suite tests/test_selection_look.gd (41 checks: Theme-Quelle, Grundmaterial
unangetastet, Outline nur bei Markierung, restlos weg bei Abwahl, Zwilling
dünner+gestrichelt, Spawn-Ring, Bau-Teil-Kontur). Altsuiten an die neue
Semantik angepasst (test_glb_view: GLB-Auswahl ohne Override; test_map_tint
T3: Kontur statt Orange-gewinnt; test_object_mirror: Partner per Kontur).

Beweise: 4 Renderproben (fass, echtes GLB — vorher Vollorange / nachher
Kontur, je auf Sand und Fels) in /opt/data/scopedvr/work/u6_auswahl_beweise/.
Voll-Batterie 32/32 grün (EXIT 0) inkl. der drei angepassten Suiten.
2026-10-20 10:33:04 +00:00
Hermes (scopedvr-dev)
f16b9c8fb6 U5 t_5e3e97c9: Platzieren EINMAL — ein Klick setzt, Werkzeug beendet sich, neues Objekt ausgewählt (Baum+Inspektor), Klartext-Status; Shift = Serienmodus (weiter setzen, Status nennt ESC); Ghost/Panel-Kachel nach dem Setzen zurück; eine Platzierung = ein Undo-Schritt. Abbruch via ESC (ShortcutPolicy) — Rechtsklick bleibt Flug-Vorrang (t_2470de89, test_rmb_vorrang). Neue Suite tests/test_place_once.gd (52 checks) + Renderprobe. 2026-10-20 10:15:01 +00:00
Hermes (scopedvr-dev)
403895b743 t_a7d15717: run_suites.py False-Positive gefixt — Fehler-Regex nur noch auf ^SCRIPT ERROR: / ^Parse Error: am Zeilenanfang (re.MULTILINE). Vorher matchte die Phrase-Regex das eigene Check-Label der Suite ("ok: Parse Error wird erkannt ...", test_updater.gd Zeile 73) -> SCRIPT_ERROR obwohl ALL OK: 75. Zusaetzlich \n zwischen stdout+stderr (sonst verkleben die Streams und der ^-Anker verliert die erste stderr-Zeile). Abnahme: Voll-Batterie SUITEN_TOTAL=30 GRUEN=30 ROT=0, test_updater sauber OK. 2026-10-20 10:12:14 +00:00
Hermes (scopedvr-dev)
60b09bfb74 U2 t_e3c0310a: Szenen-Baum links mit Kategorie-Ebene + Zwei-Wege-Auswahl — scene_tree_panel: Objekte unter Kategorie-Knoten (LibraryBrowser.group_of + Label-Mapping), Spawn-Zonen Team A/B, Bau-Teile, Gruppen-Ebene (GroupOps; Klick auf Gruppe waehlt alle Mitglieder), Kopf mit Zaehlern (Objekte (N)), Auge-Praefix je Eintrag (nur Anzeige, keine Doc-Aenderung), leerer Zustand mit Klartext-Hinweis, Zwei-Wege-Kopplung ueber _obj_index_map (O(1) Markieren + scroll_to_item). Neue Suite tests/test_scene_tree.gd (24 checks: Baum fuellt sich, Klick->3D, 3D->Baum, Mehrfach, Gruppen-Klick, leerer Hinweis, UIScale). Renderprobe probe_u2_scene_tree_render.gd. main.gd NICHT angefasst. 2026-10-20 10:03:34 +00:00
scopedvr-operator
3d7ba993d5 glb_mesh_store: verschachtelte Pack-Modelle auch in models/ finden
Operator-Befund aus dem Eigenstaendigkeits-Beweis auf dem PC (20.10.):

Die App baute den Registry-Pfad nur als <quelle>/<rel-pfad-mit-Praefix
'assets/objekte/'>. Der mitgelieferte models/-Ordner NEBEN der App enthaelt die
Pack-Struktur aber DIREKT (models/military_desert/props/box_010.glb). Ergebnis:
291 von 334 Modellen (alle verschachtelten Pack-Objekte — genau die, mit denen
die Karten gebaut werden) wurden in der eigenstaendigen App nicht gefunden.

Fix: zusaetzlicher Kandidat <quelle>/<rel-pfad OHNE 'assets/objekte/'> — der
flache Alt-Pfad bleibt unveraendert.

Belegt (exportierte App, nacktes Umfeld, frisches HOME):
  box_010       -> models/military_desert/props/box_010.glb   (vorher: leer)
  baum_kiefer_1 -> models/baum_kiefer_1.glb
  fehlende_glbs=0   PROBE ERGEBNIS: OK
Suiten: gui_smoke 92 OK, Import 0 Fehler.
2026-10-20 10:01:57 +00:00
scopedvr-operator
a62357dad8 tools/probe_gen_map.gd: OIDs per --oid= waehlbar
Damit lassen sich auch VERSCHACHTELTE Pack-Modelle pruefen
(models/military_desert/<kategorie>/<oid>.glb) — noetig, weil der erste
Eigenstaendigkeits-Beweis nur flache Modelle abdeckte.
2026-10-20 10:00:29 +00:00
scopedvr-operator
e317706df0 tools/sync_models.sh: Pack-Referenzordner als Textur-Quelle
Operator-Befund auf dem PC (20.10.): die Original-Texturen liegen NICHT im
Client-Klon, sondern in einem eigenen Pack-Ordner:
  ~/godot-projects/ithappy_pack_ref/ithappy/military_desert/textures/
Deshalb sucht das Skript jetzt zusaetzlich dort (<clone>/../ithappy_pack_ref/...)
und als letzte Instanz per find -maxdepth 6 unter <clone>/.. und <client>/..
(also neben dem Projekt). Belegt mit einem nachgebauten PC-Aufbau: Palette wird
gefunden, landet FLACH in models/textures/, --check danach Exit 0.
2026-10-20 09:58:47 +00:00
scopedvr-operator
8c97d00871 tools/sync_models.sh: Texturen suchen + flach spiegeln + Textur-Ordner nicht mehr loeschen
Operator-Nachbesserung nach dem Eigenstaendigkeits-Beweis (exportierte App,
kein Client/Godot im Dateisystem):

- SRC_TEX wird GESUCHT statt fest verdrahtet: SCOPEDVR_TEX_DIR ->
  assets/textures/military_desert -> assets/textures -> Ordner der
  Demonstration_Textures*.png unter assets/. Findet er nichts, gibt es einen
  HINWEIS samt Loesung (SCOPEDVR_TEX_DIR=/pfad).
- Texturen werden FLACH nach models/textures/<name>.png gespiegelt — die App
  sucht sie dort; im Client liegen sie verschachtelt.
- Modell-Spiegel schliesst den Unterordner textures/ aus (skip_top): vorher
  galt er als 'ueberfluessig' und wurde bei JEDEM Lauf geloescht, der
  Textur-Spiegel legte ihn neu an -> Endlosschleife, --check nie sauber.

Belegt: 2. Lauf 0 Aenderungen, --check Exit 0; exportierte App findet
baum_kiefer_1/stein_mittel_1 aus models/, fehlende_glbs=0, ERGEBNIS: OK.
2026-10-20 09:55:05 +00:00
Hermes (scopedvr-dev)
7c7dc38566 U-Nachlauf t_efb3d022: assets_panel _library_load_open_states robuster Typ-Check
bool(cfg.get_value()) warf SCRIPT ERROR "Nonexistent bool constructor" bei
Nicht-Bool-Eintraegen in [library] (Array-Eintraege neben zuletzt_benutzt) —
die Klapp-Zustaende luden dann gar nicht (STATES leer).

Fix: nur echte Bool-Werte uebernehmen, String/Int/Array auf Fallback true,
kein Crash mehr.

Beweis-Probe tests/probe_t_efb3d022_repro.gd:
  vorher: 2x SCRIPT ERROR "Invalid call. Nonexistent 'bool' constructor",
          STATES leer
  nachher: 0 Fehler, 3 Zustaende geladen (Array-Eintrag -> Fallback true)

Suite test_assets_panel.gd erweitert um Testgruppe 0
"Klapp-Zustaende aus kaputter cfg" (6 neue Checks, 75 -> 81, ALL OK):
Array/String/Int-Eintraege + save->load-Rundtrip.

test_inspector 47/47, test_gui_smoke 92/92, --import 0 Parse-Errors.
2026-10-20 09:52:26 +00:00
Hermes (scopedvr-dev)
4fb883a44c T3 t_aaff5dcc: Komfort im Testlauf — Fliegen/God-Mode (G, Noclip+Blickrichtung), Spawn-Sprung 1-4 (exakt, Terrain-Hoehe), HUD-Toggle H + Blick/Hoehe, Achsen-Linien X, Hilfe F1, Maus-Empfindlichkeit (PlaytestSettings + Einstellungen-Panel via Registry, main.gd unangetastet); Suite tests/test_playtest_comfort (51 Checks), test_ui_layout Tabs registry-getrieben 2026-10-20 09:47:28 +00:00
scopedvr-operator
ab173ac8aa T2 Ueberall installierbar (Operator-Push): Modelle aus mitgeliefertem models/-Ordner
Worker hatte die Arbeit fertig, aber NICHT committet/gepusht (3. Fall heute) —
aus dem Karten-Arbeitsordner gerettet und hier gepusht.

- GlbMeshStore.quellen(): Reihenfolge EINE Stelle — Setting -> Sibling-Repos ->
  mitgelieferter Ordner models/ direkt neben der App (dedupliziert, typisiert)
- Klartext-Meldung FEHLT_TEXT statt stillem Platzhalter; Notfall-Modus: Terrain
  und Bauteile laufen ohne GLBs
- neu: tools/sync_models.sh (fuellt models/, --check), utils/modelle_status.gd
  (Autoload ModelleStatus), tools/probe_gen_map.gd
- neu: README_INSTALL.md (85 Zeilen, Schritt fuer Schritt + §5 Lizenzhinweis:
  ITHappy-Rohdaten NICHT weitergeben, App fuer Slys eigene Rechner)
- export_presets.cfg (eigenstaendiger Linux-/Windows-Export), .gitignore:
  models/ + export/ bleiben draussen (Lizenz + Groesse)
- Tests: test_model_sources (26/27; die rote ist umgebungsabhaengig:
  'lager_ist_leer' erwartet fehlende Sibling-Repos)

Belegt: Import 0 Fehler, gui_smoke 92, ui_layout 71, playtest_spawn 56.
2026-10-20 09:43:05 +00:00
scopedvr-operator
0f86472cfa T1b-Wiederanbindung (Operator): Test-Startpunkt zurueck nach dem U1-Umbau
Sly-Befund: der Knopf 'Test-Start' fehlte; tests/test_playtest_spawn.gd brach
mit "Nonexistent function '_test_start_reload'" ab und hing.

- Werkzeug-Modus 'teststart' wieder in _set_tool_mode (+ enter/leave/sync)
- ShortcutPolicy.esc_action: exit_teststart inkl. Test
- Taste T: Editor = Werkzeug umschalten, Lauf-Modus = Punkt setzen + Teleport
  aufs Fadenkreuz (playtest.teleport_crosshair)
- Entf im Werkzeug loescht den Punkt; Klartext-Hinweis des Werkzeugs wieder in
  der Statuszeile ('auf dem Boden unter dem Objekt')
- Leisten-Knopf '🎯 Test-Start' als Umschalter (registry.gd ACTIONS)
- neues Panel-Modul src/ui/panels/test_start_panel.gd + Registry-Eintrag
- Haken _test_start_apply/_delete/_reload + test_start_point/test_start_panel/
  test_start_btn stehen fest auf main (Vertrag der Pruefsuite)
- _test_start_reload() wieder nach Neu/Laden/Speichern; playtest.enter() bekommt
  den aufgeloesten Startpunkt (TestStart.resolve_start)

Belegt: test_playtest_spawn 56/56, test_shortcuts 204, gui_smoke 92, ui_layout 71,
Import 0 Fehler.
2026-10-20 09:37:05 +00:00
Hermes (scopedvr-dev)
40449932b9 K1d t_6c1027c1: Startansicht 80 m über der Kartenmitte (Pos1 bleibt ganze Karte)
fit_to_document setzt die Ansicht jetzt auf START_DISTANCE_M = 80 m über der
Kartenmitte (XZ-Mitte der Map-Bounds, Höhe = Gelände dort), yaw 45°, pitch 35°.
Die Regel ist unabhängig von Kartengröße (256/1024/4096) und Inhaltslage;
leeres Dokument ebenfalls 80 m. Statuszeile: „Ansicht: Kartenmitte, Abstand
80 m (Pos1 = ganze Karte).“ Pos1/Home bleibt fit_to_bounds = ganze Karte
(MAP-Bounds werden weiterhin gemerkt, nicht die Content-Bounds).

Ziel-Y bewusst auf der Geländehöhe: die y-Mitte der Bounds liegt bei neuen
Maps 35 m über dem Boden — damit hätte die Ansicht über den Boden gezielt und
die Objekte stünden am unteren Bildrand.

Suite test_orbit_camera auf die neue Regel umgestellt (50 Checks):
Startabstand/Richtung, 80-m-Regel über Größen und Inhaltslagen, leeres
Dokument, „Pos1 ≠ Startansicht“ (80 m → 1215,6 m, alle 8 Map-Ecken).
Dazu Renderprobe tests/probe_k1d_render.gd (2 PNGs).
2026-10-20 09:33:16 +00:00
scopedvr-operator
85bee00f3d U1-Nachbesserung (Operator, Sly-Befund 'Damit kann man nicht mehr arbeiten'): Werkzeugleiste erbte EditorPanel (=VBoxContainer) und wurde selbst senkrecht -> jeder Knopf eine fensterbreite Zeile (~440px), 3D-Fenster auf einen Streifen gedrueckt. Fix: toolbar.gd extends HBoxContainer + size_flags SHRINK_BEGIN; neue Groessenprobe in test_ui_layout (Leiste <=80px, Mittelbereich >=120px). 2026-10-20 09:22:39 +00:00
Hermes (scopedvr-dev)
8f0e011a07 U4 t_69687afc: Assets-Bereich Unity-artig ausgebaut — Kategorien-Leiste (Alle + Gruppen aus LibraryBrowser, Summe = Gesamtzahl), Suche Name+ID, Zaehler "N von M sichtbar", Zuletzt benutzt (cfg [library] zuletzt_benutzt, Chip + Ansicht), Kacheln mit 96-px-Vorschau + ID + Name, Hover vergroessert die ID, fehlende GLB = abgedunkeltes Bild + orange Badge "Modell fehlt" (GlbMeshStore.fehlt), Klick = auswaehlen (Signal object_selected genau 1x je Wechsel), Doppelklick = Werkzeug starten wie bisher (nochmal = abbrechen), Spaltenzahl breitenabhaengig (180 px je Kachel, ueber ui_scale), Dock-Groesse bleibt in [fenster] gemerkt (U1). Klapp-Gruppen + Alle auf/zu + gepinnte Masse (Suchfeld 36, Kachel 126, Thumb 96) unveraendert. Suite tests/test_assets_panel.gd 75 checks ALL OK; test_library_browser_ui 30, test_library 101, test_ui_layout 63, test_gui_smoke 92, test_inspector 49, test_playtest 11, test_object_mirror 74, test_ui_scale OK — alle unveraendert gruen. Renderproben unter /opt/data/scopedvr/work/u4_assets_beweise/. test_playtest_spawn ist VOR dieser Karte rot (U1-Regression _test_start_reload, siehe Kommentar auf t_eaccbfed) — nicht von U4 beruehrt. 2026-10-20 09:19:37 +00:00
scopedvr-operator
69ffe23a71 t_eaccbfed: U1 UI-Fundament (Operator-Push) — main.gd 3677->2479 Zeilen, src/ui/panels/ (11 Module + registry), Tabs rechts+links, Assets unten, theme/editor_theme.tres, tests/test_ui_layout.gd (63 checks). Worker meldete fertig ohne Push; Arbeit aus dem Scratch-Ordner gerettet. 2026-10-20 09:11:00 +00:00
Hermes (scopedvr-dev)
a5d8f7f79d T1b t_96e3c4ee: Test-Startpunkt per Klick — Toolbar/Taste T setzt den Punkt, wo F5 den Testlauf startet (Höhe automatisch vom Terrain, Klick auf Objekt/Bauteil = Boden darunter). Sidecar user://scopedvr_teststart.cfg je Kartenpfad, KEIN .svmap-Eintrag, kein Undo, kein Spawn-Check-Kontakt. F5-Fallback mit Klartext: Test-Start → erster Team-Spawnpunkt → Kartenmitte. T im Testlauf versetzt sofort zum Blickpunkt (Augenhöhe 1,70 m, Kollision geprüft) und setzt den Punkt neu. Marker (Säule+Ring, Akzentfarbe) sichtbar solange gesetzt. Suite tests/test_playtest_spawn.gd 56/56, Voll-Batterie 25/25 grün, Renderprobe (4 PNGs) unter /opt/data/scopedvr/work/t96e3_beweise/ 2026-10-20 08:22:47 +00:00
Hermes (scopedvr-dev)
b53e05a145 T1 t_1cdc1a72: F5-Smoke-Test an neuen Lauf-Modus angepasst (PlaytestMode statt Client-Fehler) + Render-Probe-Script 2026-10-20 08:07:15 +00:00
Hermes (scopedvr-dev)
1b943ceb35 T1 t_1cdc1a72: Testlauf im Map Builder selbst (erste Person, WASD+Maus, ohne Client/VR/Server) — PlaytestMode + PlaytestBody in src/editor/playtest/, minimale main.gd-Einbindung (F5/Toolbar), test_run.gd als ALT markiert, Suite tests/test_playtest.gd (11 Checks, vor Push noch Rebase auf 969e928 + Voll-Suiten-Batterie + Beweis-Bild) 2026-10-20 07:45:33 +00:00
Hermes (scopedvr-dev)
969e928d74 k1b: Editor-Kamera mit Inhalt ausrichten (Startansicht auf Objekte/Bauteile, Pos1 ganzer Terrain-Klotz, 180-Grad-Schwenk um Inhaltsmitte) 2026-10-20 07:42:50 +00:00
Hermes (scopedvr-dev)
2c5b58f04c t_950fddaa K1: Kamera-Drehen fixen — Drehpunkt unter den Mauszeiger, Map beim Laden passend einpassen
orbit_camera.gd: set_pivot(hit) rechnet aus unveränderter Position/Basis
target/yaw/pitch/distance neu (kein Sprung); MMT-Druck pivoted auf den
Punkt unterm Cursor — Objekt-Treffer zuerst (interaction._pick_object),
sonst Terrain (interaction._ground_point), sonst alter Ziel-Punkt.
ORBIT_DEG_PER_PX = 0.3 gekapselt, Zoom-Minimum DISTANCE_MIN = 5.
fit_to_bounds(mn, mx) ersetzt das tote frame_map(): target = Kartemitte
aus bounds, Abstand über Umkugel-Formel (alle 8 Ecken sicher im Frustum —
reiner Kanten-Faktor 0,65 reicht bei flachen Maps nicht), Mindestabstand
30 m; Pos1/Home = ganze Karte ins Bild (Bounds bleiben gemerkt).
main.gd: Laden/Neu + Resize rufen fit_to_bounds mit den echten doc-Bounds.
Neu: tests/test_orbit_camera.gd (30 checks: kein Sprung, Drehpunkt wandert,
fit, gleichmäßiges Ziehen, Flug-Übergang, Pos1).
2026-10-20 07:09:11 +00:00
Hermes (scopedvr-dev)
dca0947a71 t_1e82f747: Test-Umgebung aufgeraeumt — test_updater Setup robust, test_glb_view auf Pack-Palette (beide Suiten statt immer rot jetzt gruen/SKIP). test_updater: Verzeichnisname war Time.get_ticks_msec (kleine Zahl, kollidierte mit nicht loeschbaren Alt-Laeufen aus read-only git-Packs) → jetzt Unix-Zeit+ticks_usec; Cleanup chmod -R u+w (blocking via OS.execute mit output-Array, 2-Arg-Variante ist async) + include_hidden (.git ist versteckt) + rm -rf-Fallback; git-Fehler geben echten Fehlertext statt roter Zahl. Echte Updater-Pruefungen laufen wieder (75/75, vorher 15/75 FAIL an Setup). test_glb_view: Nachbar-Repos suchen wie tools/sync_objects.sh (Env SCOPEDVR_CLIENT_DIR/SCOPEDVR_MATCH_SERVER_DIR, dann ../+../../-Siblings), Quellen zur Laufzeit in ProjectSettings gesetzt (basis-relativ gerechnet, path_join ketten Absolutpfade); ohne Nachbar klare SKIP-Zeilen+Exit 0 statt 15 FAILs. Camo-Erwartungen entfernt (ueberholt seit d0d4fbd): jetzt Pack-Palette — alle Flaehen albedo_texture != null, Kante 1024², pack_datei_fuer-Zuordnung (military_base→Demonstration_Textures1.png, picture_1/2→Demonstration_picture_N.png 2048²) + Pack-Dateien im Nachbar-Repo (48/48, vorher 4/38 FAIL). Notiz tools/TEST_UMGEBUNG.md (Abhaengigkeiten + test_gui_smoke-F5 vorbestehend rot am 0adb161 via git-stash verifiziert, nicht Teil dieser Karte). 2026-10-20 07:06:48 +00:00
Hermes (scopedvr-dev)
2c10863759 M2 (t_f9eff526): Terrain spiegeln — Höhen UND Boden-Material in allen Live-Pfaden (Spec vc27). terrain_tool: jeder Pinsel-Schritt (Heben/Senken/Glätten/Malen) wendet den Pinsel EINMAL an und kopiert die geänderten Zellen (Snapshot-Vergleich) exakt per SymmetryCore.mirror_cell in die andere Hälfte — Strich-Seite ist Master, Überlappung gewinnt strichnäher, Mitte/Achse wird nicht gedoppelt. demo_stroke läuft über dieselben Schritte. Ohne sym_mode-Feld/Modus AUS: exakt altes Verhalten (byte-identisch zu origin/main, SHA256-Gegenprobe). terrain_mesh unverändert (Live-Pfade tragen Höhe+Schicht bereits, Audit im Handoff). Neu: tests/test_terrain_mirror.gd (105 checks, alle 3 Modi × 4 Pinsel + Mitte/Achse/Rand/Überlapp + Gegenprobe) und tests/probe_terrain_mirror_render.gd (Beweis-Bild Draufsicht). 2026-10-20 06:49:08 +00:00
Hermes (scopedvr-dev)
238ca7bb2f M3 (t_34908425): Objekte spiegeln — Platzieren/Verschieben/Drehen/Löschen/Duplizieren inkl. Partner-Bindung (Spec vc27). ObjectOps: symmetry_place_twin/symmetry_sync_pairs/symmetry_expand_delete (Mathe aus SymmetryCore M1, keine zweite Spiegel-Mathe; Mitte/Achse nicht gedoppelt). interaction: Zwilling in ALLE Objekt-Pfade integriert (Platzieren, Drehen, Löschen, Duplizieren, Pfeiltasten, Ziehen live+Commit, Greifen live+Esc-Rückabwicklung, Spiegeln-Button) — jede Änderung = EIN Undo-Schritt. map_view_builder: schwächere Partner-Markierung über die bestehende Auswahl-Material-Logik (keine zweite Technik). Neu: tests/test_object_mirror.gd (74 checks, alle 3 Modi + Gegenprobe AUS), tests/probe_m3_partner_render.gd (Beweis-Bild). 2026-10-20 06:48:29 +00:00
Hermes (scopedvr-dev)
c15c606311 t_ffa207b1: M4 Bau-Teile + Spawnpunkte spiegeln (Spec vc27)
- BuildOps.place_mirrored / update_mirrored / remove_mirrored /
  inside_match_bounds: pure Statics in build_ops.gd, sym-Kopplung aus M1,
  gleiche Snap-Regeln (Paritaet bleibt), tiefe Kopien (kein Aliasing).
- V9-Grenzen scharf: laege der Zwilling ausserhalb der Match-Grenzen
  (Loader-Regel _inside_bounds ±1 m), wird er NICHT gespeichert und der
  Status meldet V9 im Klartext — auch kein halber sym-Link.
- SpawnTool-Spiegelung im GEGNER-Team (Blau West <-> Rot Ost = faire
  Gegenstelle, Radius 1:1), Partner-Map in design_notes.spawn_mirror
  (Loader-frei, rundtrip-sicher). Auf der Mitte: nicht gedoppelt.
- build_tool.gd: duenne Hooks an den 6 Klick-Mutationen + demo_place —
  EIN Undo-Schritt nimmt Original UND Zwilling zurueck.
- spawn_tool.gd: Hooks in setzen/verschieben/loeschen/demo_place/demo_move;
  _sym_mode() defensiv ueber main.get() (Mock-Mains ohne sym_mode -> OFF,
  statt SCRIPT ERROR — Lektion aus test_shortcuts).
- Neue Suite tests/test_build_spawn_mirror.gd (76 Checks ALL OK) +
  Probe tests/probe_m4_mirror_render.gd (2 Beweis-Bilder).
- Regression: 19/20 Suiten gruen; test_glb_view 15/25 FAIL pre-existing
  (per git-stash gegen 437f6bf verifiziert, Ursache unabhaengig vom Diff).
2026-10-20 06:47:58 +00:00
Hermes (scopedvr-dev)
0adb161386 t_f5935023: M1 Symmetrie-Fundament (Spec vc27, Sly 20.10.) — SymmetryCore (Punkt/Achse X/Achse Z, Zell-Abbildung, Partner-Link sym, alle statisch/pure, headless testbar) + Validator V16 in svmap_io (sym muss String, nicht leer, ≤64 Zeichen, genau 2 pro Kennung) + Toolbar-Schalter + Statuszeile-Anzeige in main.gd (Standard=Punkt, Sly sieht Modus immer) + Suite test_symmetry_core (93 Checks: Modus-Katalog, Kartenmitte aus bounds, zweimal-spiegeln=Identity für alle Modi, Achse/Zentrum-nicht-doppeln, Raster-Zell-Mapping inkl. gerader/ungerader res, Partner-Link-API, V16-Validator, Roundtrip mit sym byte-identisch, Legacy-Karte ohne sym unverändert). 17/19 Suiten grün (test_updater 15/75 FAIL + test_glb_view 15/25 FAIL = PRE-EXISTING per git-stash-Verfahren gegen 437f6bf verifiziert, Ursache unabhängig vom Diff). M1 legt nur Mathe + Anzeige fest — gespiegeltes Bauen selbst kommt mit M2 (Terrain), M3 (Objekte), M4 (Bau-Teile + Spawns). 2026-10-20 06:37:50 +00:00
Hermes (scopedvr-dev)
437f6bf368 Fix Update-Banner hing endlos: git-Warnzeile wurde als Ziel-Commit gelesen (t_50a07d4b-Live-Bug, Meldung Sly 20.10.)
git_run mischt stdout+stderr; bei der HTTP-Umleitung der Forgejo-URL schreibt
git zuerst 'warning: Umleitung nach ...'. Die alte Fassung nahm die ERSTE
Zeile und hielt 'warning' fuer den Remote-SHA -> Banner 'Update verfuegbar:
<sha> -> warning' blieb fuer immer, obwohl der Klon auf origin/main stand
(live auf dem PC belegt: STDOUT korrekt, STDERR Warnung).

Jetzt: sha_aus_ls_remote() liest nur echte '<sha>\t<refs/heads/...>'-Zeilen
(Warnungen/hints/fatal uebersprungen, SHA muss Hex sein), Notnagel = erste
hex-artige Zeile; 'kein Commit in der Ausgabe' gilt als unreachable statt
update_available. Regressionstest: 6 neue Checks in test_updater.gd.
2026-10-20 05:27:02 +00:00
Hermes (scopedvr-dev)
d0d4fbd221 Fix Editor-Live-Texturen: Pack-Original-Zuordnung nach Materialname (t_50a07d4b, aa94bde korrigiert)
aa94bde hat im Live-Pfad die alten Camo-Dateien (desert_multicam_*) verwendet und
zusaetzlich per Inhaltsvergleich auch KORREKTE Texturen wieder durch Camo ersetzt
- das haette die Palette-Arbeit aus t_279df760 im Editor rueckgaengig gemacht.
Jetzt: Zuordnung Materialname -> Pack-Textur (military_base -> Demonstration_Textures1.png,
picture_1/2/3 -> Demonstration_picture_N.png), Pfad ../../../textures/military_desert/
(Client-Repo, dort liegen die Dateien seit 88fded1). Fuer den Inhalt gibt es EINE Quelle:
tools/PACK_TEXTURE_MAPPING.md. Fuer den Inhalt gibt es EINE Quelle: dieselbe Tabelle.
Flaechen mit bereits aufgeloester Textur bleiben unangetastet.
2026-10-20 00:24:54 +00:00
Hermes Agent
aa94bde708 WYSIWYG Camo-Texturen: Live-Viewport lädt Multicam-Texturen (Cache, Warnung bei Fehlen) 2026-10-19 23:26:38 +00:00
Hermes (scopedvr-dev)
c5bdb0518e Pack-Original-Optik (t_279df760): gen_thumbs auf Pack-Palette (Materialname->Demonstration_*.png, Client assets/textures/military_desert) + 345 Thumbs neu gerendert
- _camo_tex_setzen -> Pack-Zuordnung je MATERIALNAME (PACK_TEXTURE_MAPPING.md
  im Client = Quelle der Wahrheit): military_base -> Demonstration_Textures1,
  picture_1/_2 -> Demonstration_picture_1/_2; je Surface ihre Textur
- Thumbs: alle 345 Bibliotheksobjekte neu (Palette-Objekte zeigen Pack-
  Farbfelder, Multicam-Objekte echtes Multicam statt der alten Camo-Box-Projektion)
- Beweis-Thumbs: barrier_001 (weiss/rot wie Pack), cactus_001 (Pack-Gruen
  27,94,32 per UV-Sampling belegt), tent_001 (Palette + Multicam-Flaechen),
  apc_002 (Sand/Multicam)
2026-10-19 23:05:47 +00:00
Hermes (scopedvr-dev)
967e4dc54a Mehrfachauswahl gemaessigt (t_bceb0965): Gemeinsames Verschieben aller markierten Objekte relativ — Anker-Objekt fuehrt, Abstaende bleiben exakt erhalten (Probe: 3 Objekte, Delta (+4,-2), Distanzen 6.325/5.831/7.616 vor UND nach). Entf loescht die GESAMTE Auswahl (vorher: nur letztes Objekt), mit einem Undo-Schritt fuer die Gruppe. Undo: 1 Schritt stellt ALLE Objekte gemeinsam wieder her (vorher: 3 Einzelschritte); Redo groupen-basiert. Drehung/Scale-Eingabe bei Mehrfachauswahl: '—' eintippen setzt ALLE auf denselben Wert, Position bleibt relativ. Einzelauswahl unveraendert (Gizmo-Zug ist gizmo-gated und nutzt gemeinsam-ziehend-Pfad nicht). Tests: tests/probe_multi_move.gd neu (12 Checks: Positionen, Abstaende, Undo/Redo, Entf), tests/test_inspector.gd 47/47 gruen (T5 erweitert), test_ui_scale 51/0 gruen, --import 0 Parse-Fehler. 2026-10-19 21:57:28 +00:00
Hermes Agent
370917f96e D11 (t_c8748efc): Thumb-Neurender mit Camo-Fix — 292 Thumbs neu gerendert, gen_thumbs.gd lädt Multicam-Texturen manuell (GLTFDocument kann externe URIs in Sibling-Repos nicht laden). 2026-10-19 21:31:57 +00:00
Hermes (Operator)
e664d09666 Map Builder Inspektor Teil 2 (t_e0120dc2): Achsen-Gizmo — drei farbige Pfeile (X rot / Y gruen / Z blau) am einzeln markierten Objekt, ziehbar; Zieh-Werte rasten auf die Schrittweite (Mausrad waehrend des Zugs blaettert fein 0.1 / normal 0.5 / grob 2.0, SpinBox 'Schrittweite' im rechten Panel, ueberlebt UI-Rebuilds); Inspektor-Zahlen folgen live; Y-Zug klemmt am Terrain, X/Z folgen dem Boden; Esc rollt exakt auf die Startposition zurueck (kein History-Eintrag), Loslassen committet EINEN Undo-Schritt; nur Einzelauswahl. Neues src/viewport/gizmo.gd haengt als Kind von interaction.gd (Input-Prioritaet), interaction.gd unveraendert. OPERATOR-NACHTRAG: dieser Stand lag nur als Karten-Anhang vor (nie committet) — vom Operator uebernommen und verifiziert: --import 0 Parse-Fehler, test_gui_smoke 89 OK, test_inspector 33 OK, test_ui_scale 51/0, test_library 101 OK. 2026-10-19 21:29:09 +00:00
Hermes (scopedvr-dev)
9fa8029910 Farbe je Objekt (t_b0edb807): Inspektor-Abschnitt 'Farbe / Material' mit 7er-Palette (Wüstensand/Oliv/Tarn-Grün/Stahlgrau/Holzbraun/Dunkelbraun/Weiß) + 'Zurücksetzen (Pack-Farbe)'; Mehrfachauswahl = EIN Undo-Schritt; Speichern als OPTIONALES Objekt-Feld tint:[r,g,b] (Anzeige-sRGB, 4 Stellen — Rückwärtskompatibel, Karten ohne Feld unverändert). Neuer Helfer src/svmap/svmap_tint.gd (1:1-Spiegel im Client, gemeinsame Semantik Editor+Spiel): albedo_color-ERSATZ via Surface-Override-Kopien — Original-Materialien bleiben unangetastet, Auswahl-Orange (material_override) gewinnt weiter; set_selected_multi re-synced Tint beim Abwählen, tint_refresh für Inspector-Änderungen ohne Rebuild. Map-View-Bauer wendet tint beim Aufbau an (echte GLBs UND Platzhalter). tests/test_map_tint.gd neu (31 Checks grün), Editor-Gesamtsuite 18/18 (gui_smoke-F5-Artefakt per Gegenprobe entkräftet, updater falsch-rot im Sammel-Filter). Rauheit/Metallisch bewusst weggelassen (Begründung im Handoff/BEWEIS.md). 2026-10-19 20:54:34 +00:00
Hermes (scopedvr-dev)
c8b9c59fda Objekt-Inspektor rechts (t_bb0d11c9): Position/Drehung/Größe tippbar+ziehbar (Godot-Konvention: am Label ziehen), Mehrfachauswahl mit „—“ bei ungleichen Werten, JEDE Änderung = 1 Undo-Schritt (Tippen beim Bestätigen, Ziehen beim Loslassen), live im 3D (Marker-Sync ohne Rebuild, Rescale inkl. Pick-Metas), Auf-Boden-Setzen über vorhandene Logik, Info-Block (Fußpunkt/Kollision/echtes Modell), Kopf mit N von M + Poly. Scale via UIScale (alle 3 Stufen layout-geprüft @2328x1310). Drive-by-Fixes (Vorbestand): Tree.ensure_visible → Tree.scroll_to_item (SCRIPT ERROR bei jeder Auswahl) + sel_label füllt jetzt bei select_object/select_many. tests/test_inspector.gd neu (35 Checks), Suiten grün. 2026-10-19 20:02:14 +00:00
Hermes (scopedvr-dev)
366d482a1a A6-Aufraeum (t_a6a96e4c): Beispiel-Faesser entfernt — Thumbs + .import geloescht, Registry-Sync (345 Objekte, note erhalten), probe_store_a6 auf barrel_001 umgezogen 2026-10-19 20:00:52 +00:00
Hermes (scopedvr-dev)
c8a41c0794 UI-Größe: zentrale Skalierung Klein/Mittel/Groß (t_297221b5) — Top-Bar-Menü, sofort wirksam (Rebuild), gespeichert in user://scopedvr_editor.cfg [ui]; Fenster startet maximiert + merkt Lage; Panel-Min-Maße/Schrift an einen Faktor gekoppelt; Layout-Überlauf-Fixes (Hilfe-Hinweis Umbruch+Grow-up, Statuszeile/Top-Bar-Ellipsis, linkes Panel scrollbar) 2026-10-19 19:21:45 +00:00
Hermes (scopedvr-dev)
f4748af371 D9 (t_9962eba7): 293 Bibliotheks-Thumbs neu gerendert — Kategorie-Farbe statt ausgewaschen
Folge der Farbraum-Korrektur im Client (f04b515): gen_thumbs laedt die GLBs
direkt aus ../scopedvr-client/assets/objekte, deshalb zeigen die 293
Military-Thumbs jetzt den gewuenschten Anzeige-Wert (Panzer oliv statt fast
weiss, Waende tan, Kisten holzbraun). Alt-Bestand-Thumbs byte-identisch
(Gegenprobe: genau 293 von 347 PNG geaendert).
2026-10-19 19:08:59 +00:00
Hermes (scopedvr-dev)
ccf1c43f2e D8 (t_ba4278f8): Thumbs neu gerendert (347 Objekte, 27 Bilder neu) nach dem Alt-Bestand-Fusspunkt-Bake
Stichprobe angesehen: baum_kiefer_1, stein_mittel_2, box_018, tank_001 - Objekt vollstaendig und mittig im Bild.
2026-10-19 19:04:34 +00:00
Hermes (scopedvr-dev)
43636c840f Bibliothek: 96-px-Kacheln, Kategorie-Gruppen, klappbar + gemerkt, Suche (t_59fc0f21) 2026-10-19 18:49:26 +00:00
Hermes (scopedvr-dev)
91e2b03843 PROBE entfernt (t_f73a1cc5): Nachweis abgeschlossen 2026-10-19 18:41:56 +00:00
Hermes (scopedvr-dev)
2bd1e671e4 PROBE (t_f73a1cc5): lose-Datei-Kollision nachstellen — wird nach dem Beweis entfernt 2026-10-19 18:41:27 +00:00
Hermes (scopedvr-dev)
90c3c7fdc7 Selbst-Update: lose/geänderte Dateien werden automatisch beiseitegelegt (t_f73a1cc5)
EditorUpdater.pull: vor dem pull --ff-only git stash push -u (erfasst
GEÄNDERT + UNTRACKED — genau die losen .import-Dateien, die am 19.10.
den Update-Knopf blockierten). Nach dem Pull zurückholen (apply+drop);
kollidiert das Zurückholen: reset --hard gegen halb gepoppte Zustände,
Stash bleibt als Sicherung, ehrliche Klartext-Meldung in der Statuszeile
statt roher Git-Zeile. Alte Auto-Stashes gleicher Kennung werden beim
Anlegen verworfen (keine Anhäufung). main.gd zeigt die Klartext-Info.

tests/test_updater.gd: Negativprobe (nackter pull bricht an untracked
vs. getrackt ab), Fix-Probe (läuft durch), geänderte Datei bleibt
erhalten, Konflikt-Fall mit Sicherungs-Stash, Anti-Anhäufung. 69 checks.
2026-10-19 18:41:19 +00:00
Hermes (scopedvr-dev)
3e07e91f5a Revert "Pack-Texturen (t_9675e48d): Editor-Viewport + Thumbs texturiert"
This reverts commit c0cbe4bb0c.
2026-10-19 18:33:15 +00:00
Hermes (godot-vr)
785a858ae0 Thumb-.import vervollstaendigt (30 Fahrzeug-Thumbs, t_9675e48d Nachziehen) 2026-10-19 18:31:42 +00:00
Hermes (godot-vr)
c0cbe4bb0c Pack-Texturen (t_9675e48d): Editor-Viewport + Thumbs texturiert
GlbMeshStore setzt fuer texturlose Military-GLBs die Pack-Palette-Textur
(Atlas per absolutem Pfad aus dem Client-Mesh-Lager, NEAREST + Mipmaps),
gen_thumbs nutzt denselben Helfer → Thumbs zeigen die echten Palette-
Farben statt weisser Silhouetten. 261 Thumbs neu gerendert (317 gesamt).
Beweis: tests/probe_pack_texturen.gd headless — 7/7 Flaechen texturiert.
2026-10-19 18:26:47 +00:00
Hermes (scopedvr-dev)
e1e07e57b0 D6 (t_31e800f2): Editor-Sync — 30 Fahrzeug-Thumbs + Map-Builder-Nachweis
- library/objects.json = 347 Objekte (Merge-Stand Match-Server)
- library/thumbs/: 30 neue Thumbs (tank/apc/air_defense/antenna), in
  Kategorie-Farbe Oliv (D0-Muster) — uebrige 317 Thumbs byte-identisch
- tests/probe_d6_mapbuilder.gd: 3 Fahrzeuge ueber den ECHTEN Map Builder
  platziert (Panzer + Turm exakt auf dem Turm-Ring 1,5929 m + Transportpanzer,
  dazu die Kanone auf 2,1273 m), Kollisionsboxen als Drahtgitter,
  PNG 1600x1000 — PROBE_D6_MAPBUILDER_OK
2026-10-19 18:25:15 +00:00
scopedvr-dev
8f598dc67d Test-Fix (t_03d0f442): test_library-Katalog-Check datengetrieben statt harte 54
Die harte Erwartung lib.size() == 54 (15 Basis + 39 Natur-Update) brach bei
JEDER Kategorie-Karte: schon der A6-Stand hatte 56 (2 Beispiel-Objekte), nach
D1-D5 sind es 317. Der Check ist jetzt datengetrieben:

- Basis-Bestand bleibt garantiert: lib.size() >= 54 (Merge-Fundament A6) —
  der Katalog darf nur wachsen, schrumpfen failt weiterhin.
- Neu: Registry <-> Thumbnails deckungsgleich (je OID ein 128px-Thumb, keine
  verwaisten Thumbs) — fängt vergessene/abgebrochene sync_objects-Läufe.
- Neu: OID-Vergleich gegen die gemergte Match-Server-Registry
  (../scopedvr-match-server, Quelle der Wahrheit aus objects.d/*), wenn das
  Sibling-Repo liegt; ohne Sibling verkürzt sich der Test statt zu failen.
- Testgruppe Registry-Fußpunkt: "alle 54 Objekte" -> dynamische Zahl im Titel.

Suite grün auf main: 95/95 checks (ist 317). Beweis Drift-Erkennung: eine
injizierte Fremd-OID in der MS-Registry failt den neuen Vergleich gezielt.
2026-10-19 18:23:56 +00:00
Hermes (scopedvr-dev)
6572f1e10c D0 (t_47f05319): 317 Thumbs mit Kategorie-Farben neu gerendert + Thumb-Licht entschärft
- gen_thumbs: ambient 0.7->0.35, sun 1.1->0.85 — helle Albedos überstrahlten
  sonst zu Weiss im 128px-Thumb
- alle 317 Thumbs neu (263 Military-Desert-GLBs mit baseColorFactor-Farben)
2026-10-19 18:22:32 +00:00
Hermes (scopedvr-dev)
6f81add8f4 D7 (t_f77bc681): Editor-Notiz wiederhergestellt + .import-Dateien der Thumbs vervollstaendigt
* library/objects.json: D4's Sync (6c65b13) hatte die Editor-eigene `note` mit
  der Server-Notiz ueberschrieben (c92d35d/29b1668/0f86182 tragen alle die
  Editor-Notiz). Registry-Inhalt unveraendert: 317 Objekte, Merge --check gruen.
* library/thumbs: 147 fehlende .import-Dateien nachgezogen (88 Natur-Thumbs aus
  D5 + 59 Prop-Thumbs aus D4) — Godot regeneriert sie sonst bei jedem Lauf neu
  und hinterlaesst ungetrackte Dateien. Die Thumb-PNGs selbst sind unveraendert
  (317 PNG, Thumb-Zahl == Objekt-Zahl).
2026-10-19 18:15:22 +00:00
Hermes (scopedvr-dev)
6c65b13a8f D4 (t_691b51ce): Editor-Sync — 59 neue Prop-Thumbs + Map-Builder-Probe (3 Props mit Kollisionsboxen) 2026-10-19 18:11:07 +00:00
Hermes (scopedvr-dev)
b288e8a1e8 D3 (t_6690c59c): .uid der Render-Probe nachgezogen 2026-10-19 18:09:34 +00:00
Hermes (scopedvr-dev)
c92d35dca2 D3 (t_6690c59c): Render-Probe — Gebaeude in einer Reihe, Kamera naeher (Sichtpruefung Fusspunkt/Groesse) 2026-10-19 18:08:40 +00:00
Hermes (scopedvr-dev)
29b1668f57 D3 (t_6690c59c): Gebaeude in der Editor-Bibliothek (93 Objekte, 37 neue Thumbs) + Map-Builder-Render-Probe 2026-10-19 18:08:05 +00:00
Hermes (scopedvr-dev)
56a808f052 D1 (t_78746c97): Waende in der Editor-Bibliothek (Registry-Kopie + 50 Thumbs) + Render-Probe 2026-10-19 18:07:02 +00:00
Hermes (scopedvr-dev)
c0d426152e D2 (t_33250710): Editor-Bibliothek 83 Objekte + 27 Absperrungs-Thumbs + Renderprobe
- library/objects.json: Editor-Kopie (note behalten), 83 Objekte.
- library/thumbs: 27 neue Thumbs (echte GLB-Meshes), Alt-Thumbs unveraendert.
- tests/probe_d2_barriers_render.gd: Map-Builder-Beweis — 3 Objekte ueber
  map_view_builder platziert, PNG gerendert, Fußpunkt/Groesse gemessen.
2026-10-19 18:06:55 +00:00
Hermes (scopedvr-dev)
ebe3dd276b D5 (t_e81aed01): Render-Nachweis tools/probe_d5_render.gd (Fels/Kaktus/Stein/Decal platziert) 2026-10-19 18:06:39 +00:00
Hermes (scopedvr-dev)
0f86182a2c D5 (t_e81aed01): Editor-Sync — Bibliothek 144 Objekte + 88 Natur-Thumbs (gen_thumbs) 2026-10-19 18:05:41 +00:00
Hermes (scopedvr-dev)
310aa695e6 A6 (t_c5a27ca7): Editor loest Kategorie-GLBs aus der Registry auf (GlbMeshStore + gen_thumbs), Thumbs fuer Beispiel-Objekte 2026-10-19 18:02:12 +00:00
Hermes (scopedvr-dev)
76f8143540 A6 (t_c5a27ca7): Merge-Script ortsunabhaengig, Editor-Sync importiert vor Thumbs, Client-Probe belegt Registry-GLB 2026-10-19 18:01:08 +00:00
Hermes (scopedvr-dev)
9dca88e10e A6 Fundament (t_c5a27ca7): Fragment-System + Merge + Editor-Sync fuer Baukasten-Objekte
library/objects.json ist generiert: library/objects.d/*.json (ein Fragment pro
Kategorie) + tools/merge_object_library.py (idempotent, --check, --aabb-Messung,
--new-fragment). base.json = Alt-Bestand (54 Objekte) unveraendert. Beispiel-
Kategorie beispiel_fass_01/02 beweist die Kette end-to-end. Doku:
docs/objekt_fragmente.md.
2026-10-19 18:00:24 +00:00
Slyphax-Hermes
1cfc4e8c3a Update-Test-Zeile aus dem H-Overlay entfernt — Selbst-Update-Kette und Restart-Fix live verifiziert (17.10., 2 Zyklen) 2026-10-17 21:56:43 +00:00
Slyphax-Hermes
f6f5ef6c90 Selbst-Update-Test Zyklus 2: Restart-Verifikation — soll die APP selbst neu öffnen (Fix f99d1e2 im Speicher) 2026-10-17 21:54:23 +00:00
Slyphax-Hermes
f99d1e2a28 Fix Selbst-Restart (Live-Befund 17.10.): Update-Restart ging auf den Godot-Editor statt die Map-Editor-APP — self_restart_args ohne --editor (die App startet über main.tscn mit nacktem --path), Kommentar + test_updater angepasst 2026-10-17 21:53:37 +00:00
Slyphax-Hermes
23f7e1d39c Selbst-Update-Test 17.10: sichtbare Test-Zeile im H-Overlay — Sly soll Update-Check + Jetzt-updaten live ziehen 2026-10-17 21:50:40 +00:00
Hermes (godot-vr)
4c960e2587 test_testrun: Stale-Export-Falle behoben (t_2a90ceab): _cleanup purgt user://testrun KOMPLETT statt nur Testmap.svmap — eine Stale-Export-Datei fremden Owners (Root-Testlauf mit HOME=/home/slyphax erzeugt root-gehoerige Dateien in slyphax-Appdata; ein slyphax-Lauf kann sie nicht ueberschreiben) liess save_to_file still mit Datei-nicht-schreibbar (Fehler 12) scheitern, waehrend Temp-Datei-existiert + Roundtrip dank der alten Datei faelschlich gruen blieben — darum wirkte der 1/14-FAIL pre-existing ohne jeden Code-Bezug. Der Ordner-Owner darf unlinken, der Purge raeumt darum auch root-Dateien weg; zusaetzlich druckt der Export-Check jetzt die save-Fehlerliste (Diagnose ohne Probe-Script). Verifikation: Suite als slyphax UND root je 14/14 ALL OK; Fehlermeldung per Probe als slyphax bestaetigt. 2026-10-17 22:55:05 +02:00
slyphax
73847ed1ce Map-Editor Selbst-Update gehärtet (t_1457e935): KEIN Credential-Dialog mehr — git-Läufe nie interaktiv — Root-Cause: der systemd-User-Manager exportiert SSH_ASKPASS (ksshaskpass); ein nackter git-Spawn im Editor hat damit mitten in der Update-Prüfung das KDE-Login-Fenster geöffnet, und der Credential-Store (~/.git-credentials) war leer (0 Bytes). Fix (Belt AND Braces, EditorUpdater.spawn_argv): JEDE git-Ausführung läuft über /usr/bin/env mit GIT_TERMINAL_PROMPT=0 + GIT_ASKPASS=/bin/true + HOME aus der eigenen Umgebung (kein Hardcode), dazu -c credential.helper= (Helper-Reset, sonst öffnet z. B. der Windows-Credential-Manager sein GUI) + -c credential.helper=store + core.askPass=/bin/true (Askpass-Fallback tot, auch ohne env-Wrapper). Askpass-Präzedenz am PC verifiziert: core.askPass/GIT_ASKPASS schlagen geerbtes SSH_ASKPASS — Fake-Askpass startet nicht, sauberes Authentication failed Exit 128 statt GUI. Neuer Status auth_failed mit klarem UI-Text („Updates nicht prüfbar — Zugangsdaten fehlen (kein Login-Dialog geöffnet)“), Auth-Erkennung pure (looks_like_auth_error: could-not-read-Username / Authentication failed / terminal prompts disabled / Unauthorized — Connection refused ist KEIN Auth-Fehler); pull-Fehler bekommen denselben Zusatz-Hinweis. Credential-Store am PC befüllt (Forgejo-Token für 127.0.0.1:13020, chmod 600) — ls-remote/pull/ls-remote-push laufen stimmlos. Tests: test_updater +10 Checks (auth-Erkennung 4, spawn_argv-Zusammensetzung 6) → 47/47 ALL OK; komplette Suite 12/13 grün (test_testrun: 1/14 FAILED — PRE-EXISTING, per git-stash-Verfahren verifiziert, Ursache unabhängig von diesem Diff, Follow-up-Karte folgt). docs/tasten.md: Abschnitt Updates um „Zugangsdaten fehlen“ ergänzt. 2026-10-17 22:49:42 +02:00
Hermes (godot-vr)
f2b4f6a824 Map-Editor Selbst-Update (t_d95f087a): Start-Check + Banner + Jetzt-updaten + Pruef-Button — EditorUpdater (src/editor/updater.gd): stiller Check beim Start (git ls-remote origin/main vs. lokaler HEAD, keep-alive-leicht), Bernstei-Banner „🔄 Update verfügbar: alt → neu“ mit Knopf „Jetzt updaten“, Datei-Menü-ID 7 „🔄 Updates prüfen“ mit Statuszeilen-Ergebnis (Update verfügbar / auf dem neusten Stand / Nicht erreichbar). Update-Ausführung: Dirty-Schutz via _request_lossy (derselbe deutsche Dialog wie Strg+O/N, Abbrechen bricht ab), dann git pull --ff-only (Tunnel + credential store) → Godot-Import-Lauf (--headless --import) → Neustart. Import-Ausgabe wird geparst: Parse-/SCRIPT-Fehler trotz Exit-Code 0 = Abbruch mit Meldung, KEIN Neustart (Lektion 17.10. class_name-Cache). Restart plattformneutral: cfg [update] restart_command > systemd-User-Unit map-editor auto-erkennung (systemd-run --on-active=3s Kick im eigenen Scope, überlebt den Unit-Neustart) > Selbst-Restart (dieselbe Godot-Binary, --editor --path). Keine Linux-Pfade hartkodiert; git_command aus cfg möglich. Offline: kein Banner, kein Fehler-Geballer. Ein-Hintergrund-Thread je Richtung (check/update) mit Busy-Guard, Thread-join über call_deferred-Ordnung. Tests: neue Suite test_updater (37 Checks: Check divergiert/gleich/unreachable/kein-Repo, Banner-Texte, Pull gegen echte Temp-Klone, Import-Abbruch-Pfad inkl. Exit-0-Falle, Restart-Auflösung), Suite 13/13 ALL OK (997 Checks). docs/tasten.md: Abschnitt „Updates“; H-Overlay-Zeile. 2026-10-17 19:38:07 +00:00
Hermes (godot-vr)
2065ef2252 Map-Editor R3 (t_c863b93b): Kartengröße-Dialog + Menü + Undo-Verdrahtung — neuer Dialog „Kartengröße ändern“ (Datei-Menü, Stil wie new_map_dialog): aktuelle Größe/Auflösung, Freifelder X/Z (256–4096 clamp) + Preset-Buttons 256/512/1024/2048, Live-Preview aus resize_preview (R2): Verlust-Liste beim Verkleinern (Objekte/Bauteile/Spawn-Punkte draußen) mit Entfernen-Schalter, V9-Warnung (Drop aus = Map nicht speicherbar), Spawn-Speicher-Warnung (< 4 Punkte/Team, V11), Warnband beim Vergrößern über ~1024 m (Dichte-Deckel 513 → m/Punkt, Quest-Perf). Ausführen: resize_apply → doc IN-PLACE ersetzt (History-Referenz bleibt gültig) als EINE Undo-Einheit (kompletter doc-Snapshot), Dirty-Flag, Terrain/Viewport-Rebuild + Kamera-Refit. ActionHistory: snapshot/_restore_block vertragen jetzt skalare Ober-Keys (format_version/weather) → Ganz-doc-Snapshots möglich. Tests: neue Suite test_resize_dialog (40 Checks: Dialog-Aufbau/Clamp/Preview-Texte, Menü-Verdrahtung, Undo-Roundtrip BYTE-IDENTISCH, Redo, Drop-aus-Pfad), komplette Suite 12/12 ALL OK (960 Checks). docs/tasten.md: Abschnitt „Kartengröße ändern“. 2026-10-17 19:01:15 +00:00
Hermes (godot-vr)
116acd63ed Map-Editor R2 (t_27ba6c37): Verkleinern + Out-of-Bounds-Liste + Filter — resize_preview() listet je Kategorie (Objekte nach Anker-Regel, Bauteile nach Fußabdruck-"punkte" mit Rotation, Spawn-Punkte nach Punkt-Regel; Kante exakt = drin, Rand-Überlapper bleiben unange tastet), resize_apply(doc, x, z, drop_outside) resampelt wie R1 und entfernt bei Bedarf genau die gelisteten Einträge (Eingabe-doc bleibt rein). Shrink 512→256: Terrain an Weltposition 1:1 (±0.05 m), res bleibt 129 (2 m/Punkt). Tests: 19 neue Checks in test_terrain (Preview/Filter/Roundtrip), 11 Suiten ALL OK (920 Checks). 2026-10-17 18:56:10 +00:00
Hermes (godot-vr)
2d8b9822f0 Map-Editor R1 (t_0d436687): Resize-Kern + Vergrößern — SvmapDocument.resize_terrain(), Anker Mitte/Rand flach, Dichte-Leiter 129→257→513.
Semantik aus PLAN.md „Map-Größe ändern" (17.10., Sly-geankert):
- Anker Mitte: Karte bleibt am Ursprung zentriert, bestehendes Terrain behält
  seine WELTPOSITION (bilinear resampeln, kein Stretch).
- Neue Randzone: flach auf min_height + Material 0 (Gras), wie create_new.
- Dichte-Regel: würde m/Punkt über ~2 sinken, res hoch 129→257→513
  (Format-Limit 513 hart — darüber Warnband, Anzeige in R3).
- size clamp 256–4096 m (MIN_SIZE_M/MAX_SIZE_M), bounds auf neue Größe
  (Y-Grenzen unverändert).
- R1-Schnitt: spawn_zones/objects/gebaeude unverändert übernommen (Filter +
  Out-of-Bounds-Dialog kommen in R2). Reine Funktion — Eingabe-doc bleibt
  unangetastet (Undo-Snapshot), alte Prüfsumme verworfen (save signiert neu).

Tests (test_terrain.gd, 22 neue Checks → Suite 134/134, 11 Suiten ALL OK):
Grow 256→512 — alte Gridpunkte ±0.01 m an Weltposition, Rand flach/Gras,
Material resampelt, res-Bump 129→257 (bei 2048 m gedeckelt auf 513),
spawn/object-Erhalt, svmap-v2-Roundtrip, Clamp.
2026-10-17 18:48:21 +00:00
Hermes godot-vr
b85e3bed52 Map-Editor: Höhenlinien nur im Terrain-Werkzeug (t_89c0ec95)
Sly-Wunsch 17.10.: "die Dauersicht stört" — die Konturen sind genial, aber
nur im Terrain-Werkzeug (F1) gewünscht.

Kopplung am Werkzeug-Wechselpunkt: _set_tool_mode, _after_structure_change
und _recheck_terrain_mode eichen die Konturen NACH (effektiv = Nutzer-Fix
ODER Terrain-Werkzeug). Kein _process-Poll — die Umschaltung passiert
synchron im selben Aufruf, hart ohne Fade (billig und deterministisch).

NEU: Taste L (Action editor_contours_toggle; L war sonst unbesetzt —
Kollisions-Prüfung im Test) und der Button „≈ Höhenlinien" fixieren die
Linien als NUTZER-Wahl dauerhaft in jedem Werkzeug. Default = Fix AUS,
Linien erscheinen also nur bei F1.

Regression aus dem Fundus gefixt: _after_structure_change baute
terrain_view neu OHNE Kontur-Nacheich (das Gitter hatte ihn, die Linien
nicht) — nach jedem Strg+Z kehrte die Dauersicht zurück. Der Rebuild
bekommt jetzt denselben Nacheich wie das Gitter.

Das Relief-Shading (Hillshade + Höhen-Kurve) bleibt davon unberührt und
immer an. Auf flachen Maps (< 2 m Relief) schaltet der Shader weiter
selbst auf aus.

Doku: docs/tasten.md (L-Zeile in Werkzeuge + Kopplungs-Absatz) und
H-Overlay-Zeile (help_text.gd).

Tests: test_shortcuts +5 Checks (Action registriert, physische Taste L,
Kollisions-Freiheit, L ≠ F1/H). test_gui_smoke +21 Checks am ECHTEN main:
Start AUS · Undo/Rebuild bleibt AUS (Root-Cause-Regression) · F1 ein →
F2/F3/F4 aus (synchron, 0 Frames Poll) · L-Fix an/aus inkl. Button-Sync
und Statusmeldungen · Button-Weg ditto.
Suite 11/11 ALL OK: ux 137, shortcuts 203, editor_svmap 31, terrain 112,
library 94, glb_view 28, spawns 52, testrun 14, bauen 87, gui_smoke 89,
rmb_vorrang 32.
2026-10-17 18:24:45 +00:00
Hermes (godot-vr)
d6a0fe2196 Relief-Nachreich (t_3ebe4086): relief_color klammaert Kanaele auf 0..1 — der Mesh-Speicher ARRAY_COLOR/RGBA8 clamp't genauso, der Helfer muss die Render-Wahrheit liefern, sonst liegt der Probe-Vergleich um >0.05 daneben. Live-Material-Test t_55ed041a erwartet jetzt dieselbe relief_color-Formel statt der alten 0.72..1.15-Konstanten. Suite: test_terrain 112/112, 11 Suiten ALL OK. 2026-10-17 18:06:39 +00:00
Hermes (godot-vr)
ffb27645d4 Terrain-Relief lesbar machen (t_3ebe4086) — Sly-Befund 17.10. "Erhoehungen/Vertiefungen kaum erkennbar, alles knallgruen und hell".
1) Hoehen-Faktor kontrastreicher kalibriert: S-Kurve t*t*(3-2t) statt linear, Spanne 0.72..1.15 -> 0.58..1.28 (Senken deutlich dunkler, Huegel deutlich heller).
2) Pseudo-Licht (Hillshade) in den Vertex-Farben: Terrain-Normale x feste Sonnenrichtung RELIEF_SUN=(0.45,0.75,0.5) (seitlich-hinten fuer den Default-Blick), Licht-Faktor 0.62..1.22, Gesamtklemme 0.40..1.35. Formel/Konstanten IDENTISCH zum Match-Client (svmap_terrain.gd im scopedvr-client-Repo) — relief_color() static, Material-Farben unangetastet.
3) Editor-only Extra: Hoehenlinien alle 1 m + kraeftige 5-m-Indexlinie (terrain_contour.gdshader, UV2.x = Vertex-Hoehe, fwidth-stabil, unshaded). Der Quest-3-Vertrag "kein Custom-Shader" gilt fuer das SPIEL — der Editor darf teurer sein (DECISIONS-Konvention); der Match-Client bleibt StandardMaterial3D. Flach-Map-Schutz: Linien erst ab 2 m Gesamtrelief, UI-Toggle "≈ Hoehenlinien" (Default AN, nach jedem Rebuild re-synchronisiert).
Material unshaded: das gebackene Shading ist exakt das sichtbare (kein Doppel-Shading mit dem Viewport-Sonnenlicht), deterministisch fuer die Render-Probe.

Tests: test_terrain +19 Relief-Checks (99 gesamt): Tal/Huegel-Delta > 0.10 UND > 1.2x alte Kalibrierung, Hang-zur-Sonne heller, Stufen-Kanten-Delta > 0.08 UND > 1.2x alt, Materialfarben (Gras/Sand/Fels) unterscheidbar, Alpha=1.0, Kontur-Toggle + Flach-Map-Schutz. Suite 11 Suiten ALL OK: test_ux 137, shortcuts 198, editor_svmap 31, terrain 99, library 94, glb_view 28, spawns 52, testrun 14, bauen 87, gui_smoke 68, rmb_vorrang 32. (Hinweis: gui_smoke-F5 schlug in DIESEM Workspace nur wegen des scopedvr-client-Symlinks neben dem Editor auf — TestRun.find_client findet den Client dann wirklich; ohne Symlink = Standard-Layout: 68/68.)
Renderprobe (xvfb, opengl3): tests/probe_terrain_relief_render.gd — Stufen-Delta 0.149 (>0.10), Hoehenlinien Pixel-Diff 0.167, Huegel/Senke-Fenster-Delta 0.225 — ALL OK 3/3; PNGs im Scratch (terrain_relief_stufe.png, terrain_relief_demo.png).
Housekeeping: fehlende .png.import-Sidecars der M2-Natur-Thumbs nachgereicht.
2026-10-17 18:05:20 +00:00
Hermes (godot-vr)
e1c737df61 Terrain: Material-Farben bleiben LIVE während Heben/Senk-Stroke (t_55ed041a) — Live-Rebuild bekommt die Material-Schicht mit
Sly-Befund: Beim Heben/Senken über gemalten Sand/Fels verschwanden die
Material-Farben live (alles Gras), erst am Stroke-Ende kehrten sie mit
dem Vollrebuild zurück.

Root-Cause: terrain_tool rief im Live-Pfad (Heben/Senken/Glätten)
update_terrain(_heights) OHNE Material-Schicht auf — _mats wurde nur im
Malen-Modus dekodiert — und terrain_mesh fällt bei leerer Schicht auf
alles-Gras zurück (Legacy-Fallback).

Fix (zwei Ebenen):
- terrain_tool._refresh_from_doc dekodiert die Material-Schicht immer,
  der Live-Stroke übergibt sie mit (Heben/Senken/Glätten wie Malen).
- terrain_mesh cached die zuletzt gesehene Schicht: Live-Updates ohne
  mats-Param nutzen denselben Farb-Pfad wie der Idle-Rebuild; Legacy-v1
  (nie gesehene Schicht) bleibt alles Gras — kein Phantom-Cache.

Test: tests/test_terrain.gd neue Gruppe „Live-Material-Farben" —
per-Frame-Check (5 Frames Heben über Sand-Zone, nicht nur Endzustand),
terrain_tool-Integration (Heben-Stroke übergibt Sand-Schicht, Stroke-Ende
= EIN Commit), Legacy-Gras ohne Phantom-Cache. Suite: 99 checks ALL OK;
alle weiteren Headless-Suiten grün (test_glb_view 12/22 FAIL ist
vorbestehend auf origin/main — GLB-Quellen fehlen im Workspace).
2026-10-17 17:51:07 +00:00
Hermes (godot-vr)
db6bcec971 Flugmodus-Tasten: Leertaste=hoch, Shift=runter, Strg=schneller (Sly 17.10., Shooter-Konvention)
- InputMap (shortcut_keys.gd): editor_fly_up=Space, editor_fly_down=Shift,
  editor_fly_fast=Ctrl (vorher E/Q, Shift=fast). E/Q sauber entfernt, keine
  Aliasse (Sly-Freispruch). Alt=langsamer unveraendert, Mausrad bleibt
  Grundtempo.
- Konflikt-Check (verpflichtend): Space/Shift/Ctrl allein triggern KEINE
  Werkzeug-Aktion — neue Testgruppe prueft InputMap-Matches (kein Undo/Save/
  Redo/Pinsel/Freelook-Toggle ohne Combo-Partner) + echtes Terrain-Werkzeug:
  Tasten-Events starten keinen Strich. Strg+LMB-Senken bleibt LMB-gebunden,
  Entf/Strg+Z unangetastet.
- orbit_camera._enter_fly gibt den GUI-Fokus frei, damit Leertaste keinen
  fokussierten Button ausloest (Space=ui_accept, Bibliothek=Platzierung).
- Livetest: Leertaste hebt/Shift senkt die Kamera im Flug (orbit_camera).
- Docs: docs/tasten.md, H-Overlay (help_text.gd), README, Status-/Hilfe-
  Texte der Kamera aktualisiert.
- Test-Hygiene: _test_camera_fly_roundtrip ist jetzt Coroutine (await),
  view_yaw-Orbit-Toleranz 0.01 Grad (float32-Rundtrip-Drift ~2e-4 Grad/Exit).
- Suite: 11 Suiten gruen (test_shortcuts 198 checks).
2026-10-17 17:45:37 +00:00
Hermes (godot-vr)
d6c472603f Build-Tool: ray_pick ohne 500-m-Cap (t_555e33da) — Umschalt+Klick/Baustapel-Pick funktioniert jetzt auch im Default-Framing (~614 m Kamera-Distanz); Workaround-Zoom im RMB-Test entfernt. 2026-10-17 17:39:30 +00:00
Hermes (godot-vr)
7521db7f8d Map-Editor: RMB = Flug mit absolutem Vorrang (t_2470de89) — Terrain-Senken auf Strg+LMB, Hand-Objekt-Abwurf auf Esc.
- Terrain-Pinsel: Rechtsklick startet keinen Strich mehr; Strg+LMB invertiert
  (Heben↔Senken, Blender/Photoshop-Konvention; Malen: Strg+Klick = Gras zurück).
- Platzieren: RMB bricht nicht mehr ab — Objekt bleibt in der Hand, Abwurf nur
  über Esc. build_tool/spawn_tool: RMB-Deselect/Werkzeugwechsel entfernt
  (Auswahl-Deselect = Esc / LMB auf Leere).
- orbit_camera: rmb_filter entfernt — Flug startet in JEDEM Werkzeugzustand.
- docs/tasten.md + H-Overlay + Status-/Paneltexte aktualisiert.
- Neuer Test tests/test_rmb_vorrang.gd (32 Checks, echte Main-UI headless);
  in tools/run_tests.sh aufgenommen. Suite grün (test_glb_view: 12/22 FAIL auch
  auf origin/main — fehlende GLB-Quellen im Scratch-Workspace, umgebungsbedingt).
2026-10-17 17:37:09 +00:00
slyphax
cda6f48b01 GLB-View (t_9b50b231): echte Modelle im Editor-Viewport — Platzhalter nur als Fallback.
- GlbMeshStore (neu): laedt OID+.glb aus konfigurierbaren Quellen
  (ProjectSettings scopedvr/editor/glb_quellen, Voreinstellung =
  Sibling-Repos, wie gen_thumbs; plattformneutral, relative Pfade),
  backt ALLE MeshInstance3D der Szene in EIN normalisiertes ArrayMesh
  (Fusspunkt y=0, XZ zentriert — wie scopedvr_export.normalisiere),
  Materialien/Texturen bleiben je Oberflaeche erhalten.
- Cache pro OID (Parse nur einmal; fehlende Datei ungcached, Parse-Fail
  negativ gecached).
- map_view_builder: echtes GLB vor Platzhalter; kein half_height-Lift
  (schon Fusspunkt-Koordinaten), Scale ueber Node-Scale + AABB-Metas
  mal Scale; Auswahl-Orange nur als Override im Auswahl-Zustand
  (Texturen bleiben sichtbar). fehlende_glbs-Meldung an die Stats-Zeile.
- interaction (Ghost-Preview): nutzt denselben echten Mesh-Pfad;
  Platzier-Status haengt „(Platzhalter — echte GLB fehlt)“ an.
- tests/test_glb_view.gd (neu, in run_tests.sh): Moosfelsen-Probe
  (echtes GLB > Platzhalter-Vertices), stein_mittel_1 1026>>36,
  Normalisierung y=0/XZ-zentriert, Cache-Identitaet, Viewport-Rebuild
  Fusspunkt == Terrainhoehe (b20dff2), Fallback + fehlende_glbs.
  Suite: 773 checks, alle gruen.
2026-10-17 17:23:56 +00:00
Hermes (godot-vr)
afcbda8dd0 Shortcuts 3/3 (t_11b5c25b): Strg+O/N/Umschalt+S + Redo-Wege dokumentiert, F1-F4 Werkzeugwechsel, H-Kurzhilfe-Overlay, F5 Karte testen, Dirty-Schutz vor Neu/Oeffnen, permanente Werkzeug-Anzeige in der Statuszeile. 2026-10-16 17:58:34 +00:00
Hermes (godot-vr)
08a75ac430 Shortcuts 2/3 (t_55257792): R-Drehen + Pfeil-Raster + PgUp/Down + Strg+Bild↑ + Greifen + Terrain-Pinsel-Tasten.
- R = Auswahl 90° drehen (Szene um Auswahl-Mitte, Bauen-Bauteil um Fußpunkt, Gruppe komplett; Umschalt+R = -90°), Mausrad-Feindrehen 15° bleibt
- Pfeiltasten = 2-m-Raster-Verschiebung, Richtung = Kamera-Blickrichtung auf nächste Weltachse gerastet (MoveMath.nudge_delta, pure)
- Bild↑/↓ = ±0,25 m auf der GANZEN Auswahl (EIN Undo), Strg+Bild↑ = Fußpunkt aufs Terrain/Boden absetzen
- G = Greifen (Blender-Semantik): Auswahl hängt an der Maus, LMB setzt mit Raster-Snap, Esc rollt byte-genau zurück (ShortcutPolicy cancel_grab)
- Terrain-Pinsel: [ / ] = Preset-Zyklus Klein/Mittel/Groß (Slider/Label/Preset-Buttons wandern mit), 1-4 = Heben/Senken/Glätten/Malen (PAINT-Clamp-Fix, Palette mit Malen sichtbar); Ziffern NUR im Terrain-Werkzeug
- Strg halten = temporärer Snap-Zwang beim Bewegen/Greifen/Platzieren
- Tests: test_shortcuts.gd 157 Checks (R byte-genau, Pfeil-Raster, Lift-Stufen, Boden-Snap, Greifen+Esc, Pinsel-Zyklus, History-Eintrag pro Operation), test_gui_smoke.gd erweitert (38 Checks) und in run_tests.sh aufgenommen; F-Regression seit Karte 1 damit abgedeckt
- Suite 9/9 grün (696 Checks), Builder-Export 21/0, Parse-Check 11 Dateien grün
2026-10-16 17:53:10 +00:00
Hermes (godot-vr)
095af5ff3c Shortcuts 1/3 (t_c46d31c2): Freelook-Flug + F-Focus + Esc-Policy + Entf-Mehrfachauswahl + Strg+D (+2 m X) + Strg+LMB-Mehrfachauswahl.
- InputMap-Actions mit physischen KEY_*-Codes (shortcut_keys.gd, idempotent) — DE-Layout/Windows-unabhängig
- Freelook: RMT halten = Umsehen+Flug (WASD/EQ, Shift x3, Alt x0.3, Rad = Grundtempo dauerhaft), Shift+F toggle; Orbit bleibt Default, Flug-Exit ohne Kamera-Sprung (FlyMath pure)
- F zentriert Auswahl (SelectionMath.focus_center); Esc = pure Prioritätskette (ShortcutPolicy)
- Strg+LMB + Baum SELECT_MULTI: Mehrfachauswahl synchron Szene↔Baum; Entf/Strg+D wirken auf die ganze Auswahl (EIN Undo-Schritt)
- docs/tasten.md = deutsche Source of truth (Karten 2/3 ergänzen)
- Tests: test_shortcuts.gd NEU 70 Checks, Suite 571 grün, Builder-Export 21/0, Parse-Check grün
2026-10-16 17:43:17 +00:00
Hermes (godot-vr)
b20dff2b5d Terrain C (t_4092e3a0): Objekt-Platzierung == Ghost-Preview — EIN Platzier-Pfad, Fusspunkt auf Terrainhoehe.
Root-Cause: Ghost-Preview und Platzierung nutzten zwei unterschiedliche
Pfade/Strukturen — der Ghost-Root trug den halben Mesh-Lift selbst
(Root=Y+half), das platzierte Objekt hebt nur das Kind-Mesh; die
Platzierung rastete X/Z und hob Y auf terrain_height_at des UNGERASTETEN
Punkts. Bei Raster-Snap und an Haengen wich die platzierte Position von
der Vorschau ab.

Fix — EIN Pfad (ObjectOps.placement_foot): Raster-Snap auf X/Z, dann
Y = Terrainhoehe am GERASTETEN Punkt (SvmapDocument.terrain_height_at).
Ghost-Root = Fusspunkt, Kind-Mesh um half_height gehoben — identische
Struktur wie map_view_builder. Objekt-Cursor trifft jetzt das GELAENDE
(TerrainField.raycast, gleicher Pfad wie Terrain-/Bauen-/Spawn-Werkzeug)
statt der flachen Basis-Ebene; Hoehen-Cache dekodiert einmal pro
Struktur-Aenderung (refresh_terrain_cache aus _after_structure_change).

Tests (test_library 40->94 Checks): Platzier-Fusspunkt (Rampe 0..20 m:
Fusspunkt == Terrainhoehe, Ghost-Pfad == Platzierungs-Pfad, Raster-Snap-Y
am gerasteten Punkt), Registry-Fusspunkt (alle 54 Objekte: Mesh-Unterseite
== pos, kein Schweben/Eingraben), Ghost-Preview == platziertes Objekt mit
simulierter Kamera (Treffer-Zelle == Cursor-Zelle, <5 cm). Editor-Suiten
501 Checks gruen, Builder-Export 21/0, GUI-Smoke 20 gruen.
2026-10-16 17:36:30 +00:00
Hermes (godot-vr)
e505329a42 Terrain B (t_234490cf): Pinsel-Position == Cursor-Position — Ray→Terrain-Mapping gefixt
Root-Cause 1 (Spiegelung): terrain_mesh.gd positionierte die Mesh-Vertices
gespiegelt (+0.5 − index·cell in X UND Z), während ALLE anderen Verbraucher
der Heightmap der Dokument-Konvention folgen (Index 0 = −X/−Z-Ecke):
SvmapDocument.terrain_height_at, TerrainField.height_at/apply_brush und der
Match-Client (svmap_terrain.gd). Der Renderer war der einzige Ausreißer —
der Pinsel-Strahl traf das gerenderte Mesh punktgespiegelt um die Map-Mitte
(Sly-Befund: Erhöhung erschien an ganz anderer Stelle). Fix: Vertices auf
−size*0.5 + index*cell (identisch zum Client), Normalen-Vorzeichen angepasst
(n ∝ (−∂h/∂x, 1, −∂h/∂z)); Winding bleibt gültig (180°-Rotation erhält die
Orientierung, Kreuzprodukt-Normale weiter nach unten = Frontseite).

Root-Cause 2 (Tangenten-Graze): Ein Strahl, der exakt auf eine Hügelkuppe
zielt, grast die gewölbte Oberfläche — f(t) bleibt minimal knapp über 0 und
steigt danach wieder, obwohl weiter hinten (VERDECKT hinter dem Hügel) noch
ein Vorzeichenwechsel an der Basis-Ebene lag. Der March lief durch und der
Pinsel sprang auf den verdeckten Basis-Punkt meterweit weg. Fix in
TerrainField.raycast: Regel 'erster sichtbarer Oberflächen-Kontakt gewinnt' —
kommt f innerhalb des Bandes (½ March-Schritt) an die Oberfläche und entfernt
sich der Strahl wieder, wird das Graze-Minimum lokal per Ternärsuche
verfeinert und als Treffer zurückgegeben. Echter Treffer (f ≤ 0) gewinnt
weiterhin immer; betrifft auch Bauen-Werkzeug und Spawn-Zonen (gleiche
Raycast-Routine).

Tests (test_terrain.gd, 64 → 86 Checks): Neue Gruppe 'Ray→Terrain-Mapping'
mit simulierter Kamera (SubViewport, schief + steil): exakter Screen-Punkt
(unproject) → project_ray → raycast → asserted Treffer-Zelle == Cursor-Zelle
(< 1 Zelle) auf Mitte/Innenraum/Corner/Kanten; Brush-Stroke am Treffer mit
Höhen-Änderungs-Zentrum == erwarteter Zelle; erhabenes Terrain (Strahl trifft
NEUE Oberfläche, y = Hügelhöhe); Mesh-Vertices komplett gegen die
Dokument-Konvention asserted (Root-Cause-Regression). Hügel-Normalen-Checks
auf die korrigierte Konvention umgestellt (lehnt jetzt −X/−Z).

Suite: 7 Editor-Suiten 463 Checks grün + Export 21/0 + GUI-Smoke 20 grün.
2026-10-16 17:27:19 +00:00
Hermes (godot-vr)
b7b160d3bc Terrain-Darstellung (t_7ac70014): Winding-Order geflippt — Terrain bei h=0 sofort als Rasen-Platte sichtbar.
Root-Cause für Slys Gerätesymptom (dunkle Platte + Gitter, Terrain „wacht erst nach langem Drücken auf“): terrain_mesh.gd baute alle Dreiecke von oben CCW = Backface → komplette Deckfläche wurde von oben ge-cullt; sichtbar war nur der dunkle Sky-Boden (0.1/0.1/0.12) plus Gitter-Overlay. Gehobene Flanken waren nur sichtbar, wenn sie über die unsichtbare Deckfläche ragten (Rückenansicht) — daher „wirkt der Pinsel erst nach langem Drücken“.

- terrain_mesh.gd: Dreiecks-Indices gespiegelt [i, i+1, i+res, i+1, i+res+1, i+res] (CW von oben = Frontseite, Godot-Konvention); Normalen-Gradient korrigiert (x-Index läuft gegen Welt-X: n ∝ ((hx_r-hx_l)·cell_z, 2·cell_x·cell_z, (hz_u-hz_d)·cell_x)) — Hänge waren vorher falsch beleuchtet. MeshInstance lazily erzeugt (update_heights sicher vor _ready).
- map_view_builder.gd: Gitter default AUS über sichtbarem Terrain (grid_node), ohne Terrain-Block als Boden-Referenz sichtbar; set_grid_visible()/grid_is_visible() ohne Rebuild.
- main.gd: Toggle-Knopf „⊞ Gitter“ in der Top-Bar; Nutzer-Wahl überlebt jeden Viewport-Rebuild.
- Erster Pinselkontakt: mit sichtbarem Terrain wirkt der Live-Pfad ab Frame 1 (code-seitig korrekt, war nur unsichtbar).

Tests: test_terrain 64 Checks (NEU: Winding aller Dreiecke = Frontseite von oben, Normale h=0 = (0,1,0), X/Z-Hügel-Normale nicht invertiert, erster RAISE-Druck verschiebt Vertex messbar im Mesh, Gitter-Default/Toggle/Boden-Referenz). Suite 441 Checks + Export-Suite 21 grün; --import ohne Parse-Error.
2026-10-16 17:14:54 +00:00
Hermes (godot-vr)
adb585d30e Vereinigung main+feature/builder-export (t_695d57ee): Bauen-Modus + Bibliothek-Export, EIN Vendor-Verzeichnis addons/modular_builder.
feature/builder-export (b3fcb35) in main (e2954a3) gemerged; Doppel-Vendor
aufgelöst: addons/modular_builder_en gelöscht, Export-Kern nach
addons/modular_builder portiert (Pfad-Referenzen angepasst):
- scopedvr_export.gd (Kern: slugify, normalisiere, trimesh_vertices, glb_bytes)
- scopedvr_export_ui.gd (Name-Dialog + Objekt-Pipeline + Thumbnail)
- editor_actions/UI: Export-Modus 'In ScopedVR-Bibliothek (Objekt-Pipeline)'
- tests/test_builder_export.gd (21 Checks, Pitfall ARRAY_INDEX=NULL)
- gen_thumbs: Trimesh-Objekte mit vorhandenem echten Thumbnail überspringen
  (harmlos kombiniert mit dem GLB-Echt-Mesh-Rendering aus t_7cf80244)
- project.godot: Plugin 'res://addons/modular_builder/plugin.cfg'
2026-10-16 16:49:45 +00:00
Hermes
e2954a38ed M2-Karte t_97ea59c5: Modular Builder fest integriert — Bauen-Modus im App-Skelett. Addon CC0-Code unter addons/modular_builder (Demo + nicht-CC0-Texturen entfernt, ADDON_LIZENZ.txt), Plugin im Godot-Editor aktivierbar; App bekommt eigenen Bauen-Modus (build_tool.gd) auf derselben Runtime-Geometrie (modular_piece_geometry/_detail) — keine Godot-Editor-API im App-UI. 10 Bauteile (Fundament/Boden/Wand/Tür/Fenster/Giebel×2/Dach×2/Treppe), 2-m-Raster (ecke/mitte), Klicken zum Stapeln (Ray-Pick auf gebackenen Flächen), 90°-Drehen, Anheben 0,2 m. UNDO: 1 ActionHistory-Eintrag pro Bauteil (Snapshot Block gebaeude). Speichern: Bauteile als optionale Sektion gebaeude in .svmap (checksum-neutral, Legacy-Maps laden weiter); Baken deterministisch aus Teilen (Editor-Ansicht + Match-Export in Karte 2). Statuszeile clip_text (langer Text sprengte Layout → rechtes Panel weg). Tests: test_bauen.gd 87 Checks (Katalog, Meshes, Snap, Baken/Ray, Undo, Save/Laden, Legacy), Suite 421 Checks grün; GUI-Smoke --bauen-demo (Screenshots + Save/Laden-Flow). 2026-10-15 23:12:06 +00:00
Slyphax
905edc021a M2 Natur-Update t_7cf80244: Bibliothek 54 Objekte in Gruppen bäume/felsen/pflanzen/gras/pilze. library_browser: 39 deutsche Namen, alte Gruppe 'natur' thematisch aufgelöst (tree_pine->bäume, rock_large/boulder_small->felsen, bush_small->pflanzen). mesh_shapes: 'none' = flache Fußpunkt-Scheibe (Deko ohne Kollider bleibt sichtbar/wählbar). gen_thumbs: rendert jetzt ECHTE GLB-Meshes aus dem Mesh-Lager (Sibling-Repo, GLTFDocument ohne Import-System), Fallback weiter Platzhalter — alle 54 Thumbnails neu gerendert. test_library: 15->54 + Gruppen-Checks, Suite 56 Checks grün (alle 6 Editor-Suiten OK). 2026-10-15 23:08:24 +00:00
724 changed files with 50635 additions and 8050 deletions

8
.gitignore vendored
View file

@ -2,5 +2,13 @@
.godot/ .godot/
*.tmp *.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 # System
.DS_Store .DS_Store

5
.gitmodules vendored Normal file
View file

@ -0,0 +1,5 @@
[submodule "core"]
path = core
url = ../scopedvr-map-core.git
branch = main
ignore = untracked

16
ADDON_LIZENZ.txt Normal file
View 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.

115
README.md
View file

@ -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/ Neue-Map-Dialog mit 4 Pflicht-Spawns pro Team, Orbit-Viewport mit Gitter/
Boden/Wetter-Stimmung — siehe Commit 84bc081 und `svmap_io.gd`.)* 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 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). 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. Strg+Z / Strg+Y UND große Buttons „↩ Rückgängig / ↪ Wiederholen" oben.
Spätere Werkzeuge melden sich mit `snapshot(keys)` vor und Spätere Werkzeuge melden sich mit `snapshot(keys)` vor und
`commit(desc, before, after)` nach der Änderung an — Terrain-Strokes und `commit(desc, before, after)` nach der Änderung an — Terrain-Strokes und
Spawn-Zonen landen damit automatisch in derselben History. 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 Bibliothek links (deutsche Namen + Poly-Zahl), Klick startet den
Platzier-Modus — eine transparente Vorschau schwebt am Cursor auf dem Boden 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 Mausrad dreht die Vorschau VOR dem Setzen (15°-Raster). Modus bleibt an
für schnelles Mehrfachsetzen. Poly-Budget wird VOR dem Platzieren geprüft. für schnelles Mehrfachsetzen. Poly-Budget wird VOR dem Platzieren geprüft.
- **Auswahl-Anfassen:** Klick wählt ein Objekt (orange + Emission), Ziehen - **Auswahl-Anfassen:** Klick wählt ein Objekt (orange + Emission), Ziehen
@ -93,16 +94,26 @@ synchron ziehen (IDs sind der Vertrag, Entscheidung 20).
Auf dem Terrain-Werkzeug (t_9aba04f1) aufgebaut, nach dem Chef-Grundsatz: Auf dem Terrain-Werkzeug (t_9aba04f1) aufgebaut, nach dem Chef-Grundsatz:
- **Bibliothek-Browser mit Gruppen (`src/editor/library_browser.gd`):** Die - **Bibliothek-Browser mit Gruppen (`core/editor/library_browser.gd`):**
11 etablierten Objekte aus `library/objects.json`, sortiert in deutsche *(Stand 25.10.2026, Military-Pack-Umstellung)* Die Bibliothek ist das
Klapp-Gruppen (Kisten & Barrieren / Bauwerke / Natur + automatisch komplette Military-Desert-Pack (418 Modelle + 5 Kombis = 423 Objekte aus
„Sonstiges" für neue, noch ungruppierte IDs). Jeder Button zeigt ein `library/objects.json`), sortiert in 19 deutsche Kategorien aus dem
128-px-Thumbnail (`library/thumbs/*.png` — einmalig mit Manifest-Feld `kategorie` (Panzer, Radfahrzeuge, Flugzeuge & Hubschrauber,
`xvfb-run -a godot4 --rendering-driver opengl3 --path . --script Geschütze & Stellungen, Fahrzeugteile, Gebäude & Hangars, Zelte, Türme &
res://tools/gen_thumbs.gd` gerendert und committed; fehlt ein PNG, fällt Masten, Mauern & Wände, Sperren & Zäune, Kisten/Fässer/Container, Technik &
der Button sauber aufs Text-Layout zurück). Deutsche Namen wohnen jetzt in Funk, Ausrüstung, Training, Bäume, Pflanzen/Kakteen/Gras, Felsen & Steine,
`LibraryBrowser` (ohne UI → headless testbar). Boden/Straßen/Dünen, Boden-Decals). Fahrzeuge sind **fertig zusammengebaut**
- **Snap to Ground (`src/editor/object_ops.gd`):** Ein Klick auf (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 „⚡ 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 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. ganze Aktion. Objekte, die schon auf dem Boden stehen, bleiben unangetastet.
@ -127,14 +138,14 @@ Export-Workflow.
Auf der Objekt-Bibliothek (t_3a998ae4) aufgebaut, nach dem Chef-Grundsatz: Auf der Objekt-Bibliothek (t_3a998ae4) aufgebaut, nach dem Chef-Grundsatz:
- **Spawn-Zonen malen (`src/editor/spawn_tool.gd`):** Werkzeug - **Spawn-Zonen malen (`core/editor/spawn_tool.gd`):** Werkzeug
„🚩 Spawn-Zonen malen“ — Team Blau 🔵 / Team Rot 🔴 (TDM-Konvention: „🚩 Spawn-Zonen malen“ — Team Blau 🔵 / Team Rot 🔴 (TDM-Konvention:
IDs A…/B…, mapformat § spawn_zones), Klick setzt eine farbige flache IDs A…/B…, mapformat § spawn_zones), Klick setzt eine farbige flache
Kreis-Zone auf dem Terrain (Vorschau-Kreis folgt der Maus), Radius Kreis-Zone auf dem Terrain (Vorschau-Kreis folgt der Maus), Radius
2–30 m per Regler. Zone anklicken → auswählen + ziehen (Terrain-Höhe 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 = bleibt), Entf löscht, Strg+Z nimmt JEDE Operation zurück (jede Zone =
EIN History-Eintrag, Radius-Slider-Ticks sind merge-gekoppelt). EIN History-Eintrag, Radius-Slider-Ticks sind merge-gekoppelt).
- **Live-Validierung (`src/editor/spawn_ops.gd`):** Block „Spawn-Check“ - **Live-Validierung (`core/editor/spawn_ops.gd`):** Block „Spawn-Check“
rechts bleibt STÄNDIG sichtbar und spiegelt exakt die Loader-Regeln rechts bleibt STÄNDIG sichtbar und spiegelt exakt die Loader-Regeln
(V11: ≥ 4 Spawn-Punkte pro Team · V7: Objekt-IDs in der Bibliothek) — (V11: ≥ 4 Spawn-Punkte pro Team · V7: Objekt-IDs in der Bibliothek) —
z. B. „✅ Spawns Team Blau: 4 · ❌ Spawns Team Rot: 2 (mindestens 4 z. B. „✅ Spawns Team Blau: 4 · ❌ Spawns Team Rot: 2 (mindestens 4
@ -170,9 +181,11 @@ aufgebaut, nach dem Chef-Grundsatz (Klick statt Konsole, deutsche UI,
Undo für alles): Undo für alles):
- **Duplizieren (`⧉ Duplizieren`-Button + Strg+D):** Die Kopie des - **Duplizieren (`⧉ Duplizieren`-Button + Strg+D):** Die Kopie des
ausgewählten Objekts hängt diagonal 1 m versetzt daneben (mit Raster-Snap ausgewählten Objekts hängt +2 m in X versetzt daneben (Rasterversatz,
eingerastet, wenn an) und ist DIREKT angewählt — sie lässt sich gleich 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. 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 Poly-Budget und Objekt-Limit werden VOR dem Kopieren geprüft
(Klartext-Meldung, was zu tun ist). EIN Undo-Schritt. (Klartext-Meldung, was zu tun ist). EIN Undo-Schritt.
- **Anheben/Absenken (`▲ Anheben / ▼ Absenken` + Bild↑/Bild↓):** Objekte - **Anheben/Absenken (`▲ Anheben / ▼ Absenken` + Bild↑/Bild↓):** Objekte
@ -183,10 +196,12 @@ Undo für alles):
ist **KEIN Extra-Feld im Dateiformat nötig** und die Loader-Kompatibilität ist **KEIN Extra-Feld im Dateiformat nötig** und die Loader-Kompatibilität
bleibt unangetastet (bewiesen in `tests/test_ux.gd`: Erhöhtes Objekt bleibt unangetastet (bewiesen in `tests/test_ux.gd`: Erhöhtes Objekt
überlebt gespeichern → laden intakt (Höhe bleibt exakt)). überlebt gespeichern → laden intakt (Höhe bleibt exakt)).
- **Absenken stoppt an der Terrain-Höhe** am Objektstandort — nichts wird - **Absenken / VERSENKEN (Karte t_e166e656):** Absenken geht FREI unter die
in Hügel begraben. Steckt ein Objekt im Boden (z. B. nach dem Terrain-Höhe — Steine dürfen halb im Boden stecken (Sly-Wunsch).
Terrain-Formen), bewegt Absenken es NICHT, und die Statusleiste erklärt, Untergrenze ist die Match-Grenze `bounds.min.y`; die Auswahl-Anzeige und
dass `▲ Anheben` oder `⚡ Alles auf den Boden setzen` hilft. 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 - **Anheben** hat als Obergrenze die Match-Grenze `bounds.max.y` — höher
passt der Server nicht (V9). passt der Server nicht (V9).
- **Ziehen behält die Höhe:** Ein angehobenes Objekt schwebt beim - **Ziehen behält die Höhe:** Ein angehobenes Objekt schwebt beim
@ -200,6 +215,11 @@ Undo für alles):
(Herleitung im Quellcode: gespiegelter Richtungs-Vektor = gespiegelter (Herleitung im Quellcode: gespiegelter Richtungs-Vektor = gespiegelter
Winkel; jede Spiegelung ist dadurch selbstinvers — zweimal spiegeln = Winkel; jede Spiegelung ist dadurch selbstinvers — zweimal spiegeln =
Ausgangszustand.) Die Rotation wird mitgespiegelt, wie bestellt. 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 Bewusst NICHT um die Kartemitte gespiegelt: das würde das Objekt quer
über die Map springen lassen — überraschend für Einsteiger. Klartext über die Map springen lassen — überraschend für Einsteiger. Klartext
in Buttons + Tooltip. in Buttons + Tooltip.
@ -218,13 +238,13 @@ Undo für alles):
Mehrfachauswahl + Gruppieren + Gruppen-Verschieben, auf dem UX-Fundament Mehrfachauswahl + Gruppieren + Gruppen-Verschieben, auf dem UX-Fundament
(t_40d855d0) aufgebaut, nach dem Chef-Grundsatz: (t_40d855d0) aufgebaut, nach dem Chef-Grundsatz:
- **Mehrfachauswahl (`src/editor/interaction.gd`):** Umschalt+Klick fügt - **Mehrfachauswahl (`core/editor/interaction.gd`):** Umschalt+Klick fügt
ein Objekt hinzu/entfernt es, Rahmen-Aufziehen (Rubberband) über leeren ein Objekt hinzu/entfernt es, Rahmen-Aufziehen (Rubberband) über leeren
Boden wählt ALLE Objekte im Rahmen (Umschalt + Rahmen = zusätzliche zur Boden wählt ALLE Objekte im Rahmen (Umschalt + Rahmen = zusätzliche zur
Auswahl). Loser Klick auf leere Fläche hebt die Auswahl auf. ALLE Auswahl). Loser Klick auf leere Fläche hebt die Auswahl auf. ALLE
markierten Objekte färben sich orange, das Auswahl-Panel rechts listet markierten Objekte färben sich orange, das Auswahl-Panel rechts listet
sie namentlich auf (ab 9: „… und N weitere“). Projektions-Mathe liegt sie namentlich auf (ab 9: „… und N weitere“). Projektions-Mathe liegt
in `src/editor/selection_math.gd` (headless getestet). in `core/editor/selection_math.gd` (headless getestet).
- **Gruppieren:** Buttons „🔗 Gruppieren / ⛓ Gruppe lösen“ im Auswahl-Panel - **Gruppieren:** Buttons „🔗 Gruppieren / ⛓ Gruppe lösen“ im Auswahl-Panel
+ Kürzel Strg+G / Strg+Umschalt+G. Eine Gruppe braucht ≥ 2 Objekte. + Kürzel Strg+G / Strg+Umschalt+G. Eine Gruppe braucht ≥ 2 Objekte.
Gruppen-Mitglieder ziehen und drehen GEMEINSAM — ein Griff an ein Gruppen-Mitglieder ziehen und drehen GEMEINSAM — ein Griff an ein
@ -267,7 +287,7 @@ auch ungespeicherte! — sofort als Spiel in einem EIGENEN Fenster. Der Editor
bleibt offen. Im Spiel: WASD laufen, Maus umsehen, Shift sprinten, Leertaste bleibt offen. Im Spiel: WASD laufen, Maus umsehen, Shift sprinten, Leertaste
springen, ESC beendet das Test-Fenster sauber (kein Prozess-Kill nötig). springen, ESC beendet das Test-Fenster sauber (kein Prozess-Kill nötig).
- **Temp-Export (`src/editor/test_run.gd`):** Die Map wird nach - **Temp-Export (`core/editor/test_run.gd`):** Die Map wird nach
`user://testrun/<name>.svmap` exportiert (deterministischer Name aus dem `user://testrun/<name>.svmap` exportiert (deterministischer Name aus dem
Map-Namen, erneuter Testlauf überschreibt) — über dieselbe Map-Namen, erneuter Testlauf überschreibt) — über dieselbe
`SvmapIO.save_to_file`-Validierung wie das echte Speichern. Ist die Map `SvmapIO.save_to_file`-Validierung wie das echte Speichern. Ist die Map
@ -290,3 +310,50 @@ springen, ESC beendet das Test-Fenster sauber (kein Prozess-Kill nötig).
ungültige Map blockiert, Client-Suche inkl. Konfig-Vorrang, Godot-Findung, ungültige Map blockiert, Client-Suche inkl. Konfig-Vorrang, Godot-Findung,
exakte Argumentliste). Parse-Check umfasst `test_run.gd`. 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
View 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.*

View file

@ -4,9 +4,9 @@ Build modular structures directly in the Godot 4.7 editor. The panel appears at
## Install and try the demo ## Install and try the demo
Open this repository as a Godot project to explore `addons/modular_builder_en/demo/Demo.tscn`. After Godot finishes importing the assets, enable **Modular Builder (English)** in **Project > Project Settings > Plugins**. The demo and its five textures are contained in `addons/modular_builder_en/demo/`. 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_en/`, then enable **Modular Builder (English)** in **Project > Project Settings > Plugins**. Open a 3D scene to use the editor panel. The demo remains inside the addon folder and can be removed if you do not need it. 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 ## Build
@ -28,7 +28,7 @@ Save a selected structure as a reusable `.tscn` module, or insert a saved module
## Files and license ## Files and license
The English plugin lives in `addons/modular_builder_en/`. Editor code is split into UI, scene actions, detail placement, vertex editing, and viewport interaction. Geometry generation, mesh baking, roof/pillar placement, and vertex simplification have separate scripts. The 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. 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.

View file

Before

Width:  |  Height:  |  Size: 315 B

After

Width:  |  Height:  |  Size: 315 B

View 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

View file

Before

Width:  |  Height:  |  Size: 195 B

After

Width:  |  Height:  |  Size: 195 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bdfxl3d88c45p" uid="uid://np6bb2n6ohj3"
path="res://.godot/imported/cancel.svg-47b87f3e857516f74642fe118e831480.ctex" path="res://.godot/imported/cancel.svg-48b548656752953c76fdbf1fa691b961.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/cancel.svg" source_file="res://addons/modular_builder/icons/cancel.svg"
dest_files=["res://.godot/imported/cancel.svg-47b87f3e857516f74642fe118e831480.ctex"] dest_files=["res://.godot/imported/cancel.svg-48b548656752953c76fdbf1fa691b961.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 315 B

After

Width:  |  Height:  |  Size: 315 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://blcmsiuegfhy" uid="uid://cjc2d332bqgfy"
path="res://.godot/imported/collision.svg-e4cf0320ea5b34c663eb051e13179fb1.ctex" path="res://.godot/imported/collision.svg-c665b3652a42642be8f703d82faa449e.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/collision.svg" source_file="res://addons/modular_builder/icons/collision.svg"
dest_files=["res://.godot/imported/collision.svg-e4cf0320ea5b34c663eb051e13179fb1.ctex"] dest_files=["res://.godot/imported/collision.svg-c665b3652a42642be8f703d82faa449e.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 259 B

After

Width:  |  Height:  |  Size: 259 B

View 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

View file

Before

Width:  |  Height:  |  Size: 265 B

After

Width:  |  Height:  |  Size: 265 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://shrvgd57wqmy" uid="uid://booy2vg0ci6p7"
path="res://.godot/imported/door.svg-abe4c0258238339b0ce092292a14a613.ctex" path="res://.godot/imported/door.svg-5067a7f9805041b1e0019db73b7f247e.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/door.svg" source_file="res://addons/modular_builder/icons/door.svg"
dest_files=["res://.godot/imported/door.svg-abe4c0258238339b0ce092292a14a613.ctex"] dest_files=["res://.godot/imported/door.svg-5067a7f9805041b1e0019db73b7f247e.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 404 B

After

Width:  |  Height:  |  Size: 404 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bmvaiklprw3ob" uid="uid://62ci7tsy4yeq"
path="res://.godot/imported/fence.svg-4eb777531b4c56b60ad89e9e327e738c.ctex" path="res://.godot/imported/fence.svg-b115d5a88a7e382552216868149e7186.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/fence.svg" source_file="res://addons/modular_builder/icons/fence.svg"
dest_files=["res://.godot/imported/fence.svg-4eb777531b4c56b60ad89e9e327e738c.ctex"] dest_files=["res://.godot/imported/fence.svg-b115d5a88a7e382552216868149e7186.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 298 B

After

Width:  |  Height:  |  Size: 298 B

View 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

View file

Before

Width:  |  Height:  |  Size: 291 B

After

Width:  |  Height:  |  Size: 291 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://b11j8aumv12dr" uid="uid://dghymeocq75is"
path="res://.godot/imported/floor_square.svg-929ec0d7e4700eccd8871b9e7bdc53c8.ctex" path="res://.godot/imported/floor_triangle.svg-cbffdc894d9762c4cb9806dbfa52405c.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/floor_square.svg" source_file="res://addons/modular_builder/icons/floor_triangle.svg"
dest_files=["res://.godot/imported/floor_square.svg-929ec0d7e4700eccd8871b9e7bdc53c8.ctex"] dest_files=["res://.godot/imported/floor_triangle.svg-cbffdc894d9762c4cb9806dbfa52405c.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 286 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://blm8vvelfa0ov" uid="uid://3df01la6lxht"
path="res://.godot/imported/floor_triangle.svg-dccf4703c638fbb44617d82da92f981f.ctex" path="res://.godot/imported/foundation_square.svg-23bf2f453c993134ea4ea05145d98488.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/floor_triangle.svg" source_file="res://addons/modular_builder/icons/foundation_square.svg"
dest_files=["res://.godot/imported/floor_triangle.svg-dccf4703c638fbb44617d82da92f981f.ctex"] dest_files=["res://.godot/imported/foundation_square.svg-23bf2f453c993134ea4ea05145d98488.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 285 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://d4iv2qc3e6k8f" uid="uid://ln0jh5ibvhe0"
path="res://.godot/imported/create_vertex.svg-b23af865d7a800b0e078b26b2e99a9f7.ctex" path="res://.godot/imported/foundation_triangle.svg-4b3e87f2198d6b6a8427029009457134.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/create_vertex.svg" source_file="res://addons/modular_builder/icons/foundation_triangle.svg"
dest_files=["res://.godot/imported/create_vertex.svg-b23af865d7a800b0e078b26b2e99a9f7.ctex"] dest_files=["res://.godot/imported/foundation_triangle.svg-4b3e87f2198d6b6a8427029009457134.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 262 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://420a0ay7eep0" uid="uid://b16auokjgcjuv"
path="res://.godot/imported/gable_wall.svg-c2d60f6ba4255837e31c628621486d99.ctex" path="res://.godot/imported/gable_wall.svg-1d82415321f6b6ad59f51ba7640a6c53.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/gable_wall.svg" source_file="res://addons/modular_builder/icons/gable_wall.svg"
dest_files=["res://.godot/imported/gable_wall.svg-c2d60f6ba4255837e31c628621486d99.ctex"] dest_files=["res://.godot/imported/gable_wall.svg-1d82415321f6b6ad59f51ba7640a6c53.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 336 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://d3smkmuij3tiu" uid="uid://0jckxct26pqs"
path="res://.godot/imported/remove_vertex.svg-a2650c4a5b88e73778470ad394dd8d24.ctex" path="res://.godot/imported/gable_wall_right.svg-3336022dcfa05da5960f0dc8e3ecc6ff.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/remove_vertex.svg" source_file="res://addons/modular_builder/icons/gable_wall_right.svg"
dest_files=["res://.godot/imported/remove_vertex.svg-a2650c4a5b88e73778470ad394dd8d24.ctex"] dest_files=["res://.godot/imported/gable_wall_right.svg-3336022dcfa05da5960f0dc8e3ecc6ff.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bhqb8ky2bus1f" uid="uid://cf2okj2f2eq1h"
path="res://.godot/imported/hatch.svg-0413a819fca720a8bc1e64dc6ad76826.ctex" path="res://.godot/imported/hatch.svg-f0ee2bc7f824245d3082af2deb5cb59a.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/hatch.svg" source_file="res://addons/modular_builder/icons/hatch.svg"
dest_files=["res://.godot/imported/hatch.svg-0413a819fca720a8bc1e64dc6ad76826.ctex"] dest_files=["res://.godot/imported/hatch.svg-f0ee2bc7f824245d3082af2deb5cb59a.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 293 B

After

Width:  |  Height:  |  Size: 293 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://yesw2vj8yaah" uid="uid://dqhgwj72rp5o1"
path="res://.godot/imported/load_module.svg-63e2b3e2ac8c24bc991fcf4e75ace01b.ctex" path="res://.godot/imported/load_module.svg-308877482b25606c5b7ed43292d640f3.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/load_module.svg" source_file="res://addons/modular_builder/icons/load_module.svg"
dest_files=["res://.godot/imported/load_module.svg-63e2b3e2ac8c24bc991fcf4e75ace01b.ctex"] dest_files=["res://.godot/imported/load_module.svg-308877482b25606c5b7ed43292d640f3.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 460 B

After

Width:  |  Height:  |  Size: 460 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://di37e5sjuktph" uid="uid://n7jw3p1fa5eu"
path="res://.godot/imported/save_module.svg-de34a0b11c2e62ba100617a23de20ad2.ctex" path="res://.godot/imported/merge_vertices.svg-d9204aed762a783069511285341aaecc.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/save_module.svg" source_file="res://addons/modular_builder/icons/merge_vertices.svg"
dest_files=["res://.godot/imported/save_module.svg-de34a0b11c2e62ba100617a23de20ad2.ctex"] dest_files=["res://.godot/imported/merge_vertices.svg-d9204aed762a783069511285341aaecc.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 351 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://cfh7b0kxdwl6i" uid="uid://buetyw0nroq6k"
path="res://.godot/imported/move_vertex.svg-ceb416f7c4a6671d49b72b5d7058c829.ctex" path="res://.godot/imported/move_vertex.svg-a43c7dd371bb862b05efa6f900fd0556.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/move_vertex.svg" source_file="res://addons/modular_builder/icons/move_vertex.svg"
dest_files=["res://.godot/imported/move_vertex.svg-ceb416f7c4a6671d49b72b5d7058c829.ctex"] dest_files=["res://.godot/imported/move_vertex.svg-a43c7dd371bb862b05efa6f900fd0556.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 275 B

After

Width:  |  Height:  |  Size: 275 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dl55t56w8agod" uid="uid://bkxxyh8ec5oed"
path="res://.godot/imported/parapet.svg-ed0a5acaa465eefbbe527d0d92ac41e6.ctex" path="res://.godot/imported/parapet.svg-f91880fefed49e8dc8e533dcad6bb584.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/parapet.svg" source_file="res://addons/modular_builder/icons/parapet.svg"
dest_files=["res://.godot/imported/parapet.svg-ed0a5acaa465eefbbe527d0d92ac41e6.ctex"] dest_files=["res://.godot/imported/parapet.svg-f91880fefed49e8dc8e533dcad6bb584.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 257 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bsvap0ukvvtxl" uid="uid://dukm4mprtq7l"
path="res://.godot/imported/pillar.svg-0acb51ea7c090f4767757e19389c7697.ctex" path="res://.godot/imported/pillar.svg-ccd9ad9da9982abc939a9856fa296417.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/pillar.svg" source_file="res://addons/modular_builder/icons/pillar.svg"
dest_files=["res://.godot/imported/pillar.svg-0acb51ea7c090f4767757e19389c7697.ctex"] dest_files=["res://.godot/imported/pillar.svg-ccd9ad9da9982abc939a9856fa296417.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 250 B

After

Width:  |  Height:  |  Size: 250 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dkuvxakhpo11m" uid="uid://bjvg4halxhoy5"
path="res://.godot/imported/add_building.svg-c540099545c836d05aaa84aabb9d42a4.ctex" path="res://.godot/imported/remove_vertex.svg-824eacf485182fddca49a06831d65906.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/add_building.svg" source_file="res://addons/modular_builder/icons/remove_vertex.svg"
dest_files=["res://.godot/imported/add_building.svg-c540099545c836d05aaa84aabb9d42a4.ctex"] dest_files=["res://.godot/imported/remove_vertex.svg-824eacf485182fddca49a06831d65906.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 257 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://7mg7tjvygg82" uid="uid://dvtqw0h5dd5yl"
path="res://.godot/imported/roof_gable.svg-19c7e85e7be7616ba9781f8a0b3ca1c3.ctex" path="res://.godot/imported/roof_gable.svg-8b4da12734ae02486f4e916ee6f2b16d.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/roof_gable.svg" source_file="res://addons/modular_builder/icons/roof_gable.svg"
dest_files=["res://.godot/imported/roof_gable.svg-19c7e85e7be7616ba9781f8a0b3ca1c3.ctex"] dest_files=["res://.godot/imported/roof_gable.svg-8b4da12734ae02486f4e916ee6f2b16d.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 306 B

After

Width:  |  Height:  |  Size: 306 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://55uqhamplshs" uid="uid://ccew6wsdsj0kn"
path="res://.godot/imported/roof_pyramid.svg-9e1f692744fe6843f12da4ad40b1e7fd.ctex" path="res://.godot/imported/roof_pyramid.svg-97d144eff93367622d2cd47f9133b57d.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/roof_pyramid.svg" source_file="res://addons/modular_builder/icons/roof_pyramid.svg"
dest_files=["res://.godot/imported/roof_pyramid.svg-9e1f692744fe6843f12da4ad40b1e7fd.ctex"] dest_files=["res://.godot/imported/roof_pyramid.svg-97d144eff93367622d2cd47f9133b57d.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 236 B

After

Width:  |  Height:  |  Size: 236 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dl0xf605xhwdl" uid="uid://wyg331tl3rdt"
path="res://.godot/imported/roof_shed.svg-6a66b1c82d6d6e005d37c43a5b9009df.ctex" path="res://.godot/imported/roof_shed.svg-47c9ee22596d5277674eb88f312c9478.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/roof_shed.svg" source_file="res://addons/modular_builder/icons/roof_shed.svg"
dest_files=["res://.godot/imported/roof_shed.svg-6a66b1c82d6d6e005d37c43a5b9009df.ctex"] dest_files=["res://.godot/imported/roof_shed.svg-47c9ee22596d5277674eb88f312c9478.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 329 B

After

Width:  |  Height:  |  Size: 329 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dhi55x4bo0dmp" uid="uid://cxcb3kjn18udy"
path="res://.godot/imported/rotate.svg-2f2c1519ec865048747f49f7cbeed3c8.ctex" path="res://.godot/imported/rotate.svg-84e58dd8fc90e4d634e7fbc55f8eda70.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/rotate.svg" source_file="res://addons/modular_builder/icons/rotate.svg"
dest_files=["res://.godot/imported/rotate.svg-2f2c1519ec865048747f49f7cbeed3c8.ctex"] dest_files=["res://.godot/imported/rotate.svg-84e58dd8fc90e4d634e7fbc55f8eda70.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 293 B

After

Width:  |  Height:  |  Size: 293 B

View 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

View file

Before

Width:  |  Height:  |  Size: 355 B

After

Width:  |  Height:  |  Size: 355 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bl85bimtdaffm" uid="uid://b6ibyaujhybao"
path="res://.godot/imported/stairs.svg-8a83477aca2bfaf1ecd92ddcb3d7ad52.ctex" path="res://.godot/imported/stairs.svg-9b9203b050bb5cf817168a5866b66b2a.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/stairs.svg" source_file="res://addons/modular_builder/icons/stairs.svg"
dest_files=["res://.godot/imported/stairs.svg-8a83477aca2bfaf1ecd92ddcb3d7ad52.ctex"] dest_files=["res://.godot/imported/stairs.svg-9b9203b050bb5cf817168a5866b66b2a.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://c61wb4mnf0wls" uid="uid://cmkpq8teurcwt"
path="res://.godot/imported/wall.svg-25deeb903c389412115a2f457cdde263.ctex" path="res://.godot/imported/wall.svg-b57c613ae16e9db4887d98c30036dee9.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/wall.svg" source_file="res://addons/modular_builder/icons/wall.svg"
dest_files=["res://.godot/imported/wall.svg-25deeb903c389412115a2f457cdde263.ctex"] dest_files=["res://.godot/imported/wall.svg-b57c613ae16e9db4887d98c30036dee9.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 222 B

After

Width:  |  Height:  |  Size: 222 B

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://np7qscb7nf3s" uid="uid://cu21qr4i7dns1"
path="res://.godot/imported/window.svg-ac8dc7e4787d27b79bd94a1bd7b1f8f8.ctex" path="res://.godot/imported/window.svg-b79f89a5f56cfe779422a43f44ba067f.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://addons/modular_builder_en/icons/window.svg" source_file="res://addons/modular_builder/icons/window.svg"
dest_files=["res://.godot/imported/window.svg-ac8dc7e4787d27b79bd94a1bd7b1f8f8.ctex"] dest_files=["res://.godot/imported/window.svg-b79f89a5f56cfe779422a43f44ba067f.ctex"]
[params] [params]

View file

@ -1,10 +1,10 @@
@tool @tool
extends "res://addons/modular_builder_en/modular_builder_editor_ui.gd" extends "res://addons/modular_builder/modular_builder_editor_ui.gd"
## ScopedVR-Bibliothek-Export (Karte t_82d2a8dd): dritter Export-Modus ## ScopedVR-Bibliothek-Export (Karte t_82d2a8dd): dritter Export-Modus
## „In ScopedVR-Bibliothek" — gebackenes Mesh + Trimesh-Kollision durch die ## „In ScopedVR-Bibliothek" — gebackenes Mesh + Trimesh-Kollision durch die
## Objekt-Pipeline (tools/objekt im Match-Server-Repo), Name-Dialog inklusive. ## Objekt-Pipeline (tools/objekt im Match-Server-Repo), Name-Dialog inklusive.
const SCOPEDVR_EXPORT_UI := preload("res://addons/modular_builder_en/scopedvr_export_ui.gd") const SCOPEDVR_EXPORT_UI := preload("res://addons/modular_builder/scopedvr_export_ui.gd")
var _scopedvr_export: RefCounted = null var _scopedvr_export: RefCounted = null
func _add_piece() -> void: func _add_piece() -> void:

View file

@ -1,8 +1,8 @@
@tool @tool
extends "res://addons/modular_builder_en/modular_builder_editor_actions.gd" extends "res://addons/modular_builder/modular_builder_editor_actions.gd"
const PILLAR_SUPPORT = preload("res://addons/modular_builder_en/modular_pillar_support.gd") const PILLAR_SUPPORT = preload("res://addons/modular_builder/modular_pillar_support.gd")
const VERTEX_SIMPLIFIER = preload("res://addons/modular_builder_en/modular_vertex_simplifier.gd") const VERTEX_SIMPLIFIER = preload("res://addons/modular_builder/modular_vertex_simplifier.gd")
func _create_detail_from_click(camera: Camera3D, screen_position: Vector2) -> bool: func _create_detail_from_click(camera: Camera3D, screen_position: Vector2) -> bool:
var placement := _find_detail_placement(camera, screen_position) var placement := _find_detail_placement(camera, screen_position)

View file

@ -1,9 +1,9 @@
@tool @tool
extends EditorPlugin extends EditorPlugin
const PIECE_SCRIPT = preload("res://addons/modular_builder_en/modular_piece.gd") const PIECE_SCRIPT = preload("res://addons/modular_builder/modular_piece.gd")
const DETAIL_SCRIPT = preload("res://addons/modular_builder_en/modular_building_detail.gd") const DETAIL_SCRIPT = preload("res://core/vendor/modular_builder/modular_building_detail.gd")
const MESH_BAKER = preload("res://addons/modular_builder_en/modular_mesh_baker.gd") const MESH_BAKER = preload("res://addons/modular_builder/modular_mesh_baker.gd")
const TOOL_NONE := "" const TOOL_NONE := ""
const TOOL_FOUNDATION := "foundation" const TOOL_FOUNDATION := "foundation"
const TOOL_FOUNDATION_SQUARE := "foundation_square" const TOOL_FOUNDATION_SQUARE := "foundation_square"
@ -133,19 +133,19 @@ func _enter_tree() -> void:
size_spin.suffix = " m" size_spin.suffix = " m"
size_spin.custom_minimum_size = Vector2(100, 0) size_spin.custom_minimum_size = Vector2(100, 0)
_creation_row.add_child(size_spin) _creation_row.add_child(size_spin)
var create_button := _make_icon_button("res://addons/modular_builder_en/icons/add_building.svg", "Add ModularBuilderPiece", Color(0.22, 0.30, 0.38)) 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")) create_button.pressed.connect(Callable(self, "_add_piece"))
_creation_row.add_child(create_button) _creation_row.add_child(create_button)
var rotate_button := _make_icon_button("res://addons/modular_builder_en/icons/rotate.svg", "Rotate selected piece 90°", Color(0.22, 0.30, 0.38)) 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")) rotate_button.pressed.connect(Callable(self, "_rotate_selected_piece"))
_creation_row.add_child(rotate_button) _creation_row.add_child(rotate_button)
var collision_button := _make_icon_button("res://addons/modular_builder_en/icons/collision.svg", "Add general collision to selected structure", Color(0.22, 0.30, 0.38)) 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")) collision_button.pressed.connect(Callable(self, "_add_general_collision"))
_creation_row.add_child(collision_button) _creation_row.add_child(collision_button)
var save_module_button := _make_icon_button("res://addons/modular_builder_en/icons/save_module.svg", "Save selected structure as a .tscn module", Color(0.22, 0.30, 0.38)) 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")) save_module_button.pressed.connect(Callable(self, "_show_save_module_dialog"))
_creation_row.add_child(save_module_button) _creation_row.add_child(save_module_button)
var load_module_button := _make_icon_button("res://addons/modular_builder_en/icons/load_module.svg", "Insert saved module into the scene", Color(0.22, 0.30, 0.38)) 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")) load_module_button.pressed.connect(Callable(self, "_show_load_module_dialog"))
_creation_row.add_child(load_module_button) _creation_row.add_child(load_module_button)
_export_row = HFlowContainer.new() _export_row = HFlowContainer.new()
@ -166,33 +166,33 @@ func _enter_tree() -> void:
dock.add_child(_tool_row) dock.add_child(_tool_row)
_tool_button_group = ButtonGroup.new() _tool_button_group = ButtonGroup.new()
_tool_button_group.allow_unpress = true _tool_button_group.allow_unpress = true
_add_tool_button("res://addons/modular_builder_en/icons/foundation_square.svg", "Square foundation", TOOL_FOUNDATION_SQUARE, Color(0.08, 0.48, 0.22)) _add_tool_button("res://addons/modular_builder/icons/foundation_square.svg", "Square foundation", TOOL_FOUNDATION_SQUARE, Color(0.08, 0.48, 0.22))
_add_tool_button("res://addons/modular_builder_en/icons/foundation_triangle.svg", "Triangular foundation", TOOL_FOUNDATION_TRIANGLE, Color(0.08, 0.48, 0.22)) _add_tool_button("res://addons/modular_builder/icons/foundation_triangle.svg", "Triangular foundation", TOOL_FOUNDATION_TRIANGLE, Color(0.08, 0.48, 0.22))
_add_tool_button("res://addons/modular_builder_en/icons/wall.svg", "Wall", TOOL_WALL, Color(0.65, 0.12, 0.12)) _add_tool_button("res://addons/modular_builder/icons/wall.svg", "Wall", TOOL_WALL, Color(0.65, 0.12, 0.12))
_add_tool_button("res://addons/modular_builder_en/icons/gable_wall.svg", "Gable wall", TOOL_GABLE_WALL, Color(0.65, 0.12, 0.12)) _add_tool_button("res://addons/modular_builder/icons/gable_wall.svg", "Gable wall", TOOL_GABLE_WALL, Color(0.65, 0.12, 0.12))
_add_tool_button("res://addons/modular_builder_en/icons/gable_wall_right.svg", "Right triangular gable wall", TOOL_GABLE_WALL_RIGHT, Color(0.65, 0.12, 0.12)) _add_tool_button("res://addons/modular_builder/icons/gable_wall_right.svg", "Right triangular gable wall", TOOL_GABLE_WALL_RIGHT, Color(0.65, 0.12, 0.12))
_add_tool_button("res://addons/modular_builder_en/icons/floor_square.svg", "Square floor", TOOL_FLOOR_SQUARE, Color(0.68, 0.48, 0.06)) _add_tool_button("res://addons/modular_builder/icons/floor_square.svg", "Square floor", TOOL_FLOOR_SQUARE, Color(0.68, 0.48, 0.06))
_add_tool_button("res://addons/modular_builder_en/icons/floor_triangle.svg", "Triangular floor", TOOL_FLOOR_TRIANGLE, Color(0.68, 0.48, 0.06)) _add_tool_button("res://addons/modular_builder/icons/floor_triangle.svg", "Triangular floor", TOOL_FLOOR_TRIANGLE, Color(0.68, 0.48, 0.06))
_add_tool_button("res://addons/modular_builder_en/icons/roof_gable.svg", "Gable roof", TOOL_ROOF_GABLE, Color(0.10, 0.38, 0.62)) _add_tool_button("res://addons/modular_builder/icons/roof_gable.svg", "Gable roof", TOOL_ROOF_GABLE, Color(0.10, 0.38, 0.62))
_add_tool_button("res://addons/modular_builder_en/icons/roof_shed.svg", "Shed roof", TOOL_ROOF_SHED, Color(0.10, 0.38, 0.62)) _add_tool_button("res://addons/modular_builder/icons/roof_shed.svg", "Shed roof", TOOL_ROOF_SHED, Color(0.10, 0.38, 0.62))
_add_tool_button("res://addons/modular_builder_en/icons/roof_pyramid.svg", "Pyramid roof", TOOL_ROOF_PYRAMID, Color(0.10, 0.38, 0.62)) _add_tool_button("res://addons/modular_builder/icons/roof_pyramid.svg", "Pyramid roof", TOOL_ROOF_PYRAMID, Color(0.10, 0.38, 0.62))
_add_tool_button("res://addons/modular_builder_en/icons/stairs.svg", "Stairs", TOOL_STAIRS, Color(0.10, 0.38, 0.62)) _add_tool_button("res://addons/modular_builder/icons/stairs.svg", "Stairs", TOOL_STAIRS, Color(0.10, 0.38, 0.62))
_add_tool_button("res://addons/modular_builder_en/icons/fence.svg", "Decorative fence", TOOL_FENCE, Color(0.88, 0.22, 0.55)) _add_tool_button("res://addons/modular_builder/icons/fence.svg", "Decorative fence", TOOL_FENCE, Color(0.88, 0.22, 0.55))
_add_tool_button("res://addons/modular_builder_en/icons/parapet.svg", "Parapet: single rail and posts", TOOL_PARAPET, Color(0.88, 0.22, 0.55)) _add_tool_button("res://addons/modular_builder/icons/parapet.svg", "Parapet: single rail and posts", TOOL_PARAPET, Color(0.88, 0.22, 0.55))
_add_tool_button("res://addons/modular_builder_en/icons/window.svg", "Detail: window", TOOL_WINDOW, Color(0.88, 0.22, 0.55)) _add_tool_button("res://addons/modular_builder/icons/window.svg", "Detail: window", TOOL_WINDOW, Color(0.88, 0.22, 0.55))
_add_tool_button("res://addons/modular_builder_en/icons/door.svg", "Detail: door", TOOL_DOOR, Color(0.88, 0.22, 0.55)) _add_tool_button("res://addons/modular_builder/icons/door.svg", "Detail: door", TOOL_DOOR, Color(0.88, 0.22, 0.55))
_add_tool_button("res://addons/modular_builder_en/icons/hatch.svg", "Detail: hatch", TOOL_HATCH, Color(0.88, 0.22, 0.55)) _add_tool_button("res://addons/modular_builder/icons/hatch.svg", "Detail: hatch", TOOL_HATCH, Color(0.88, 0.22, 0.55))
_add_tool_button("res://addons/modular_builder_en/icons/pillar.svg", "Plain pillar: two vertices or Alt+click for a vertical pillar", TOOL_PILLAR, Color(0.88, 0.22, 0.55)) _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_en/icons/cancel.svg", "Deactivate tool", Color(0.34, 0.20, 0.20)) 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)) cancel_button.pressed.connect(Callable(self, "_set_tool").bind(TOOL_NONE))
_tool_row.add_child(cancel_button) _tool_row.add_child(cancel_button)
_vertex_edit_row = HFlowContainer.new() _vertex_edit_row = HFlowContainer.new()
dock.add_child(_vertex_edit_row) dock.add_child(_vertex_edit_row)
_vertex_edit_row.add_child(_label("Vertices:")) _vertex_edit_row.add_child(_label("Vertices:"))
_add_tool_button("res://addons/modular_builder_en/icons/merge_vertices.svg", "Merge vertices to reduce markers", TOOL_MERGE_VERTICES, Color(0.38, 0.32, 0.52), _vertex_edit_row) _add_tool_button("res://addons/modular_builder/icons/merge_vertices.svg", "Merge vertices to reduce markers", TOOL_MERGE_VERTICES, Color(0.38, 0.32, 0.52), _vertex_edit_row)
_add_tool_button("res://addons/modular_builder_en/icons/create_vertex.svg", "Create a connection vertex on the clicked surface", TOOL_CREATE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row) _add_tool_button("res://addons/modular_builder/icons/create_vertex.svg", "Create a connection vertex on the clicked surface", TOOL_CREATE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
_add_tool_button("res://addons/modular_builder_en/icons/move_vertex.svg", "Move a manual connection vertex", TOOL_MOVE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row) _add_tool_button("res://addons/modular_builder/icons/move_vertex.svg", "Move a manual connection vertex", TOOL_MOVE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
_add_tool_button("res://addons/modular_builder_en/icons/remove_vertex.svg", "Remove a manual connection vertex", TOOL_REMOVE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row) _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() var marker_options_row := HFlowContainer.new()
dock.add_child(marker_options_row) dock.add_child(marker_options_row)
_marker_scope_check = CheckBox.new() _marker_scope_check = CheckBox.new()

View file

@ -1,7 +1,7 @@
@tool @tool
extends "res://addons/modular_builder_en/modular_builder_editor_details.gd" extends "res://addons/modular_builder/modular_builder_editor_details.gd"
const MESH_PICKER = preload("res://addons/modular_builder_en/modular_mesh_picker.gd") const MESH_PICKER = preload("res://addons/modular_builder/modular_mesh_picker.gd")
func _sync_vertex_markers() -> void: func _sync_vertex_markers() -> void:
var root := _get_selected_piece() var root := _get_selected_piece()

View file

@ -1,7 +1,7 @@
@tool @tool
extends "res://addons/modular_builder_en/modular_builder_editor_vertices.gd" extends "res://addons/modular_builder/modular_builder_editor_vertices.gd"
const ROOF_SUPPORT = preload("res://addons/modular_builder_en/modular_roof_support.gd") const ROOF_SUPPORT = preload("res://addons/modular_builder/modular_roof_support.gd")
func _forward_3d_gui_input(camera: Camera3D, event: InputEvent) -> int: 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: if active_tool != TOOL_NONE and event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:

View file

@ -1,6 +1,6 @@
@tool @tool
class_name ModularBuilderPiece class_name ModularBuilderPiece
extends "res://addons/modular_builder_en/modular_piece_geometry.gd" extends "res://core/vendor/modular_builder/modular_piece_geometry.gd"
func _ready() -> void: func _ready() -> void:
_rebuild() _rebuild()

View file

@ -8,7 +8,7 @@ extends RefCounted
## Skript braucht die Editor-Umgebung (Dialoge, OS.execute, Rendering). ## Skript braucht die Editor-Umgebung (Dialoge, OS.execute, Rendering).
## Deutsch für Sly: alle Status-/Fehlermeldungen auf Deutsch. ## Deutsch für Sly: alle Status-/Fehlermeldungen auf Deutsch.
const KERN := preload("res://addons/modular_builder_en/scopedvr_export.gd") const KERN := preload("res://addons/modular_builder/scopedvr_export.gd")
const BUDGET := 20000 # Export-Strukturen (match-server BUILDER_BUDGET) const BUDGET := 20000 # Export-Strukturen (match-server BUILDER_BUDGET)
const THUMB_SIZE := 128 const THUMB_SIZE := 128
@ -133,8 +133,8 @@ func _export(oid: String, name_roh: String) -> void:
if not editor_dir.is_empty(): if not editor_dir.is_empty():
var img := await _thumbnail_bilden(mesh) var img := await _thumbnail_bilden(mesh)
if img != null: if img != null:
DirAccess.make_dir_recursive_absolute(editor_dir + "/library/thumbs") DirAccess.make_dir_recursive_absolute(editor_dir + "/core/thumbs")
img.save_png(editor_dir + "/library/thumbs/" + oid + ".png") img.save_png(editor_dir + "/core/thumbs/" + oid + ".png")
# 6) Editor-Sync (Registry-Kopie; vorhandene Thumbnails bleiben stehen). # 6) Editor-Sync (Registry-Kopie; vorhandene Thumbnails bleiben stehen).
var schritt4 := _lauf(objekt_tool, ["editor", oid]) var schritt4 := _lauf(objekt_tool, ["editor", oid])

View file

@ -1,43 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bdg64bvathtkp"
path="res://.godot/imported/foundation_square.svg-dc8be81ca55b382cac6eed9723c4ea7c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/modular_builder_en/icons/foundation_square.svg"
dest_files=["res://.godot/imported/foundation_square.svg-dc8be81ca55b382cac6eed9723c4ea7c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1,43 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c7sts0wrdier2"
path="res://.godot/imported/foundation_triangle.svg-c2747d1fa5be8206a98f02f2c53db66f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/modular_builder_en/icons/foundation_triangle.svg"
dest_files=["res://.godot/imported/foundation_triangle.svg-c2747d1fa5be8206a98f02f2c53db66f.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

View file

@ -1,43 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cbap2sd7o8fwg"
path="res://.godot/imported/gable_wall_right.svg-ad644706ef5497d44f89d17f68ec357c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/modular_builder_en/icons/gable_wall_right.svg"
dest_files=["res://.godot/imported/gable_wall_right.svg-ad644706ef5497d44f89d17f68ec357c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -1,43 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://1amy53ump51l"
path="res://.godot/imported/merge_vertices.svg-5ece4f02ab55d6738ab8836b942201e1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/modular_builder_en/icons/merge_vertices.svg"
dest_files=["res://.godot/imported/merge_vertices.svg-5ece4f02ab55d6738ab8836b942201e1.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

View file

@ -1,284 +0,0 @@
@tool
class_name ModularBuilderDetail
extends Node3D
enum DetailType { WINDOW, DOOR, HATCH, PILLAR, FENCE, PARAPET }
enum DetailMode { OPENING, SEPARATE_OBJECT }
enum WindowStyle { SINGLE_PANE, CROSS_PANE }
enum DoorStyle { FLAT, PANELED }
@export var detail_type: DetailType = DetailType.WINDOW:
set(value):
detail_type = value
notify_property_list_changed()
_rebuild()
@export var detail_mode: DetailMode = DetailMode.OPENING:
set(value):
detail_mode = value
notify_property_list_changed()
_rebuild()
@export var center_align: bool = false:
set(value):
center_align = value
_apply_pillar_alignment()
notify_property_list_changed()
@export_storage var pillar_direction: Vector3 = Vector3.UP
@export_storage var pillar_alignment_basis: Basis = Basis.IDENTITY
@export var window_style: WindowStyle = WindowStyle.SINGLE_PANE:
set(value):
window_style = value
_rebuild()
@export var door_style: DoorStyle = DoorStyle.FLAT:
set(value):
door_style = value
_rebuild()
@export_range(0.01, 5.0, 0.001, "suffix:m") var width: float = 1.0:
set(value):
width = value
_rebuild()
@export_range(0.2, 5.0, 0.001, "suffix:m") var height: float = 1.0:
set(value):
height = value
_rebuild()
@export_range(0.02, 0.5, 0.001, "suffix:m") var frame_depth: float = 0.08:
set(value):
frame_depth = value
_rebuild()
@export_range(0.1, 8.0, 0.001, "suffix:m") var fence_height: float = 0.7:
set(value):
fence_height = value
_rebuild()
@export_range(0, 100, 1, "suffix: posts") var fence_divisions: int = 1:
set(value):
fence_divisions = value
_rebuild()
@export_storage var path_points: PackedVector3Array = PackedVector3Array()
@export_range(0.01, 0.5, 0.001, "suffix:m") var frame_width: float = 0.08:
set(value):
frame_width = value
_rebuild()
@export var follow_host_thickness: bool = true:
set(value):
follow_host_thickness = value
_rebuild()
@export var surface_material: Material:
set(value):
surface_material = value
_rebuild()
@export var frame_material: Material:
set(value):
frame_material = value
_rebuild()
@export var panel_material: Material:
set(value):
panel_material = value
_rebuild()
func _validate_property(property: Dictionary) -> void:
var separate_object := detail_mode == DetailMode.SEPARATE_OBJECT
var is_window := detail_type == DetailType.WINDOW
var is_door := detail_type == DetailType.DOOR
var has_frame_and_panel := separate_object and detail_type != DetailType.PILLAR and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
var show_property := true
match StringName(property.name):
&"detail_mode":
show_property = detail_type != DetailType.PILLAR and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
&"center_align":
show_property = detail_type == DetailType.PILLAR
&"fence_height", &"fence_divisions":
show_property = detail_type == DetailType.FENCE or detail_type == DetailType.PARAPET
&"path_points":
show_property = false
&"width":
show_property = detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
if detail_type == DetailType.PILLAR:
property.hint = PROPERTY_HINT_RANGE
property.hint_string = "0.01,5.0,0.001,suffix:m"
&"height":
show_property = detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
&"window_style":
show_property = is_window and separate_object
&"door_style":
show_property = is_door and separate_object
&"frame_width", &"frame_material", &"panel_material":
show_property = has_frame_and_panel
&"follow_host_thickness":
show_property = has_frame_and_panel
&"frame_depth":
show_property = has_frame_and_panel and not follow_host_thickness
&"surface_material":
show_property = separate_object
if not show_property:
property.usage = PROPERTY_USAGE_NO_EDITOR
func _ready() -> void:
set_notify_transform(true)
_rebuild()
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSFORM_CHANGED and is_inside_tree():
var host := get_parent()
if host != null and host.has_method("_rebuild"):
host.call("_rebuild")
func _on_host_thickness_changed() -> void:
_rebuild(false)
func _apply_pillar_alignment() -> void:
if detail_type != DetailType.PILLAR:
return
var direction := pillar_direction.normalized()
if direction.is_zero_approx():
direction = Vector3.UP
if center_align:
var object_axes := pillar_alignment_basis.orthonormalized()
var best_axis := direction
var best_dot := -1.0
for axis in [object_axes.x, object_axes.y, object_axes.z]:
var normalized_axis: Vector3 = axis.normalized()
var alignment := absf(direction.dot(normalized_axis))
if alignment > best_dot:
best_dot = alignment
best_axis = normalized_axis if direction.dot(normalized_axis) >= 0.0 else -normalized_axis
direction = best_axis
var reference_axis := Vector3.FORWARD if absf(direction.dot(Vector3.UP)) > 0.99 else Vector3.UP
var x_axis := reference_axis.cross(direction).normalized()
var z_axis := x_axis.cross(direction).normalized()
transform.basis = Basis(x_axis, direction, z_axis).orthonormalized()
if center_align and pillar_direction.length() > 0.0:
height = pillar_direction.length()
func _rebuild(notify_host: bool = true) -> void:
if not is_inside_tree():
return
for child in get_children():
if child.name == "DetailGeometry":
remove_child(child)
child.queue_free()
var host := get_parent()
if notify_host and host != null and host.has_method("_rebuild"):
host.call("_rebuild")
if detail_mode == DetailMode.OPENING and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET:
return
var holder := Node3D.new()
holder.name = "DetailGeometry"
add_child(holder)
var scene_root: Node = null
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
scene_root = get_tree().edited_scene_root
if scene_root:
holder.owner = scene_root
var effective_depth := _effective_frame_depth()
var frame := minf(maxf(frame_width, 0.01), minf(width, height) * 0.45)
if detail_type == DetailType.FENCE or detail_type == DetailType.PARAPET:
_build_fence_geometry(holder)
return
if detail_type == DetailType.PILLAR:
_add_box(holder, "PillarBody", Vector3(width, height, width), Vector3.ZERO, surface_material)
return
if detail_type == DetailType.HATCH:
_add_box(holder, "LeftFrame", Vector3(frame, effective_depth, height), Vector3(-width * 0.5 + frame * 0.5, 0.0, 0.0), _frame_material())
_add_box(holder, "RightFrame", Vector3(frame, effective_depth, height), Vector3(width * 0.5 - frame * 0.5, 0.0, 0.0), _frame_material())
_add_box(holder, "NearFrame", Vector3(width, effective_depth, frame), Vector3(0.0, 0.0, -height * 0.5 + frame * 0.5), _frame_material())
_add_box(holder, "FarFrame", Vector3(width, effective_depth, frame), Vector3(0.0, 0.0, height * 0.5 - frame * 0.5), _frame_material())
_add_box(holder, "HatchPanel", Vector3(width - frame * 2.0, effective_depth * 0.35, height - frame * 2.0), Vector3(0.0, effective_depth * 0.4, 0.0), _panel_material())
return
_add_box(holder, "LeftFrame", Vector3(frame, height, effective_depth), Vector3(-width * 0.5 + frame * 0.5, 0.0, 0.0), _frame_material())
_add_box(holder, "RightFrame", Vector3(frame, height, effective_depth), Vector3(width * 0.5 - frame * 0.5, 0.0, 0.0), _frame_material())
_add_box(holder, "TopFrame", Vector3(width, frame, effective_depth), Vector3(0.0, height * 0.5 - frame * 0.5, 0.0), _frame_material())
if detail_type != DetailType.DOOR:
_add_box(holder, "BottomFrame", Vector3(width, frame, effective_depth), Vector3(0.0, -height * 0.5 + frame * 0.5, 0.0), _frame_material())
if detail_type == DetailType.WINDOW:
_add_box(holder, "WindowPanel", Vector3(maxf(width - frame * 2.0, 0.001), maxf(height - frame * 2.0, 0.001), effective_depth * 0.35), Vector3.ZERO, _panel_material())
if window_style == WindowStyle.CROSS_PANE:
_add_box(holder, "VerticalMullion", Vector3(frame * 0.55, height - frame * 2.0, effective_depth * 0.4), Vector3.ZERO, _frame_material())
_add_box(holder, "HorizontalMullion", Vector3(width - frame * 2.0, frame * 0.55, effective_depth * 0.4), Vector3.ZERO, _frame_material())
elif detail_type == DetailType.DOOR:
_add_box(holder, "DoorPanel", Vector3(width - frame * 2.0, height - frame, effective_depth * 0.45), Vector3(0.0, -frame * 0.5, 0.0), _panel_material())
if door_style == DoorStyle.PANELED:
var inset_depth := maxf(effective_depth * 0.04, 0.005)
var panel_width := (width - frame * 2.0) * 0.82
var panel_height := maxf((height - frame) * 0.28, 0.05)
for panel_index in range(2):
var panel_y := -frame * 0.5 + (panel_index - 0.5) * (panel_height + frame * 0.25)
_add_box(holder, "DoorInset_%d" % panel_index, Vector3(panel_width, panel_height, inset_depth), Vector3(0.0, panel_y, effective_depth * 0.24), _frame_material())
func _effective_frame_depth() -> float:
if follow_host_thickness:
var host := get_parent()
if host != null:
var host_thickness = host.get("thickness")
if host_thickness is float or host_thickness is int:
var depth := float(host_thickness)
if detail_type == DetailType.WINDOW or detail_type == DetailType.DOOR:
depth *= 1.25
return maxf(depth, 0.02)
return frame_depth
func _build_fence_geometry(holder: Node3D) -> void:
if path_points.size() < 2 or fence_height <= 0.0:
return
var start := path_points[0]
var finish := path_points[1]
var path := finish - start
var path_length := path.length()
if path_length < 0.05:
return
var path_axis := path / path_length
var up_axis := Vector3.UP - path_axis * path_axis.dot(Vector3.UP)
if up_axis.length_squared() < 0.001:
up_axis = Vector3.FORWARD - path_axis * path_axis.dot(Vector3.FORWARD)
up_axis = up_axis.normalized()
var side_axis := path_axis.cross(up_axis).normalized()
var post_width := 0.16 if detail_type == DetailType.PARAPET else 0.08
var post_count := fence_divisions + 2
var posts: Array[Vector3] = []
for i in range(post_count):
var base := start.lerp(finish, float(i) / float(post_count - 1))
posts.append(base)
_add_oriented_box(holder, "Post_%d" % i, base + up_axis * fence_height * 0.5, path_axis, up_axis, side_axis, Vector3(post_width, fence_height, post_width))
for i in range(posts.size() - 1):
var a := posts[i]
var b := posts[i + 1]
var segment := b - a
var segment_length := segment.length()
if detail_type == DetailType.PARAPET:
_add_oriented_box(holder, "Rail_%d" % i, (a + b) * 0.5 + up_axis * fence_height * 0.78, segment.normalized(), up_axis, side_axis, Vector3(segment_length, post_width * 0.8, post_width * 0.8))
else:
for height_ratio in [0.28, 0.78]:
_add_oriented_box(holder, "Rail_%d_%d" % [i, int(height_ratio * 100.0)], (a + b) * 0.5 + up_axis * fence_height * height_ratio, segment.normalized(), up_axis, side_axis, Vector3(segment_length, post_width, post_width))
func _add_oriented_box(holder: Node3D, node_name: String, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3, dimensions: Vector3) -> void:
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = node_name
var box := BoxMesh.new()
box.size = dimensions
mesh_instance.mesh = box
var basis := Basis(axis_x.normalized(), axis_y.normalized(), axis_z.normalized()).orthonormalized()
mesh_instance.transform = Transform3D(basis, center)
mesh_instance.material_override = surface_material
holder.add_child(mesh_instance)
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
var scene_root := get_tree().edited_scene_root
if scene_root:
mesh_instance.owner = scene_root
func _frame_material() -> Material:
return frame_material if frame_material != null else surface_material
func _panel_material() -> Material:
return panel_material if panel_material != null else surface_material
func _add_box(holder: Node3D, node_name: String, box_size: Vector3, box_position: Vector3, material: Material = null) -> void:
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = node_name
var box := BoxMesh.new()
box.size = box_size
mesh_instance.mesh = box
mesh_instance.position = box_position
mesh_instance.material_override = material if material != null else surface_material
holder.add_child(mesh_instance)
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
var scene_root := get_tree().edited_scene_root
if scene_root:
mesh_instance.owner = scene_root

View file

@ -1 +0,0 @@
uid://dsttda167kuhi

View file

@ -1,748 +0,0 @@
@tool
class_name ModularBuilderPieceGeometry
extends Node3D
const DETAIL_SCRIPT_PATH := "res://addons/modular_builder_en/modular_building_detail.gd"
const DETAIL_TYPE_HATCH := 2
const DETAIL_TYPE_PILLAR := 3
enum PieceType { FOUNDATION_SQUARE, FOUNDATION_TRIANGLE, FLOOR, WALL, BUILDING, GABLE_WALL, ROOF_GABLE, ROOF_SHED, GABLE_WALL_RIGHT, ROOF_PYRAMID, STAIRS, FENCE }
@export var piece_type: PieceType = PieceType.FOUNDATION_SQUARE:
set(value):
piece_type = value
notify_property_list_changed()
_rebuild()
@export_range(0.5, 8.0, 0.001, "suffix:m") var unit_size: float = 2.0:
set(value):
unit_size = value
_rebuild()
@export_range(0.01, 1.0, 0.001, "suffix:m") var thickness: float = 0.2:
set(value):
thickness = value
_rebuild()
for child in get_children():
if child.has_method("_on_host_thickness_changed"):
child.call("_on_host_thickness_changed")
@export_range(0.1, 8.0, 0.001, "suffix:m") var rise: float = 1.0:
set(value):
rise = value
_rebuild()
@export_range(0.01, 32.0, 0.001, "suffix:m") var roof_width: float = 2.0:
set(value):
roof_width = value
_rebuild()
@export_range(1, 100, 1, "suffix: steps") var stair_count: int = 10:
set(value):
stair_count = value
_rebuild()
@export_range(0.01, 8.0, 0.001, "suffix:m") var stair_width: float = 1.0:
set(value):
stair_width = value
_rebuild()
@export_range(0, 100, 1, "suffix: posts") var fence_divisions: int = 1:
set(value):
fence_divisions = value
_rebuild()
@export_range(0.1, 8.0, 0.001, "suffix:m") var fence_height: float = 1.0:
set(value):
fence_height = value
_rebuild()
@export var symmetric_dimensions: bool = true:
set(value):
if symmetric_dimensions == value:
return
if not value:
_capture_mesh_dimensions()
symmetric_dimensions = value
notify_property_list_changed()
if value:
scale = Vector3.ONE
else:
_apply_dimension_scale()
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_width: float = 2.0:
set(value):
dimension_width = value
_apply_dimension_scale()
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_height: float = 2.0:
set(value):
dimension_height = value
_apply_dimension_scale()
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_depth: float = 2.0:
set(value):
dimension_depth = value
_apply_dimension_scale()
@export_storage var _dimension_reference: Vector3 = Vector3.ZERO
@export var center_align: bool = false:
set(value):
center_align = value
_rebuild()
@export var surface_material: Material:
set(value):
surface_material = value
_rebuild()
@export var custom_points: PackedVector3Array = PackedVector3Array():
set(value):
custom_points = value
_rebuild()
@export var centered_origin: bool = false:
set(value):
centered_origin = value
_rebuild()
func _rebuild() -> void:
pass
func _capture_mesh_dimensions() -> void:
var geometry := get_node_or_null("Geometry") as MeshInstance3D
if geometry == null or geometry.mesh == null:
return
var dimensions := geometry.mesh.get_aabb().size
if dimensions.x > 0.001:
dimension_width = dimensions.x
if dimensions.y > 0.001:
dimension_height = dimensions.y
if dimensions.z > 0.001:
dimension_depth = dimensions.z
_dimension_reference = dimensions
func _apply_dimension_scale() -> void:
if symmetric_dimensions or _dimension_reference.x <= 0.001 or _dimension_reference.y <= 0.001 or _dimension_reference.z <= 0.001:
return
var target := Vector3(dimension_width, dimension_height, dimension_depth)
if piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE or piece_type == PieceType.FLOOR:
target.y = _dimension_reference.y
if piece_type == PieceType.WALL:
target.z = _dimension_reference.z
scale = Vector3(target.x / _dimension_reference.x, target.y / _dimension_reference.y, target.z / _dimension_reference.z)
func _make_mesh() -> ArrayMesh:
if piece_type == PieceType.GABLE_WALL and custom_points.size() >= 2:
return _make_gable_wall_mesh()
if piece_type == PieceType.GABLE_WALL_RIGHT and custom_points.size() >= 2:
return _make_right_gable_wall_mesh()
if (piece_type == PieceType.ROOF_GABLE or piece_type == PieceType.ROOF_SHED or piece_type == PieceType.ROOF_PYRAMID) and custom_points.size() >= 2:
return _make_roof_mesh()
if piece_type == PieceType.STAIRS and custom_points.size() >= 2:
return _make_stair_mesh()
if piece_type == PieceType.FENCE and custom_points.size() >= 2:
return _make_fence_mesh()
if custom_points.size() >= 2 and piece_type == PieceType.WALL:
return _make_custom_wall_mesh()
if custom_points.size() >= 3 and piece_type == PieceType.FLOOR:
return _make_custom_floor_mesh()
if custom_points.size() >= 3 and (piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE):
return _make_custom_foundation_mesh()
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var s := unit_size
var polygon: Array[Vector2]
match piece_type:
PieceType.FOUNDATION_TRIANGLE:
polygon = [Vector2(-s * 0.5, -s * 0.5), Vector2(s * 0.5, -s * 0.5), Vector2(-s * 0.5, s * 0.5)]
_:
polygon = [Vector2(-s * 0.5, -s * 0.5), Vector2(s * 0.5, -s * 0.5), Vector2(s * 0.5, s * 0.5), Vector2(-s * 0.5, s * 0.5)]
var y0 := -thickness * 0.5 if centered_origin else 0.0
var y1 := thickness * 0.5 if centered_origin else thickness
if piece_type == PieceType.WALL:
polygon = [Vector2(-s * 0.5, -thickness * 0.5), Vector2(s * 0.5, -thickness * 0.5), Vector2(s * 0.5, thickness * 0.5), Vector2(-s * 0.5, thickness * 0.5)]
y0 = -s * 0.5 if centered_origin else 0.0
y1 = s * 0.5 if centered_origin else s
if piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE:
var hatch_mesh := _make_hatch_surface_mesh(polygon, y1, y0)
if hatch_mesh != null:
return hatch_mesh
for i in range(1, polygon.size() - 1):
_add_tri(vertices, normals, Vector3(polygon[0].x, y1, polygon[0].y), Vector3(polygon[i + 1].x, y1, polygon[i + 1].y), Vector3(polygon[i].x, y1, polygon[i].y))
_add_tri(vertices, normals, Vector3(polygon[0].x, y0, polygon[0].y), Vector3(polygon[i].x, y0, polygon[i].y), Vector3(polygon[i + 1].x, y0, polygon[i + 1].y))
for i in range(polygon.size()):
var j := (i + 1) % polygon.size()
var a := polygon[i]
var b := polygon[j]
_add_tri(vertices, normals, Vector3(a.x, y0, a.y), Vector3(b.x, y0, b.y), Vector3(b.x, y1, b.y))
_add_tri(vertices, normals, Vector3(a.x, y0, a.y), Vector3(b.x, y1, b.y), Vector3(a.x, y1, a.y))
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
var result := ArrayMesh.new()
result.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return result
func _make_gable_wall_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var base_points := _get_centered_gable_base() if center_align else PackedVector3Array([custom_points[0], custom_points[1]])
var a: Vector3 = base_points[0]
var b: Vector3 = base_points[1]
var direction := (b - a).normalized()
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
var peak := (a + b) * 0.5 + Vector3.UP * rise
var front_a := a + side
var front_b := b + side
var front_peak := peak + side
var back_a := a - side
var back_b := b - side
var back_peak := peak - side
_add_tri(vertices, normals, front_a, front_b, front_peak)
_add_tri(vertices, normals, back_b, back_a, back_peak)
_add_quad(vertices, normals, front_a, back_a, back_b, front_b)
_add_quad(vertices, normals, front_b, back_b, back_peak, front_peak)
_add_quad(vertices, normals, front_peak, back_peak, back_a, front_a)
return _array_mesh(vertices, normals)
func _make_right_gable_wall_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var base_points := _get_centered_gable_base() if center_align else PackedVector3Array([custom_points[0], custom_points[1]])
var a: Vector3 = base_points[0]
var b: Vector3 = base_points[1]
var direction := (b - a).normalized()
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
var peak := b + Vector3.UP * rise
var front_a := a + side
var front_b := b + side
var front_peak := peak + side
var back_a := a - side
var back_b := b - side
var back_peak := peak - side
_add_tri(vertices, normals, front_a, front_b, front_peak)
_add_tri(vertices, normals, back_peak, back_b, back_a)
_add_quad(vertices, normals, front_a, back_a, back_b, front_b)
_add_quad(vertices, normals, front_b, back_b, back_peak, front_peak)
_add_quad(vertices, normals, front_peak, back_peak, back_a, front_a)
return _array_mesh(vertices, normals)
func _make_roof_mesh() -> ArrayMesh:
if piece_type == PieceType.ROOF_SHED and custom_points.size() == 2:
return _make_shed_roof_mesh()
if custom_points.size() >= 4:
return _make_oriented_roof_mesh()
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var first: Vector3 = custom_points[0]
var second: Vector3 = custom_points[1]
var min_x := minf(first.x, second.x)
var max_x := maxf(first.x, second.x)
var min_z := minf(first.z, second.z)
var max_z := maxf(first.z, second.z)
var span_x := max_x - min_x
var span_z := max_z - min_z
if span_x < 0.02 or span_z < 0.02:
return _array_mesh(vertices, normals)
var panels: Array = []
if piece_type == PieceType.ROOF_GABLE:
if span_x <= span_z:
var ridge_x := (min_x + max_x) * 0.5
panels.append([Vector3(min_x, 0.0, min_z), Vector3(min_x, 0.0, max_z), Vector3(ridge_x, rise, max_z), Vector3(ridge_x, rise, min_z)])
panels.append([Vector3(ridge_x, rise, min_z), Vector3(ridge_x, rise, max_z), Vector3(max_x, 0.0, max_z), Vector3(max_x, 0.0, min_z)])
else:
var ridge_z := (min_z + max_z) * 0.5
panels.append([Vector3(min_x, 0.0, min_z), Vector3(max_x, 0.0, min_z), Vector3(max_x, rise, ridge_z), Vector3(min_x, rise, ridge_z)])
panels.append([Vector3(min_x, rise, ridge_z), Vector3(max_x, rise, ridge_z), Vector3(max_x, 0.0, max_z), Vector3(min_x, 0.0, max_z)])
else:
if span_x <= span_z:
var low_x := first.x
var high_x := second.x
panels.append([Vector3(low_x, 0.0, min_z), Vector3(low_x, 0.0, max_z), Vector3(high_x, rise, max_z), Vector3(high_x, rise, min_z)])
else:
var low_z := first.z
var high_z := second.z
panels.append([Vector3(min_x, 0.0, low_z), Vector3(max_x, 0.0, low_z), Vector3(max_x, rise, high_z), Vector3(min_x, rise, high_z)])
for panel in panels:
_append_solid_panel(vertices, normals, panel)
return _array_mesh(vertices, normals)
func _make_shed_roof_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var low: Vector3 = custom_points[0]
var high: Vector3 = custom_points[1]
var slope := high - low
var horizontal := Vector3(slope.x, 0.0, slope.z)
if horizontal.length_squared() < 0.0001 or roof_width <= 0.0:
return _array_mesh(vertices, normals)
var across := Vector3(horizontal.z, 0.0, -horizontal.x).normalized() * roof_width * 0.5
var panel := [low - across, low + across, high + across, high - across]
_append_solid_panel(vertices, normals, panel)
return _array_mesh(vertices, normals)
func _make_oriented_roof_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var p0: Vector3 = custom_points[0]
var p1: Vector3 = custom_points[1]
var p2: Vector3 = custom_points[2]
var p3: Vector3 = custom_points[3]
var edge_01 := Vector2(p1.x - p0.x, p1.z - p0.z).length()
var edge_12 := Vector2(p2.x - p1.x, p2.z - p1.z).length()
var panels: Array = []
if piece_type == PieceType.ROOF_GABLE:
if edge_01 <= edge_12:
var ridge_01 := (p0 + p1) * 0.5 + Vector3.UP * rise
var ridge_23 := (p2 + p3) * 0.5 + Vector3.UP * rise
panels.append([p0, p3, ridge_23, ridge_01])
panels.append([ridge_01, ridge_23, p2, p1])
else:
var ridge_12 := (p1 + p2) * 0.5 + Vector3.UP * rise
var ridge_30 := (p3 + p0) * 0.5 + Vector3.UP * rise
panels.append([p0, p1, ridge_12, ridge_30])
panels.append([ridge_30, ridge_12, p2, p3])
elif piece_type == PieceType.ROOF_PYRAMID:
var peak := (p0 + p1 + p2 + p3) * 0.25 + Vector3.UP * rise
panels.append([p0, p1, peak])
panels.append([p1, p2, peak])
panels.append([p2, p3, peak])
panels.append([p3, p0, peak])
else:
# The four points define the low and high edges. Keep their centers
# fixed when adjusting both edge widths in the Inspector.
var base_width := Vector2(p1.x - p0.x, p1.z - p0.z).length()
if base_width > 0.001 and roof_width > 0.0:
var direction := Vector3(p1.x - p0.x, 0.0, p1.z - p0.z).normalized()
var low_center := (p0 + p1) * 0.5
var high_center := (p2 + p3) * 0.5
var half_width := roof_width * 0.5
panels.append([
low_center - direction * half_width,
low_center + direction * half_width,
high_center + direction * half_width,
high_center - direction * half_width,
])
else:
panels.append([p0, p1, p2, p3])
for panel in panels:
_append_solid_panel(vertices, normals, panel)
return _array_mesh(vertices, normals)
func _make_stair_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var start: Vector3 = custom_points[0]
var finish: Vector3 = custom_points[1]
var horizontal := Vector3(finish.x - start.x, 0.0, finish.z - start.z)
var run := horizontal.length()
var rise_amount := finish.y - start.y
if run < 0.05 or rise_amount <= 0.05 or stair_count < 1 or stair_width <= 0.0:
return _array_mesh(vertices, normals)
var forward := horizontal / run
var width_axis := Vector3(-forward.z, 0.0, forward.x) * stair_width * 0.5
var profile := PackedVector2Array([Vector2(0.0, 0.0), Vector2(run, 0.0), Vector2(run, rise_amount)])
for step_index in range(stair_count - 1, -1, -1):
var tread_start := run * float(step_index) / stair_count
var tread_end := run * float(step_index + 1) / stair_count
var tread_height := rise_amount * float(step_index + 1) / stair_count
profile.append(Vector2(tread_start, tread_height))
if step_index > 0:
profile.append(Vector2(tread_start, rise_amount * float(step_index) / stair_count))
var triangulation := Geometry2D.triangulate_polygon(profile)
for side_sign in [-1.0, 1.0]:
for triangle_index in range(0, triangulation.size(), 3):
var a := profile[triangulation[triangle_index]]
var b := profile[triangulation[triangle_index + 1]]
var c := profile[triangulation[triangle_index + 2]]
var point_a: Vector3 = start + forward * a.x + Vector3.UP * a.y + width_axis * side_sign
var point_b: Vector3 = start + forward * b.x + Vector3.UP * b.y + width_axis * side_sign
var point_c: Vector3 = start + forward * c.x + Vector3.UP * c.y + width_axis * side_sign
if side_sign > 0.0:
_add_tri(vertices, normals, point_a, point_b, point_c)
else:
_add_tri(vertices, normals, point_c, point_b, point_a)
for profile_index in range(profile.size()):
var next_index := (profile_index + 1) % profile.size()
var a := profile[profile_index]
var b := profile[next_index]
var point_a_left := start + forward * a.x + Vector3.UP * a.y - width_axis
var point_b_left := start + forward * b.x + Vector3.UP * b.y - width_axis
var point_b_right := start + forward * b.x + Vector3.UP * b.y + width_axis
var point_a_right := start + forward * a.x + Vector3.UP * a.y + width_axis
_add_quad(vertices, normals, point_a_left, point_b_left, point_b_right, point_a_right)
return _array_mesh(vertices, normals)
func _make_fence_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var start: Vector3 = custom_points[0]
var finish: Vector3 = custom_points[1]
var path := finish - start
var path_length := path.length()
if path_length < 0.05 or fence_height <= 0.0:
return _array_mesh(vertices, normals)
var path_axis := path / path_length
var post_count := fence_divisions + 2
var post_positions: Array[Vector3] = []
for post_index in range(post_count):
var base_position := start.lerp(finish, float(post_index) / float(post_count - 1))
post_positions.append(base_position)
var side_axis := path_axis.cross(Vector3.UP).normalized()
if side_axis.length_squared() < 0.001:
side_axis = Vector3.FORWARD
_append_oriented_box(vertices, normals, base_position + Vector3.UP * fence_height * 0.5, path_axis, Vector3.UP, side_axis, Vector3(thickness, fence_height, thickness))
for section_index in range(post_positions.size() - 1):
var section_start := post_positions[section_index]
var section_end := post_positions[section_index + 1]
var section_axis := (section_end - section_start).normalized()
var rail_up := Vector3.UP - section_axis * section_axis.dot(Vector3.UP)
if rail_up.length_squared() < 0.001:
rail_up = Vector3.FORWARD - section_axis * section_axis.dot(Vector3.FORWARD)
rail_up = rail_up.normalized()
var rail_side := section_axis.cross(rail_up).normalized()
var section_length := section_start.distance_to(section_end)
for height_ratio in [0.28, 0.78]:
var rail_center: Vector3 = (section_start + section_end) * 0.5 + Vector3.UP * fence_height * height_ratio
_append_oriented_box(vertices, normals, rail_center, section_axis, rail_up, rail_side, Vector3(section_length, thickness, thickness))
return _array_mesh(vertices, normals)
func _append_oriented_box(vertices: PackedVector3Array, normals: PackedVector3Array, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3, dimensions: Vector3) -> void:
var x := axis_x.normalized() * dimensions.x * 0.5
var y := axis_y.normalized() * dimensions.y * 0.5
var z := axis_z.normalized() * dimensions.z * 0.5
var nnn := center - x - y - z
var pnn := center + x - y - z
var ppn := center + x + y - z
var npn := center - x + y - z
var nnp := center - x - y + z
var pnp := center + x - y + z
var ppp := center + x + y + z
var npp := center - x + y + z
_add_quad(vertices, normals, nnp, pnp, ppp, npp)
_add_quad(vertices, normals, pnn, nnn, npn, ppn)
_add_quad(vertices, normals, npp, ppp, ppn, npn)
_add_quad(vertices, normals, nnn, pnn, pnp, nnp)
_add_quad(vertices, normals, nnn, nnp, npp, npn)
_add_quad(vertices, normals, pnp, pnn, ppn, ppp)
func _get_centered_gable_base() -> PackedVector3Array:
var selected_a := to_global(custom_points[0])
var selected_b := to_global(custom_points[1])
var search_root: Node = get_parent()
while search_root != null and not search_root is ModularBuilderPieceGeometry:
search_root = search_root.get_parent()
if search_root == null:
return PackedVector3Array([custom_points[0], custom_points[1]])
var walls: Array[ModularBuilderPieceGeometry] = []
_collect_centering_walls(search_root, walls)
var centered_a := selected_a
var centered_b := selected_b
var nearest_a := INF
var nearest_b := INF
for wall in walls:
if wall.custom_points.size() < 2:
continue
var wall_a := wall.to_global(wall.custom_points[0] + Vector3.UP * wall.unit_size)
var wall_b := wall.to_global(wall.custom_points[1] + Vector3.UP * wall.unit_size)
var tolerance: float = wall.thickness + 0.04
for endpoint in [wall_a, wall_b]:
var distance_a := selected_a.distance_to(endpoint)
if distance_a < nearest_a and distance_a <= tolerance:
nearest_a = distance_a
centered_a = endpoint
var distance_b := selected_b.distance_to(endpoint)
if distance_b < nearest_b and distance_b <= tolerance:
nearest_b = distance_b
centered_b = endpoint
if nearest_a < INF and nearest_b < INF and centered_a.distance_to(centered_b) >= 0.02:
return PackedVector3Array([to_local(centered_a), to_local(centered_b)])
return PackedVector3Array([custom_points[0], custom_points[1]])
func _collect_centering_walls(node: Node, walls: Array[ModularBuilderPieceGeometry]) -> void:
if node is ModularBuilderPieceGeometry and node.piece_type == PieceType.WALL:
walls.append(node)
for child in node.get_children():
_collect_centering_walls(child, walls)
func _append_solid_panel(vertices: PackedVector3Array, normals: PackedVector3Array, panel: Array) -> void:
if panel.size() < 3:
return
var p0: Vector3 = panel[0]
var p1: Vector3 = panel[1]
var p2: Vector3 = panel[2]
var normal: Vector3 = (p1 - p0).cross(p2 - p0).normalized()
if normal.is_zero_approx():
return
var half_depth: Vector3 = normal * thickness * 0.5
var front: Array[Vector3] = []
var back: Array[Vector3] = []
for point in panel:
front.append(point + half_depth)
back.append(point - half_depth)
for i in range(1, panel.size() - 1):
_add_tri(vertices, normals, front[0], front[i], front[i + 1])
_add_tri(vertices, normals, back[i + 1], back[i], back[0])
for i in range(panel.size()):
var next := (i + 1) % panel.size()
_add_quad(vertices, normals, front[i], back[i], back[next], front[next])
func _make_custom_wall_mesh() -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var wall_points: PackedVector3Array = _get_centered_wall_points() if center_align else custom_points
var a: Vector3 = wall_points[0]
var b: Vector3 = wall_points[1]
var direction := (b - a).normalized()
var length := a.distance_to(b)
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
var height := Vector3.UP * unit_size
var openings: Array[PackedVector2Array] = []
for child in get_children():
if _is_detail_node(child) and int(child.get("detail_type")) != DETAIL_TYPE_HATCH and int(child.get("detail_type")) != DETAIL_TYPE_PILLAR:
var detail := child as Node3D
var center := Vector2((detail.position - a).dot(direction), detail.position.y - a.y)
var axis_x := Vector2(detail.transform.basis.x.dot(direction), detail.transform.basis.x.y).normalized()
var axis_y := Vector2(detail.transform.basis.y.dot(direction), detail.transform.basis.y.y).normalized()
var opening := PackedVector2Array([
center - axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
center + axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
center + axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5,
center - axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5
])
var wall_rect := PackedVector2Array([Vector2(0.0, 0.0), Vector2(length, 0.0), Vector2(length, unit_size), Vector2(0.0, unit_size)])
for clipped_opening in Geometry2D.intersect_polygons(opening, wall_rect):
if clipped_opening.size() >= 3:
openings.append(clipped_opening)
var xs: Array[float] = [0.0, length]
var ys: Array[float] = [0.0, unit_size]
for opening in openings:
for point in opening:
xs.append(clampf(point.x, 0.0, length))
ys.append(clampf(point.y, 0.0, unit_size))
xs.sort()
ys.sort()
for xi in range(xs.size() - 1):
for yi in range(ys.size() - 1):
var u0: float = xs[xi]
var u1: float = xs[xi + 1]
var v0: float = ys[yi]
var v1: float = ys[yi + 1]
var mid := Vector2((u0 + u1) * 0.5, (v0 + v1) * 0.5)
var cell := PackedVector2Array([Vector2(u0, v0), Vector2(u1, v0), Vector2(u1, v1), Vector2(u0, v1)])
var remaining: Array[PackedVector2Array] = [cell]
for opening in openings:
var next_remaining: Array[PackedVector2Array] = []
for polygon in remaining:
for clipped in Geometry2D.clip_polygons(polygon, opening):
if clipped.size() >= 3:
next_remaining.append(clipped)
remaining = next_remaining
for polygon in remaining:
var indices := Geometry2D.triangulate_polygon(polygon)
for ti in range(0, indices.size(), 3):
var p0: Vector2 = polygon[indices[ti]]
var p1: Vector2 = polygon[indices[ti + 1]]
var p2: Vector2 = polygon[indices[ti + 2]]
var f0 := a + direction * p0.x + Vector3.UP * p0.y
var f1 := a + direction * p1.x + Vector3.UP * p1.y
var f2 := a + direction * p2.x + Vector3.UP * p2.y
_add_tri(vertices, normals, f0 + side, f1 + side, f2 + side)
_add_tri(vertices, normals, f2 - side, f1 - side, f0 - side)
# Close the wall body at its ends, top, and base.
_add_quad(vertices, normals, a - side, a + side, a + side + height, a - side + height)
_add_quad(vertices, normals, b + side, b - side, b - side + height, b + side + height)
_add_quad(vertices, normals, a + side + height, b + side + height, b - side + height, a - side + height)
_add_quad(vertices, normals, a - side, b - side, b + side, a + side)
for opening in openings:
for edge_index in range(opening.size()):
var p0 := opening[edge_index]
var p1 := opening[(edge_index + 1) % opening.size()]
var a0 := a + direction * p0.x + Vector3.UP * p0.y
var a1 := a + direction * p1.x + Vector3.UP * p1.y
_add_quad(vertices, normals, a0 + side, a0 - side, a1 - side, a1 + side)
return _array_mesh(vertices, normals)
func _get_centered_wall_points() -> PackedVector3Array:
if custom_points.size() < 2:
return custom_points
var selected_a := to_global(custom_points[0])
var selected_b := to_global(custom_points[1])
var search_root: Node = get_parent()
while search_root != null and not search_root is ModularBuilderPieceGeometry:
search_root = search_root.get_parent()
if search_root == null:
return custom_points
var walls: Array[ModularBuilderPieceGeometry] = []
_collect_centering_walls(search_root, walls)
var centered_a := selected_a
var centered_b := selected_b
var nearest_a := INF
var nearest_b := INF
for wall in walls:
if wall == self or wall.custom_points.size() < 2:
continue
var direction := (wall.custom_points[1] - wall.custom_points[0]).normalized()
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5
for endpoint in wall.custom_points:
var center := wall.to_global(endpoint + Vector3.UP * wall.unit_size)
for surface_corner in [center + side, center - side]:
var distance_a := selected_a.distance_to(surface_corner)
if distance_a < nearest_a and distance_a <= wall.thickness + 0.04:
nearest_a = distance_a
centered_a = center
var distance_b := selected_b.distance_to(surface_corner)
if distance_b < nearest_b and distance_b <= wall.thickness + 0.04:
nearest_b = distance_b
centered_b = center
if nearest_a < INF and nearest_b < INF and centered_a.distance_to(centered_b) >= 0.02:
return PackedVector3Array([to_local(centered_a), to_local(centered_b)])
return custom_points
func _make_custom_floor_mesh() -> ArrayMesh:
var polygon: Array[Vector2] = []
for point in custom_points:
polygon.append(Vector2(point.x, point.z))
var hatch_mesh := _make_hatch_surface_mesh(polygon, custom_points[0].y, custom_points[0].y - thickness)
if hatch_mesh != null:
return hatch_mesh
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var points := custom_points
var bottom: Array[Vector3] = []
for point in points:
bottom.append(point - Vector3.UP * thickness)
for i in range(1, points.size() - 1):
_add_tri(vertices, normals, points[0], points[i + 1], points[i])
_add_tri(vertices, normals, bottom[0], bottom[i], bottom[i + 1])
for i in range(points.size()):
var j := (i + 1) % points.size()
_add_quad(vertices, normals, bottom[i], bottom[j], points[j], points[i])
return _array_mesh(vertices, normals)
func _make_hatch_surface_mesh(polygon: Array[Vector2], top_y: float, bottom_y: float) -> ArrayMesh:
var openings: Array[PackedVector2Array] = []
for child in get_children():
if _is_detail_node(child) and int(child.get("detail_type")) == DETAIL_TYPE_HATCH:
var detail := child as Node3D
var center := Vector2(detail.position.x, detail.position.z)
var axis_x := Vector2(detail.transform.basis.x.x, detail.transform.basis.x.z).normalized()
var axis_y := Vector2(detail.transform.basis.z.x, detail.transform.basis.z.z).normalized()
var opening := PackedVector2Array([
center - axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
center + axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
center + axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5,
center - axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5
])
openings.append(opening)
if openings.is_empty() or polygon.size() < 3:
return null
var min_x := polygon[0].x
var max_x := polygon[0].x
var min_z := polygon[0].y
var max_z := polygon[0].y
var xs: Array[float] = []
var zs: Array[float] = []
for point in polygon:
min_x = minf(min_x, point.x)
max_x = maxf(max_x, point.x)
min_z = minf(min_z, point.y)
max_z = maxf(max_z, point.y)
xs.append(point.x)
zs.append(point.y)
for opening in openings:
for point in opening:
xs.append(clampf(point.x, min_x, max_x))
zs.append(clampf(point.y, min_z, max_z))
xs.sort()
zs.sort()
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var source_polygon := PackedVector2Array()
for point in polygon:
source_polygon.append(point)
for xi in range(xs.size() - 1):
for zi in range(zs.size() - 1):
var x0: float = xs[xi]
var x1: float = xs[xi + 1]
var z0: float = zs[zi]
var z1: float = zs[zi + 1]
if x1 - x0 < 0.0001 or z1 - z0 < 0.0001:
continue
var cell := PackedVector2Array([Vector2(x0, z0), Vector2(x1, z0), Vector2(x1, z1), Vector2(x0, z1)])
var remaining: Array[PackedVector2Array] = []
for clipped_surface in Geometry2D.intersect_polygons(source_polygon, cell):
remaining.append(clipped_surface)
for opening in openings:
var next_remaining: Array[PackedVector2Array] = []
for surface_polygon in remaining:
for clipped_surface in Geometry2D.clip_polygons(surface_polygon, opening):
if clipped_surface.size() >= 3:
next_remaining.append(clipped_surface)
remaining = next_remaining
for clipped_surface in remaining:
var triangulated := Geometry2D.triangulate_polygon(clipped_surface)
for ti in range(0, triangulated.size(), 3):
var a: Vector2 = clipped_surface[triangulated[ti]]
var b: Vector2 = clipped_surface[triangulated[ti + 1]]
var c: Vector2 = clipped_surface[triangulated[ti + 2]]
_add_tri(vertices, normals, Vector3(a.x, top_y, a.y), Vector3(c.x, top_y, c.y), Vector3(b.x, top_y, b.y))
_add_tri(vertices, normals, Vector3(a.x, bottom_y, a.y), Vector3(b.x, bottom_y, b.y), Vector3(c.x, bottom_y, c.y))
for i in range(polygon.size()):
var j := (i + 1) % polygon.size()
var a := polygon[i]
var b := polygon[j]
_add_quad(vertices, normals, Vector3(a.x, bottom_y, a.y), Vector3(b.x, bottom_y, b.y), Vector3(b.x, top_y, b.y), Vector3(a.x, top_y, a.y))
for opening in openings:
var visible_edges := Geometry2D.intersect_polygons(opening, source_polygon)
for visible_opening in visible_edges:
for edge_index in range(visible_opening.size()):
var p0 := visible_opening[edge_index]
var p1 := visible_opening[(edge_index + 1) % visible_opening.size()]
_add_quad(vertices, normals, Vector3(p0.x, bottom_y, p0.y), Vector3(p0.x, top_y, p0.y), Vector3(p1.x, top_y, p1.y), Vector3(p1.x, bottom_y, p1.y))
return _array_mesh(vertices, normals)
func _is_detail_node(node: Node) -> bool:
if not node is Node3D or node.get_script() == null:
return false
return node.get_script().resource_path == DETAIL_SCRIPT_PATH
func _make_custom_foundation_mesh() -> ArrayMesh:
var polygon: Array[Vector2] = []
for point in custom_points:
polygon.append(Vector2(point.x, point.z))
var bottom_y := -thickness * 0.5 if centered_origin else 0.0
var top_y := thickness * 0.5 if centered_origin else thickness
var hatch_mesh := _make_hatch_surface_mesh(polygon, top_y, bottom_y)
if hatch_mesh != null:
return hatch_mesh
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
for i in range(1, custom_points.size() - 1):
var a := custom_points[0]
var b := custom_points[i]
var c := custom_points[i + 1]
_add_tri(vertices, normals, Vector3(a.x, top_y, a.z), Vector3(c.x, top_y, c.z), Vector3(b.x, top_y, b.z))
_add_tri(vertices, normals, Vector3(a.x, bottom_y, a.z), Vector3(b.x, bottom_y, b.z), Vector3(c.x, bottom_y, c.z))
for i in range(custom_points.size()):
var j := (i + 1) % custom_points.size()
var a := custom_points[i]
var b := custom_points[j]
_add_quad(vertices, normals, Vector3(a.x, bottom_y, a.z), Vector3(b.x, bottom_y, b.z), Vector3(b.x, top_y, b.z), Vector3(a.x, top_y, a.z))
return _array_mesh(vertices, normals)
func _add_quad(v: PackedVector3Array, n: PackedVector3Array, a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> void:
_add_tri(v, n, a, b, c)
_add_tri(v, n, a, c, d)
func _array_mesh(v: PackedVector3Array, n: PackedVector3Array) -> ArrayMesh:
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = v
arrays[Mesh.ARRAY_NORMAL] = n
var result := ArrayMesh.new()
result.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return result
func _add_tri(v: PackedVector3Array, n: PackedVector3Array, a: Vector3, b: Vector3, c: Vector3) -> void:
var normal := (b - a).cross(c - a).normalized()
v.append(a)
v.append(b)
v.append(c)
n.append(normal)
n.append(normal)
n.append(normal)
# Also generate reversed faces to show both sides.
# This keeps floors and foundations visible from below with any material.
v.append(c)
v.append(b)
v.append(a)
n.append(-normal)
n.append(-normal)
n.append(-normal)

View file

@ -1 +0,0 @@
uid://dp1333xlylabf

View file

Binary file not shown.

1
core Submodule

@ -0,0 +1 @@
Subproject commit 5e98535a0afc83af4e4017806686a76af4913afd

Some files were not shown because too many files have changed in this diff Show more