From 3a5c9d44bbbad7270d771006855c3c35138c1735 Mon Sep 17 00:00:00 2001 From: "Hermes (godot-vr)" Date: Thu, 15 Oct 2026 12:18:11 +0000 Subject: [PATCH] Objekt-Bibliothek: Gruppen-Browser mit Thumbnails, Snap-to-Ground, Raster-Snap, Tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bibliothek-Browser: 11 etablierte Objekte in deutschen Klapp-Gruppen (Kisten & Barrieren / Bauwerke / Natur, neue IDs wandern nach Sonstiges) - 128-px-Thumbnails einmalig gerendert (tools/gen_thumbs.gd, xvfb) und committed; fehlendes PNG = sauberer Text-Fallback (LibraryBrowser) - Snap to Ground: ein Klick setzt ALLE schwebenden Objekte auf Terrain-Höhe, EIN Undo-Eintrag (ObjectOps.snap_to_ground) - Raster-Snap (1 m): Toggle — rastet Platzieren und Ziehen auf ganze Meter - Deutsche Namen nach LibraryBrowser verschoben (headless testbar) - tests/test_library.gd: 47 Checks (Bibliothek, Gruppen, Thumbnails, Snap, CRUD+Undo über alle Objekte, svmap-Roundtrip byte-identisch, Poly-Budget) - Desktop-Smoke: --library-demo erzeugt Screenshots (lokal via xvfb verifiziert) - UI-Layout: linke Spalte min 909→797 px, Statusleiste wieder vollständig sichtbar --- README.md | 37 ++- library/thumbs/barrier_sandbag.png | Bin 0 -> 848 bytes library/thumbs/barrier_sandbag.png.import | 40 +++ library/thumbs/boulder_small.png | Bin 0 -> 1534 bytes library/thumbs/boulder_small.png.import | 40 +++ library/thumbs/bunker_concrete.png | Bin 0 -> 979 bytes library/thumbs/bunker_concrete.png.import | 40 +++ library/thumbs/bush_small.png | Bin 0 -> 1534 bytes library/thumbs/bush_small.png.import | 40 +++ library/thumbs/crate_metal.png | Bin 0 -> 920 bytes library/thumbs/crate_metal.png.import | 40 +++ library/thumbs/crate_wood.png | Bin 0 -> 911 bytes library/thumbs/crate_wood.png.import | 40 +++ library/thumbs/fence_metal.png | Bin 0 -> 737 bytes library/thumbs/fence_metal.png.import | 40 +++ library/thumbs/rock_large.png | Bin 0 -> 1632 bytes library/thumbs/rock_large.png.import | 40 +++ library/thumbs/tree_pine.png | Bin 0 -> 606 bytes library/thumbs/tree_pine.png.import | 40 +++ library/thumbs/wall_concrete.png | Bin 0 -> 775 bytes library/thumbs/wall_concrete.png.import | 40 +++ library/thumbs/watchtower_wood.png | Bin 0 -> 773 bytes library/thumbs/watchtower_wood.png.import | 40 +++ src/editor/interaction.gd | 17 +- src/editor/library_browser.gd | 94 +++++++ src/editor/library_browser.gd.uid | 1 + src/editor/object_ops.gd | 34 +++ src/editor/object_ops.gd.uid | 1 + src/ui/main.gd | 173 +++++++++--- tests/test_library.gd | 307 ++++++++++++++++++++++ tests/test_library.gd.uid | 1 + tools/gen_thumbs.gd | 100 +++++++ tools/gen_thumbs.gd.uid | 1 + tools/parse_check.sh | 15 ++ tools/run_tests.sh | 13 + 35 files changed, 1195 insertions(+), 39 deletions(-) create mode 100644 library/thumbs/barrier_sandbag.png create mode 100644 library/thumbs/barrier_sandbag.png.import create mode 100644 library/thumbs/boulder_small.png create mode 100644 library/thumbs/boulder_small.png.import create mode 100644 library/thumbs/bunker_concrete.png create mode 100644 library/thumbs/bunker_concrete.png.import create mode 100644 library/thumbs/bush_small.png create mode 100644 library/thumbs/bush_small.png.import create mode 100644 library/thumbs/crate_metal.png create mode 100644 library/thumbs/crate_metal.png.import create mode 100644 library/thumbs/crate_wood.png create mode 100644 library/thumbs/crate_wood.png.import create mode 100644 library/thumbs/fence_metal.png create mode 100644 library/thumbs/fence_metal.png.import create mode 100644 library/thumbs/rock_large.png create mode 100644 library/thumbs/rock_large.png.import create mode 100644 library/thumbs/tree_pine.png create mode 100644 library/thumbs/tree_pine.png.import create mode 100644 library/thumbs/wall_concrete.png create mode 100644 library/thumbs/wall_concrete.png.import create mode 100644 library/thumbs/watchtower_wood.png create mode 100644 library/thumbs/watchtower_wood.png.import create mode 100644 src/editor/library_browser.gd create mode 100644 src/editor/library_browser.gd.uid create mode 100644 src/editor/object_ops.gd create mode 100644 src/editor/object_ops.gd.uid create mode 100644 tests/test_library.gd create mode 100644 tests/test_library.gd.uid create mode 100644 tools/gen_thumbs.gd create mode 100644 tools/gen_thumbs.gd.uid create mode 100644 tools/parse_check.sh create mode 100644 tools/run_tests.sh diff --git a/README.md b/README.md index e50e011..cdb76ff 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,37 @@ trotz grünem Test-Lauf (der ja selbst mit importiertem Cache lief). Registry aus dem Match-Server-Repo — neue Objekte DORT registrieren und hier synchron ziehen (IDs sind der Vertrag, Entscheidung 20). -**Noch offen (folgende Karten der MVP-Kette):** Terrain-Werkzeuge -(heben/senken/glätten — nutzen ActionHistory mit `["terrain"]`), -Spawn-Zonen-Editor, Performance-Check-UI, Export-Workflow. +## Status (M2, Karte t_3a998ae4 — Objekt-Bibliothek FERTIG) + +Auf dem Terrain-Werkzeug (t_9aba04f1) aufgebaut, nach dem Chef-Grundsatz: + +- **Bibliothek-Browser mit Gruppen (`src/editor/library_browser.gd`):** Die + 11 etablierten Objekte aus `library/objects.json`, sortiert in deutsche + Klapp-Gruppen (Kisten & Barrieren / Bauwerke / Natur + automatisch + „Sonstiges" für neue, noch ungruppierte IDs). Jeder Button zeigt ein + 128-px-Thumbnail (`library/thumbs/*.png` — einmalig mit + `xvfb-run -a godot4 --rendering-driver opengl3 --path . --script + res://tools/gen_thumbs.gd` gerendert und committed; fehlt ein PNG, fällt + der Button sauber aufs Text-Layout zurück). Deutsche Namen wohnen jetzt in + `LibraryBrowser` (ohne UI → headless testbar). +- **Snap to Ground (`src/editor/object_ops.gd`):** Ein Klick auf + „⚡ Alles auf den Boden setzen" holt ALLE schwebenden Objekte auf die + Terrain-Höhe (z. B. nach dem Terrain-Formen) — EIN Undo-Eintrag für die + ganze Aktion. Objekte, die schon auf dem Boden stehen, bleiben unangetastet. +- **Raster-Snap (1 m):** CheckButton in den Objekt-Werkzeugen — beim + Platzieren UND Ziehen rasten X/Z auf ganze Meter (Höhe folgt weiter dem + Terrain). AUS = freies Platzieren wie bisher. +- **svmap-Roundtrip:** Objekte (Bibliotheks-ID + Pos/Rot/Scale) überleben + Speichern/Laden identisch, save→load→save bleibt byte-identisch + (`tests/test_library.gd`). +- **Tests:** `tests/test_library.gd` (Bibliothek laden, Gruppen komplett/ + überschneidungsfrei, Thumbnails 128×128, Raster-Snap, Snap-to-Ground + je + Undo für setzen/drehen/verschieben/löschen über ALLE 11 Objekte, + svmap-Roundtrip mit Objekten + Poly-Budget aus dem Katalog). +- **Desktop-Smoke:** `godot … -- --library-demo [ordner] [--library-demo-quit]` + fährt Bibliothek/Platzierung/Schweben/Snap programmatisch durch und + speichert Screenshots. + +**Noch offen (folgende Karten der MVP-Kette):** Spawn-Zonen-Editor +(t_c4162c5d — Spawn-Zonen malen, Live-Validierung, Export-Signierung), +Performance-Check-UI, Export-Workflow. diff --git a/library/thumbs/barrier_sandbag.png b/library/thumbs/barrier_sandbag.png new file mode 100644 index 0000000000000000000000000000000000000000..94027a8c9ae98e7bde4c4bdcac86591c0b3580d1 GIT binary patch literal 848 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>VCM34aSW-L z^Y)Hm-fahg)`!m((l5wfT5={c=$I>)`8pEm5=rI|%Gd$xsAj3FAnW2~^p^xE=V1p$M z1!Fcd+SuA%scR9*i~5oOu(nm`OdB-=Yavr4|hVSLu^Y_0m z+pTr}fxwa9xA+a*ua{0*Cx57dd8>p=7ekj&Q%JhDjMbz~?tswjsA~Q+1 zBSbpE>+M;CDQ|CNC^anfF!cD?oT1mSFayYF%J`;LvF7!z?ejh6$;p5Ddir_R`uktl zW(2*?etM&B|N39QCS@|Xu2oh#^nm*{L)$u0=Oqul558wy^0S7cbUv6(ozt0 znNh^W;4Dam!BMpaB`20ZI|eV#|6&`KuKl;p;Jci{r3c)*c@0X=<_Z-4nDmoPB5F79 zvpv5T^TjJQ{MX@q7{V~IU4N!LV|z}ej*I-hBWxB~=gel@Vvuap-^kbpRJ@5NK8g8^ z*;d|T(h8S9sG1$fDczypjr#dXupJ2E``!9$ZI^-llp X?H4w`cD+snW)TKYS3j3^P6_z=fcrF5FVxLeu3TRzD@sY}@mSj6?Y>SyNcl z88i27m~^1Kt&5mdiJxdHdVJs9H0)MK`YH5-PY}k?=sf1jy}fC~Lyu8WaytUewm#xi zpL#7U+O|K^YqMI-ZMJj0I5$JW8@3^yY(kj!%i#T2#B)ksKHnc@OXrI>)>HSv$?Hm#Vi{#x+M}Aap_j%Rr~l}~hXjRYJeS~@ zim|&W3oP0Z*u?M{(vYJ#VWW? zCx-;FMDF^c8*m)>?|vSE~! zSeqdgBMa#zglkh*lbQn`_w0#K-wmRBTFIt0W~7hV6yyV-xEVk_KO){97%{jbjebWD z(Fxbx(>purY8>8~6HV~&PDqKBy8c4A#M=2Z>#i>YdyS-%^DRyPPlnL7#EAxS2HDTX z%Y1U`w+^ANvV}L|{)KMK z-*km0(9WsPvKPDFV1=U#9j^~mT40ZHE6Qb>EH(2ftHPxNE;C-DdfxYH-d*BGGkAl5 zD$D!hJwgi1do2$wvT3)L?+>)I}RHAZoH(8P{T5>@n=sj)WDS*yq z{_EEWII$M#qY6XnukMaXLC7YMUCk2cZoKF*ivtP-HjtI!cUqinI71`E3B-RN#JeHF}2H2z}T9e3BB6nlGfH%S)tiV z2mlv8i|w3o#1oM8wvaDEOQ&mYYLb@fUnYALn5~2X(7{q3NKH!{bM|zDjYM+lh6pd) z{DcMzNM){ITw;3WsoEaz9)e{bRhP+t*SFfeszcTs$%LVM@{hm8#3W2goH0m?$6+W-In literal 0 HcmV?d00001 diff --git a/library/thumbs/boulder_small.png.import b/library/thumbs/boulder_small.png.import new file mode 100644 index 0000000..ee29da7 --- /dev/null +++ b/library/thumbs/boulder_small.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://der1lfrcvoeb8" +path="res://.godot/imported/boulder_small.png-c4fad8ba15711e8ff7d084f1741e1559.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://library/thumbs/boulder_small.png" +dest_files=["res://.godot/imported/boulder_small.png-c4fad8ba15711e8ff7d084f1741e1559.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 diff --git a/library/thumbs/bunker_concrete.png b/library/thumbs/bunker_concrete.png new file mode 100644 index 0000000000000000000000000000000000000000..f551fc0c4c8cc4d39e060406db5f74dac9cc224e GIT binary patch literal 979 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>VD9yFaSW-L z^Y)Hy-{k<2qaTHjh)uG+5U@$gi>)(Bqm7L-P*FunV}V1HW@N-2tJ{?sCD-8hO=qR3jQ3m(r1%Jo=G$O*v{JZ_uqNdwPD)br)T!IE8NLx zu*<#8bV9nRU03kYu_vX6Sp%-zRw&}Rd$4~0 zf792m+j$p=6>3N9eY`v74s*@FUymPr?>_oyQN8-1TBpk3@m@%O5f-ujx^c|eFCqt+f4vjk#IrAefq9C+w%c#(>&>nOrWP`DSnOgK zkY2|;BTG|G!>q4n zC%(L6XJTWx;cg)Iy4+si1ILS5uRS!}9lqrMD`~yMu+?(A-<$!aSAV-KA?BB^?Y+T7nr}WbGWvSW0>e zRb1rk9QHA0@#i)tODp^Wg_z=fcrF5FVxLeu3TRzD@sY}@mSj6?Y>SyNcl z88i27m~^1Kt&5mdiJxdHdVJs9H0)MK`YH5-PY}k?=sf1jy}fC~Lyu8WaytUewm#xi zpL#7U+O|K^YqMI-ZMJj0I5$JW8@3^yY(kj!%i#T2#B)ksKHnc@OXrI>)>HSv$?Hm#Vi{#x+M}Aap_j%Rr~l}~hXjRYJeS~@ zim|&W3oP0Z*u?M{(vYJ#VWW? zCx-;FMDF^c8*m)>?|vSE~! zSeqdgBMa#zglkh*lbQn`_w0#K-wmRBTFIt0W~7hV6yyV-xEVk_KO){97%{jbjebWD z(Fxbx(>purY8>8~6HV~&PDqKBy8c4A#M=2Z>#i>YdyS-%^DRyPPlnL7#EAxS2HDTX z%Y1U`w+^ANvV}L|{)KMK z-*km0(9WsPvKPDFV1=U#9j^~mT40ZHE6Qb>EH(2ftHPxNE;C-DdfxYH-d*BGGkAl5 zD$D!hJwgi1do2$wvT3)L?+>)I}RHAZoH(8P{T5>@n=sj)WDS*yq z{_EEWII$M#qY6XnukMaXLC7YMUCk2cZoKF*ivtP-HjtI!cUqinI71`E3B-RN#JeHF}2H2z}T9e3BB6nlGfH%S)tiV z2mlv8i|w3o#1oM8wvaDEOQ&mYYLb@fUnYALn5~2X(7{q3NKH!{bM|zDjYM+lh6pd) z{DcMzNM){ITw;3WsoEaz9)e{bRhP+t*SFfeszcTs$%LVM@{hm8#3W2goH0m?$6+W-In literal 0 HcmV?d00001 diff --git a/library/thumbs/bush_small.png.import b/library/thumbs/bush_small.png.import new file mode 100644 index 0000000..9c4e3ef --- /dev/null +++ b/library/thumbs/bush_small.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://rc4qmlu30oe0" +path="res://.godot/imported/bush_small.png-8dd2b0f2820d02d8f683642ea139221a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://library/thumbs/bush_small.png" +dest_files=["res://.godot/imported/bush_small.png-8dd2b0f2820d02d8f683642ea139221a.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 diff --git a/library/thumbs/crate_metal.png b/library/thumbs/crate_metal.png new file mode 100644 index 0000000000000000000000000000000000000000..905d99aac222fe20531ea2eb8647fade58fbee90 GIT binary patch literal 920 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>U=H$haSW-L z^Y+f(qRS2&LxQ*fMWwT*H*53@0=hinto)FexmjG4oF7OV%kL zUVhy7{rmm<_Ii6JeOh(mfAEV73zm1uxOFfT@m8(kNZi4taC|}e?mz$UtXjoa_g0f< zGh>hBH<`n8PPe!BFP(nj^c}?ycW+<+eXvUEgJ%A^ct(bO|Nli-sJ5SBy>KgU`|tJP zt3wY?os+-$=9IWYo@vY;S4;Q)zg5K}*EX+6(BV`P&+D&M|4Y9+WX#>o5Tu&y(f)4c zYlfgq9*2Jli+J{&eq#FJ-8Z$@^UL#ip7Z>@p?%;MF9XLGW*OtBp@LsM<(&i_R_V9J zIDao=VBr9n-McK)OMkJ(UmFHXR)=#b4089E#?9n;@cH`urPC+OJ*uscV8YP9=Uo&m zfM@a^xNLp(a@6iKYzn6hq#c&5UT_tt$bv!NCjWl}(}Hv>hWGx{*_Sa-_^H{z?Q&z` z8Fqn(vvX#?WYCFcJ>Hi2lZ!)w-{BcBs&;Z5xMg|ZR+ZjEC5D;(OfxhtUFNsoSg@C~ z;?J?@{frEJObj{k?Clj2b`Mk-9@MieEpOB)|3BrVKl}VipD!N{wv#H;V(@Ry-+7ZU zD1P7Hs6X#}ENTv9GC0iSXseL5d+mdKI;Vst0Pj?O AVD|KMaSW-L z^Y)Hy(G>@gBOk?%N`B*0X}NPEMa_VbMa`Izkww7a6sImVzP z?y{_re<@eOi%EACKVR@($*keq>6rRLbtZ#XDo^Zn-EXCfZ5bKl8zdHD7{aV7V^4!a2r{#XJg+7b`qgR&bDF5O_ULDs!## z7RDQ9G7T2&1q)Bywtv9zB#>R;@2jo`vj(63rZxQy422AWDK{3rU_S9d$#^k;L+CUE zS)gi#KC6a*o(;LX=WdpJrq0k{!w@~6c}8ch`L2)|44d|T(0Re!@kiS6+i!b^AO9GV zpY1C@AX5A<`^J@6jYAKrzgRnH9?BEQ{Nb>Hf#JdB>GGlb-cMO=f9peI10%y%B?;j| zpp5YWP4nyF;(z~YrZ;R{ZU5>)wPJZg(Am2RzdkgwFfzP$D|q+nyg`kEC&Phl){fI2 zGIO3|SboM*yk{VYX@0--9-~)qIrGwo%ub99mz4sf>=eWp4CD@OkKb<|!^u7q;+z+| p{E9xV4Cge;uumf z=j~nFzRLj;ZV$@^vJ0*`IG^kbTISi>vO+*el$VutN{-&mhSLE(GLhFN%=bL>*YbIY zoMupr&98Oq^OsasegAh@vF)wzOX&@pI2q++Cyp zZ;gWA^*<~Y*K3^4Oz(CP;AA+k$FdDb^BmX|te_!rfnl29bZ*8!%BPAH8@OMKwlKuq zlsRI+bGAv~fNYdsyK2WQLyiZGTerlR{8Tg8`7SrX_w0Qizo{$_S$>>9yZo?4AM5)k zmi7DrJLd2IFY>pJ@qjRc4UlkHm{q~x!_wf-z^c8M*+Yu$!{6iUFKxfzdX~v!o*>7= zZL`=6u1d8;$uUg!WANF^wyd>uJrxWyTGf>IbHNH4|j{aH^_Np+P!Zwvpl81=Y(7j>$SGHSE6j zXww6|2aRXTc}(9i2&o@%&Q4UaV0>W7NUP|+7hg2taKgQW{GY%y%i!ti=d#Wzp$Pz! Cwj!bc literal 0 HcmV?d00001 diff --git a/library/thumbs/fence_metal.png.import b/library/thumbs/fence_metal.png.import new file mode 100644 index 0000000..b60ec8e --- /dev/null +++ b/library/thumbs/fence_metal.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://kul1hamegxs0" +path="res://.godot/imported/fence_metal.png-eb6d7564dcec24bf8e9b4d5ec9d52a5a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://library/thumbs/fence_metal.png" +dest_files=["res://.godot/imported/fence_metal.png-eb6d7564dcec24bf8e9b4d5ec9d52a5a.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 diff --git a/library/thumbs/rock_large.png b/library/thumbs/rock_large.png new file mode 100644 index 0000000000000000000000000000000000000000..b51d5f68c2ae79bba9a9ade9768e21640c8ec65b GIT binary patch literal 1632 zcmchYX*kq-6vu!6X{=cz$&&rz30bO9wlQ`^$k@knyY<-RvBfovQP*|{C1VQ{5n+_I zFf#FQyJL+^t}I0cW2?rJG2FSY@B3by^L_DobIyzNIT=olHo}53f&c(vJ6lT^ZsPtc zeqL_xH4UI~1B!C7K>_lx+zJ4^GIo~cu6G`-KN<@8Xf2(V{LlSZ?6;$_D>;; z^DB@2SSo3#V-C7XyaO*X*83cUswh=Xf>mPsk%3D0T1G97b8-@=w#h9I68JZ7#TpyD zE6$%7W1H&Q65YHAvy9Ty*!oI$Uu?tO9-=v&Pcb@+zei3ScS#vUS%Rf>FenZcs=#z@ z;QrY^2~5J|6xQbV{GT#=mmj9njs#8PU#3z0aT=LYvq!d{@uH+Q&;gGx zQ=#e0ne2BA1-GC!`$3MvM~erlPWIYObM7o6)yMI>y{4)izLK*}u4|7?Q0rOX+a==3 zd~`I+zmu2;x38VG8_m+0zr879UFm#tE9E-|XUhMic9v%u1I6&gm)*Er`gyTVeci1E z`b-w}mlsOw^*ViRuODi0EJt|%(WIp(CsNYraU4F!!(Gyxr-fl&5NdnpwW$wS|cpNs8)cTOYd&iTXqZa8*ldqMb`D`rU?of0>5_I8G@w9s?P z_SC)Atz<*WJbt3B9?`h4{bi}OJHn{BaJq=SD}S9IuHG|aRDO+I`^6zcqWW>)!^82@aMW3m9)kW+1iqL$sVEHSE1-t__R;N zw2mcSg5-#Fn<>9U2^6@m$U_RkKrFX6llPX{A7*tvha2 z7}9{Lk>n0j?NiEsNdfRn-dp9$e3z^>gg4~Bt}N;uzg1djmh2(?s+VG0AB7dF{KICwhx{V;s2ded9`L{#WH#;V3P23aSW-L z^Y-?_KH)%#hKswGtn=TL_H-Yg(Ba()1}ZHqn}u2(Ke=o*eXFqRZssYLlcLY9S4oym z{{3~n_2h34KfbJ#Ox!zrZ^M~Th8;`?L>bmG6tFgEGsG}H;9>|TLa1!BxWVD${O^y~ z?U&zuH*b#L@4J!*tGTL^Usxsyq+a9@91B^pb)L! zw2Rf@%GrBPY#YAHADF+t&OhR0O$tM}FOyA!2*Y}?hZ_>**|>oowlHJ(dwfB74#Q3j zSQs3(;}Am%mKzLt{{L)QTad!bpo)uQ4MRZ=!>1ejkFrhZ28QE-4f*V_a7-v+Q#d)h zuABFP7k?w~gUip?=g;3?Q$2IXq@8kd^XI)?&(Jp)$v(280X?$r$h+QJYs17d-x-)- O7(8A5T-G@yGywpsn%#;3 literal 0 HcmV?d00001 diff --git a/library/thumbs/tree_pine.png.import b/library/thumbs/tree_pine.png.import new file mode 100644 index 0000000..8efdf3a --- /dev/null +++ b/library/thumbs/tree_pine.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3x487vku5uc7" +path="res://.godot/imported/tree_pine.png-a6c8426a9685830a95298331fa9d351e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://library/thumbs/tree_pine.png" +dest_files=["res://.godot/imported/tree_pine.png-a6c8426a9685830a95298331fa9d351e.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 diff --git a/library/thumbs/wall_concrete.png b/library/thumbs/wall_concrete.png new file mode 100644 index 0000000000000000000000000000000000000000..87decfcc7156d505ced410a2376deb5f4b2a5d9b GIT binary patch literal 775 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>U^?jO;uumf z=k48%d6xqu+&|U}o}XaD%&qKy*n_=IFSg5NriPGCtd+r(iQ=bc>~T0|pwNA6YcsQ7 z&HD3}L2TM9OP2om5*g3-{n6Fe=DQ`fi3iIcSmw*{frY`Jw#56VSJ*AKyoVc*@mcE9&%{8}yKu-mL-iTu&L@4qjekhiO~{CiL< zLyTcp*5U{qvFhvb_VqR2%w0q_vtG#CeDh80c1IN-5ebI3w-hENYCL0O;$EQrXo|pr z#UVkm z`HD^N_OQ;`Vb=7v%=)6|u3{#i8{g&am)kWlyx@G$Gu7;$FyjMZ1{=l$!UtTYZeUcI z%Cw`t0%+dmm0k>HaZDD$s_Pm$89s0_%wtenF@;w_kiib97N}v)!l3X2OOhDA^Y{N# zvT(Y}l(CA}(No&Nh?$}D@`L(WtPN_4GWXXom~&zx`@zoJCIDjde!PC{xWt~$(695xh BB8mV2 literal 0 HcmV?d00001 diff --git a/library/thumbs/wall_concrete.png.import b/library/thumbs/wall_concrete.png.import new file mode 100644 index 0000000..cd306c2 --- /dev/null +++ b/library/thumbs/wall_concrete.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dhl43w3l7s05c" +path="res://.godot/imported/wall_concrete.png-aba74f819605dd2ba26846285fc5e701.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://library/thumbs/wall_concrete.png" +dest_files=["res://.godot/imported/wall_concrete.png-aba74f819605dd2ba26846285fc5e701.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 diff --git a/library/thumbs/watchtower_wood.png b/library/thumbs/watchtower_wood.png new file mode 100644 index 0000000000000000000000000000000000000000..eb58b690e02d587b8b69f0aad2a7d6b8f9644f22 GIT binary patch literal 773 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>VA}8L;uumf z=j~m?qRS34ZXf-5%0EbEXzUbJ<>mE#;pEt5A|et{qC@BX$sw|q@|$(Rwz#*ofXKpi4~y4(Socl&D3J~a53F0k*;oqdNg zt}R{tg<0@^e*QGMLkd$5Mb8^#{0k7|t;~fD>v#AEh0FbQ-wI8K=Hu zn3TkrQ3VPT;T6CB`13HF2daGFHFf*{BMcvS7}muwWbflyRmM3*geSpwmHmHV<^#qJ z+*>#N0x=jYn0Gj6->y$$s9-Y)@?LGPDRn?fzga7eN!6A4LJ3bo%e8G_a}pMag+o+1 ztX^$z$gpP<zqF!w_5NQ+ void: _ghost.visible = true +## Raster-Snap (wenn in main angeschaltet): X/Z auf ganze Meter rasten — +## Objekte passen dann nebeneinander wie Bauklötze. Höhe bleibt Terrain. +func _apply_grid(pos: Vector3) -> Vector3: + if not bool(main.get("grid_snap")): + return pos + pos.x = ObjectOps.snap_coord(pos.x, true, GRID_M) + pos.z = ObjectOps.snap_coord(pos.z, true, GRID_M) + return pos + + func _ghost_update(viewport_pos: Vector2) -> void: var p: Variant = _ground_point(viewport_pos) if p == null: @@ -155,7 +166,7 @@ func _ghost_update(viewport_pos: Vector2) -> void: _ghost.visible = false return _ghost_active = true - var pos := p as Vector3 + var pos := _apply_grid(p as Vector3) var half := _terrain_half() pos.x = clampf(pos.x, -half.x, half.x) pos.z = clampf(pos.z, -half.y, half.y) @@ -169,7 +180,7 @@ func _place_object(viewport_pos: Vector2) -> void: if p == null: main.call("_set_status", "Da war kein Boden unter dem Mauszeiger — klicke IN die Karte (die grüne Fläche).", true) return - var pos := p as Vector3 + var pos: Vector3 = _apply_grid(p as Vector3) var half := _terrain_half() if absf(pos.x) > half.x or absf(pos.z) > half.y: main.call("_set_status", "Das wäre außerhalb der Map gelandet — klicke innerhalb der grünen Fläche.", true) @@ -274,7 +285,7 @@ func _unhandled_input(event: InputEvent) -> void: var p: Variant = _ground_point(mm.position) if p == null: return - var new_pos: Vector3 = (p as Vector3) + _grab_offset + var new_pos: Vector3 = _apply_grid((p as Vector3) + _grab_offset) var half := _terrain_half() new_pos.x = clampf(new_pos.x, -half.x, half.x) new_pos.z = clampf(new_pos.z, -half.y, half.y) diff --git a/src/editor/library_browser.gd b/src/editor/library_browser.gd new file mode 100644 index 0000000..3da8372 --- /dev/null +++ b/src/editor/library_browser.gd @@ -0,0 +1,94 @@ +extends RefCounted +class_name LibraryBrowser +## Objekt-Bibliothek des Editors (Karte t_3a998ae4): deutscher Name, Gruppen +## für den Bibliothek-Browser und Thumbnail-Laden (128-px-PNGs, einmalig per +## Headless-Render erzeugt — tools/gen_thumbs.gd). +## +## Bewusst OHNE UI: reine Daten/Funktionen, damit die Tests (tests/test_library.gd) +## headless alles prüfen können. Die IDs sind der Vertrag (Entscheidung 20) — +## Gruppen/Namen leben HIER im Editor (die objects.json bleibt 1:1 die Kopie +## der Server-Registry, damit ein Sync sie nicht verwüstet). + +## Deutsche Anzeige-Namen für Bibliotheks-IDs (IDs sind der Vertrag, Entscheidung 20) +const NAMES := { + "crate_wood": "Holzkiste", + "crate_metal": "Metallkiste", + "wall_concrete": "Betonmauer", + "barrier_sandbag": "Sandsack-Sperre", + "bunker_concrete": "Bunker", + "watchtower_wood": "Wachturm", + "fence_metal": "Zaun", + "rock_large": "Großer Felsen", + "boulder_small": "Kleiner Felsen", + "tree_pine": "Nadelbaum", + "bush_small": "Strauch", +} + +## Gruppen für den Bibliothek-Browser (Anzeige-Reihenfolge = Reihenfolge hier). +const GROUP_ORDER := ["kisten", "bauwerke", "natur", "sonstiges"] +const GROUP_LABELS := { + "kisten": "Kisten & Barrieren", + "bauwerke": "Bauwerke", + "natur": "Natur", + "sonstiges": "Sonstiges", +} +const GROUP_IDS := { + "kisten": ["crate_wood", "crate_metal", "barrier_sandbag", "wall_concrete"], + "bauwerke": ["bunker_concrete", "watchtower_wood", "fence_metal"], + "natur": ["tree_pine", "bush_small", "rock_large", "boulder_small"], +} + + +static func object_label(oid: String) -> String: + return NAMES.get(oid, oid.capitalize()) + + +## Gruppe einer ID (Objekte, die keine festen Gruppe haben → "sonstiges"). +static func group_of(oid: String) -> String: + for key in GROUP_IDS: + if (GROUP_IDS[key] as Array).has(oid): + return str(key) + return "sonstiges" + + +## Gruppen für den Browser, geprüft gegen das Manifest: nur IDs, die es in +## der objects.json WIRKLICH gibt, landen in der Liste (und unbekannte, +## noch nicht gruppierte IDs wandern nach "Sonstiges" — neue Bibliotheks- +## Einträge brechen die UI also nicht). +static func groups_in_order(manifest: Dictionary) -> Array: + var lib: Dictionary = manifest.get("objects", {}) + var by_group := {} + for oid in lib.keys(): + var key := group_of(str(oid)) + if not by_group.has(key): + by_group[key] = [] + (by_group[key] as Array).append(str(oid)) + var out: Array = [] + for key in GROUP_ORDER: + var ids: Array = (by_group.get(key, []) as Array) + ids.sort() + if ids.is_empty() and key != "sonstiges": + continue + out.append({"key": key, "label": GROUP_LABELS[key], "ids": ids}) + return out + + +## Thumbnail-Pfad (128-px-PNG, tools/gen_thumbs.gd erzeugt sie einmalig). +static func thumb_path(oid: String) -> String: + return "res://library/thumbs/%s.png" % oid + + +## Thumbnail als Textur laden — direkt über Image (kein Import-System nötig, +## geht damit auch frisch nach git pull ohne Import), auf Anzeige-Größe +## runtergerechnet. null = noch kein Thumbnail → Button fällt aufs Text-Layout +## zurück (Fehler sieht man nicht, man sieht einfach nur kein Bild). +static func load_thumb(oid: String, size_px := 40) -> Texture2D: + var path := thumb_path(oid) + if not FileAccess.file_exists(path): + return null + var img := Image.load_from_file(path) + if img == null: + return null + if img.get_width() != size_px or img.get_height() != size_px: + img.resize(size_px, size_px, Image.INTERPOLATE_LANCZOS) + return ImageTexture.create_from_image(img) diff --git a/src/editor/library_browser.gd.uid b/src/editor/library_browser.gd.uid new file mode 100644 index 0000000..d4c7d10 --- /dev/null +++ b/src/editor/library_browser.gd.uid @@ -0,0 +1 @@ +uid://o3jwxk87gtw diff --git a/src/editor/object_ops.gd b/src/editor/object_ops.gd new file mode 100644 index 0000000..7580b46 --- /dev/null +++ b/src/editor/object_ops.gd @@ -0,0 +1,34 @@ +extends RefCounted +class_name ObjectOps +## Objekt-Operationen des Editors (Karte t_3a998ae4) — bewusst pure Funktionen +## (kein UI, kein Knoten): Snap-to-Ground und Raster-Snap. main.gd/interaction.gd +## rufen auf, tests/test_library.gd prüft headless. + +const GRID_M := 1.0 # Raster-Snap-Weite (1 m — handhabbar für Objekte) +const FLOAT_EPSILON := 0.01 # ab dieser Abweichung gilt ein Objekt als "schwebend" + + +## ALLE Objekte auf die Terrain-Höhe setzen (Snap to Ground). Objekte hängen +## z. B. nach dem Terrain-Formen in der Luft — ein Klick holt sie runter. +## Rückgabe: Anzahl geänderter Objekte (0 = alles stand schon auf dem Boden). +static func snap_to_ground(doc: Dictionary) -> int: + var changed := 0 + var objects: Array = doc.get("objects", []) + for obj in objects: + if not (obj is Dictionary): + continue + var pos: Array = obj.get("pos", [0.0, 0.0, 0.0]) + if (pos as Array).size() != 3: + continue + var h := SvmapDocument.terrain_height_at(doc, float(pos[0]), float(pos[2])) + if absf(float(pos[1]) - h) > FLOAT_EPSILON: + obj["pos"] = [float(pos[0]), h, float(pos[2])] + changed += 1 + return changed + + +## Eine Koordinate aufs Raster legen (Raster-Snap). grid_m <= 0 oder aus = no-op. +static func snap_coord(value: float, enabled: bool, grid_m := GRID_M) -> float: + if not enabled or grid_m <= 0.0: + return value + return roundf(value / grid_m) * grid_m diff --git a/src/editor/object_ops.gd.uid b/src/editor/object_ops.gd.uid new file mode 100644 index 0000000..a8fb101 --- /dev/null +++ b/src/editor/object_ops.gd.uid @@ -0,0 +1 @@ +uid://dnocqoteya1h3 diff --git a/src/ui/main.gd b/src/ui/main.gd index 9c02524..aac2284 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -17,25 +17,11 @@ extends Control const APP_TITLE := "ScopedVR Map-Editor" -## Deutsche Anzeige-Namen für Bibliotheks-IDs (IDs sind der Vertrag, Entscheidung 20) -const GERMAN_NAMES := { - "crate_wood": "Holzkiste", - "crate_metal": "Metallkiste", - "wall_concrete": "Betonmauer", - "barrier_sandbag": "Sandsack-Sperre", - "bunker_concrete": "Bunker", - "watchtower_wood": "Wachturm", - "fence_metal": "Zaun", - "rock_large": "Großer Felsen", - "boulder_small": "Kleiner Felsen", - "tree_pine": "Nadelbaum", - "bush_small": "Strauch", -} - var manifest: Dictionary = {} var doc: Dictionary = {} # das Map-Dictionary (1:1 svmap-Format) var current_path := "" # leer = noch nie gespeichert var dirty := false +var grid_snap := false # Raster-Snap (1 m) an/aus — Objekt-Werkzeuge links var history := ActionHistory.new() var interaction # src/editor/interaction.gd (script-erweitert → untypisiert) @@ -242,15 +228,15 @@ func _build_left_panel() -> PanelContainer: var panel := PanelContainer.new() panel.custom_minimum_size = Vector2(280, 0) var vbox := VBoxContainer.new() - vbox.add_theme_constant_override("separation", 8) + vbox.add_theme_constant_override("separation", 6) panel.add_child(vbox) vbox.add_child(_header_label("Objekt-Bibliothek")) - vbox.add_child(_muted_label("Objekt anklicken, dann in die Karte klicken.\nDer Platzier-Modus bleibt an, bis du Rechtsklick/ESC drückst — so setzt du mehrere hintereinander.")) + vbox.add_child(_muted_label("Objekt anklicken, dann in die Karte klicken — der Modus bleibt an bis Rechtsklick/ESC.")) var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - scroll.custom_minimum_size = Vector2(0, 160) + scroll.custom_minimum_size = Vector2(0, 280) scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED vbox.add_child(scroll) library_box = VBoxContainer.new() @@ -259,12 +245,30 @@ func _build_left_panel() -> PanelContainer: scroll.add_child(library_box) _fill_library() + vbox.add_child(_separator()) + vbox.add_child(_header_label("Objekt-Werkzeuge")) + + var snap_btn := Button.new() + snap_btn.text = "⚡ Alles auf den Boden setzen" + snap_btn.custom_minimum_size = Vector2(0, 44) + snap_btn.tooltip_text = "Setzt ALLE Objekte auf die Terrain-Höhe — z. B. nach dem Terrain-Formen, wenn etwas in der Luft hängt" + snap_btn.pressed.connect(_on_snap_to_ground) + vbox.add_child(snap_btn) + + var grid_check := CheckButton.new() + grid_check.text = "Raster-Snap (1 m)" + grid_check.custom_minimum_size = Vector2(0, 40) + grid_check.tooltip_text = "Neue und verschobene Objekte rasten auf ein 1-Meter-Raster ein" + grid_check.toggled.connect(_on_grid_snap_changed) + vbox.add_child(grid_check) + vbox.add_child(_muted_label("Raster-Snap rastet Platzieren und Ziehen auf ganze Meter.")) + vbox.add_child(_separator()) vbox.add_child(_header_label("Map-Inhalt")) content_tree = Tree.new() content_tree.size_flags_vertical = Control.SIZE_EXPAND_FILL - content_tree.custom_minimum_size = Vector2(0, 200) + content_tree.custom_minimum_size = Vector2(0, 160) content_tree.allow_reselect = true content_tree.hide_root = true content_tree.item_selected.connect(_on_tree_selected) @@ -274,19 +278,44 @@ func _build_left_panel() -> PanelContainer: func _fill_library() -> void: library_buttons.clear() + # Gruppen-Browser (Karte t_3a998ae4): Klapp-Überschriften je Gruppe, + # darunter die Objekte mit Thumbnail (128-px-Render, runterskaliert). + for group in LibraryBrowser.groups_in_order(manifest): + var g_label: String = group["label"] + var g_ids: Array = group["ids"] + if g_ids.is_empty(): + continue + var header := Button.new() + header.text = "▾ %s (%d)" % [g_label, g_ids.size()] + header.toggle_mode = true + header.button_pressed = true + header.alignment = HORIZONTAL_ALIGNMENT_LEFT + header.custom_minimum_size = Vector2(240, 40) + library_box.add_child(header) + var group_box := VBoxContainer.new() + group_box.add_theme_constant_override("separation", 4) + library_box.add_child(group_box) + header.toggled.connect(func(on: bool) -> void: + group_box.visible = on + header.text = "%s %s (%d)" % ["▾" if on else "▸", g_label, g_ids.size()]) + for oid in g_ids: + _add_library_button(str(oid), group_box) + + +func _add_library_button(oid: String, parent: Container) -> void: var lib: Dictionary = manifest.get("objects", {}) - for oid in lib.keys(): - var entry: Dictionary = lib[oid] - var poly := int(entry.get("poly", 0)) - var btn := Button.new() - btn.text = "%s · %d Polys" % [object_label(str(oid)), poly] - btn.custom_minimum_size = Vector2(240, 40) - btn.alignment = HORIZONTAL_ALIGNMENT_LEFT - btn.toggle_mode = true - btn.button_pressed = false - btn.pressed.connect(_on_library_pressed.bind(str(oid))) - library_box.add_child(btn) - library_buttons[str(oid)] = btn + var entry: Dictionary = lib.get(oid, {}) + var poly := int(entry.get("poly", 0)) + var btn := Button.new() + btn.text = "%s · %d Polys" % [LibraryBrowser.object_label(oid), poly] + btn.icon = LibraryBrowser.load_thumb(oid) + btn.custom_minimum_size = Vector2(240, 44) + btn.alignment = HORIZONTAL_ALIGNMENT_LEFT + btn.toggle_mode = true + btn.button_pressed = false + btn.pressed.connect(_on_library_pressed.bind(oid)) + parent.add_child(btn) + library_buttons[oid] = btn func _build_center_viewport() -> SubViewportContainer: @@ -701,7 +730,32 @@ func _highlight_library(oid: String) -> void: func object_label(oid: String) -> String: - return GERMAN_NAMES.get(oid, oid.capitalize()) + return LibraryBrowser.object_label(oid) + + +## Snap to Ground (1 Klick): ALLE Objekte auf die Terrain-Höhe setzen — +## EIN Undo-Eintrag für die ganze Aktion. +func _on_snap_to_ground() -> void: + var objects: Array = doc.get("objects", []) + if objects.is_empty(): + _set_status("Es gibt noch keine Objekte — platziere zuerst eins aus der Bibliothek links.") + return + var before: Dictionary = history.snapshot(["objects"]) + var changed := ObjectOps.snap_to_ground(doc) + if changed == 0: + _set_status("Alle %d Objekte stehen schon auf dem Boden — nichts zu tun." % objects.size()) + return + history.commit("Alle Objekte auf Boden gesetzt (%d)" % changed, + before, history.snapshot(["objects"])) + _after_structure_change() + _set_status("%d von %d Objekten auf den Boden gesetzt — Strg+Z nimmt es zurück." % [changed, objects.size()]) + + +func _on_grid_snap_changed(pressed: bool) -> void: + grid_snap = pressed + _set_status("Raster-Snap %s — Objekte landen jetzt %s." % [ + "AN" if pressed else "AUS", + "auf ganzen Metern (wie Bauklötze nebeneinander)" if pressed else "dort, wo du hinklickst"]) # ---------------------------------------------------- Werkzeug-Status ------ @@ -1052,6 +1106,9 @@ func _maybe_start_demo_mode() -> void: if args.has("--terrain-demo"): _start_terrain_demo(args) return + if args.has("--library-demo"): + _start_library_demo(args) + return if not args.has("--ghost-demo"): return var oid := "crate_wood" @@ -1164,4 +1221,54 @@ func _demo_shot(path: String) -> void: if mi != null and is_instance_valid(mi) and mi.mesh != null: var aabb: AABB = mi.mesh.get_aabb() print("DEMO_AABB: pos=%s size=%s tris=%d" % [aabb.position, aabb.size, tv.triangle_count()]) - print("DEMO_CAM: cam=%s target=%s" % [camera.global_position, camera.target]) \ No newline at end of file + print("DEMO_CAM: cam=%s target=%s" % [camera.global_position, camera.target]) + + +## `godot ... -- --library-demo [ordner]` — Desktop-Smoke (Karte t_3a998ae4): +## Gruppen-Bibliothek + platzierte Objekte + Snap-to-Ground programmatisch +## durchspielen, PNGs speichern. Die App bleibt offen. +func _start_library_demo(args: PackedStringArray) -> void: + var out_dir := "/tmp/scopedvr_library" + var idx := args.find("--library-demo") + if idx >= 0 and idx + 1 < args.size() and not args[idx + 1].begins_with("--"): + out_dir = args[idx + 1] + DirAccess.make_dir_recursive_absolute(out_dir) + + # 1) Bibliothek-Browser: Gruppen sichtbar, Ghost-Preview aktiv. + _set_status("Demo 1/3: Bibliothek in Gruppen — ein Objekt anklicken setzt eine Vorschau in die Karte.") + interaction.demo_park_ghost(Vector3(10.0, SvmapDocument.terrain_height_at(doc, 10.0, 6.0), 6.0), "watchtower_wood") + _demo_camera(Vector3(0.0, 2.0, 0.0), 90.0, 35.0, 40.0) + await _demo_shot(out_dir + "/library_browser.png") + + # 2) Objekte platziert (versch. Gruppen), eins orange ausgewählt. + var before: Dictionary = history.snapshot(["objects"]) + (doc["objects"] as Array).append({"id": "watchtower_wood", "pos": [10.0, 0.0, 6.0], "scale": 1.0, "rot_y_deg": 30.0}) + (doc["objects"] as Array).append({"id": "crate_wood", "pos": [-8.0, 0.0, -4.0], "scale": 1.0, "rot_y_deg": 0.0}) + (doc["objects"] as Array).append({"id": "tree_pine", "pos": [-20.0, 0.0, 18.0], "scale": 1.0, "rot_y_deg": 75.0}) + (doc["objects"] as Array).append({"id": "rock_large", "pos": [24.0, 0.0, -18.0], "scale": 1.0, "rot_y_deg": 0.0}) + history.commit("Demo: Objekte gesetzt", before, history.snapshot(["objects"])) + interaction.call("cancel_placement") + _clear_edit_state() + _refresh_all() + interaction.select_object(0, true) + _demo_camera(Vector3(0.0, 2.0, 0.0), 110.0, 40.0, -35.0) + _set_status("Demo 2/3: Objekte platziert (Wachturm orange ausgewählt) — ziehen, drehen, löschen wie gehabt.") + await _demo_shot(out_dir + "/library_placed.png") + + # 3) Snap to Ground: Objekte künstlich in die Luft hängen, dann 1 Klick. + var float_before: Dictionary = history.snapshot(["objects"]) + for obj in (doc["objects"] as Array): + var pos: Array = obj["pos"] + obj["pos"] = [float(pos[0]), float(pos[1]) + 7.0, float(pos[2])] + history.commit("Demo: Objekte schweben", float_before, history.snapshot(["objects"])) + _refresh_all() + _demo_camera(Vector3(0.0, 6.0, 0.0), 110.0, 30.0, -35.0) + _set_status("Demo 3/3a: Objekte SCHWEBEN nach dem Terrain-Formen — ein Klick auf „Alles auf den Boden setzen\" …") + await _demo_shot(out_dir + "/library_floating.png") + _on_snap_to_ground() + _demo_camera(Vector3(0.0, 4.0, 0.0), 110.0, 38.0, -35.0) + _set_status("Demo 3/3b: … und schon stehen ALLE wieder auf dem Boden (Strg+Z nimmt den ganzen Klick zurück).") + await _demo_shot(out_dir + "/library_after_snap.png") + print("LIBRARY_DEMO_OK: 4 Screenshots in %s" % out_dir) + if args.has("--library-demo-quit"): + get_tree().quit() # Batch-Smoke (xvfb): automatisch beenden, sobald die Shots da sind \ No newline at end of file diff --git a/tests/test_library.gd b/tests/test_library.gd new file mode 100644 index 0000000..1203d1a --- /dev/null +++ b/tests/test_library.gd @@ -0,0 +1,307 @@ +extends SceneTree +## Editor-Tests (headless): Karte t_3a998ae4 — Objekt-Bibliothek, Snap-to-Ground, +## Raster-Snap, svmap-Roundtrip mit Objekten. +## Ausführen: godot --headless --path . --script res://tests/test_library.gd +## Exit 0 = alles grün. Läuft OHNE UI (LibraryBrowser/ObjectOps/SvmapIO/SvmapDocument). +## UI-Funktionen (Gruppen-Aufklappen, Thumbnail-Anzeige) werden im Desktop-Smoke +## verifiziert (--library-demo), nicht hier. + +var failures := 0 +var checks := 0 + + +func _init() -> void: + _run_tests() + if failures == 0: + print("ALL OK: %d checks" % checks) + quit(0) + else: + print("FAILED: %d/%d checks" % [failures, checks]) + quit(1) + + +func _check(cond: bool, label: String) -> void: + checks += 1 + if cond: + print(" ok: %s" % label) + else: + failures += 1 + print(" FAIL: %s" % label) + + +func _run_tests() -> void: + _test_manifest_load() + _test_groups() + _test_thumbnails() + _test_grid_snap() + _test_snap_to_ground() + _test_snap_to_ground_undo() + _test_crud_undo_all_objects() + _test_svmap_roundtrip_objects() + + +# ------------------------------------------------------- Bibliothek laden ---- + +func _load_manifest() -> Dictionary: + var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(SvmapIO.MANIFEST_PATH)) + return parsed if parsed is Dictionary else {} + + +func _test_manifest_load() -> void: + print("Testgruppe: Bibliothek laden (library/objects.json)") + var manifest := _load_manifest() + _check(not manifest.is_empty(), "Manifest lädt und ist ein Objekt") + var lib: Dictionary = manifest.get("objects", {}) + _check(lib.size() == 11, "Katalog hat die 11 etablierten Objekte (ist %d)" % lib.size()) + var all_ok := true + for oid in lib.keys(): + var entry: Dictionary = lib[oid] + if int(entry.get("poly", 0)) <= 0: + all_ok = false + var col: Dictionary = entry.get("collision", {}) + if str(col.get("shape", "")).is_empty(): + all_ok = false + _check(all_ok, "jedes Objekt hat poly > 0 + Kollider-Eintrag") + # Deutsche Namen decken ALLE IDs ab (Bibliothek-Browser zeigt nichts Englisches). + var names_ok := lib.keys().all(func(oid) -> bool: return LibraryBrowser.NAMES.has(oid)) + _check(names_ok, "deutsche Namen für alle IDs vorhanden") + _check(LibraryBrowser.object_label("crate_wood") == "Holzkiste", "object_label: crate_wood → Holzkiste") + _check(LibraryBrowser.object_label("gibts_nicht") == "Gibts Nicht", "object_label: unbekannte ID fällt auf capitalize zurück") + + +func _test_groups() -> void: + print("Testgruppe: Gruppen (Gruppe/Einzelobjekt im Browser)") + var manifest := _load_manifest() + var lib: Dictionary = manifest.get("objects", {}) + var groups := LibraryBrowser.groups_in_order(manifest) + var seen := {} + var all_in := true + for group in groups: + _check(not str(group["label"]).is_empty(), "Gruppe „%s“ hat Label" % str(group["key"])) + for oid in (group["ids"] as Array): + if seen.has(oid): + all_in = false + seen[oid] = true + _check(all_in, "keine ID in zwei Gruppen") + _check(seen.size() == lib.size(), "alle %d IDs landen in einer Gruppe (ist %d)" % [lib.size(), seen.size()]) + _check(LibraryBrowser.group_of("tree_pine") == "natur", "group_of: tree_pine → natur") + _check(LibraryBrowser.group_of("crate_wood") == "kisten", "group_of: crate_wood → kisten") + _check(LibraryBrowser.group_of("fremdes_ding") == "sonstiges", "group_of: unbekannte ID → sonstiges") + # Gruppen, die im Katalog leer sind, werden nicht angezeigt (außer Sonstiges). + var labels: Array = [] + for group in groups: + labels.append(str(group["label"])) + var no_empty := true + for group in groups: + if str(group["key"]) != "sonstiges" and (group["ids"] as Array).is_empty(): + no_empty = false + _check(no_empty, "leere feste Gruppen tauchen nicht auf") + + +func _test_thumbnails() -> void: + print("Testgruppe: Thumbnails (128 px, einmalig gerendert + committed)") + var manifest := _load_manifest() + var lib: Dictionary = manifest.get("objects", {}) + var missing: Array = [] + var wrong_size := false + for oid in lib.keys(): + var path := LibraryBrowser.thumb_path(str(oid)) + if not FileAccess.file_exists(path): + missing.append(str(oid)) + continue + var img := Image.load_from_file(path) + if img == null or img.get_width() != 128 or img.get_height() != 128: + wrong_size = true + _check(missing.is_empty(), "Thumbnail für jedes Objekt vorhanden (fehlend: %s)" % str(missing)) + _check(not wrong_size, "alle Thumbnails sind 128×128") + var tex := LibraryBrowser.load_thumb("crate_wood", 40) + _check(tex != null, "load_thumb liefert Textur") + var fallback := LibraryBrowser.load_thumb("gibts_nicht") + _check(fallback == null, "load_thumb: fehlendes Thumbnail → null (Button fällt aufs Text-Layout zurück)") + + +# ------------------------------------------------------------ Raster-Snap ---- + +func _test_grid_snap() -> void: + print("Testgruppe: Raster-Snap (1 m)") + _check(ObjectOps.snap_coord(10.4, true) == 10.0, "10.4 rastet auf 10") + _check(ObjectOps.snap_coord(10.6, true) == 11.0, "10.6 rastet auf 11") + _check(ObjectOps.snap_coord(-0.6, true) == -1.0, "-0.6 rastet auf -1") + _check(ObjectOps.snap_coord(-0.4, true) == 0.0, "-0.4 rastet auf 0") + _check(ObjectOps.snap_coord(10.4, false) == 10.4, "aus = unverändert") + _check(ObjectOps.snap_coord(10.4, true, 4.0) == 12.0, "4-m-Raster: 10.4 → 12") + + +# ---------------------------------------------------------- Snap to Ground --- + +## Alle Heightmap-Samples auf einen 16-bit-Wert setzen → Terrain überall +## gleich hoch (h = min + v/65535 × Spannweite). Für Snap-Tests ohne +## Terrain-Werkzeug. uint16 little-endian (Low-Byte zuerst, map_format.md). +func _flatten_terrain(doc: Dictionary, v: int) -> void: + var terrain: Dictionary = doc["terrain"] + var res := int(terrain["resolution"]) + var bytes := PackedByteArray() + bytes.resize(res * res * 2) + for i in res * res: + bytes[i * 2] = v & 0xFF + bytes[i * 2 + 1] = (v >> 8) & 0xFF + terrain["heightmap_r16_b64"] = Marshalls.raw_to_base64(bytes) + + +func _test_snap_to_ground() -> void: + print("Testgruppe: Snap to Ground (alle schwebenden Objekte auf Terrain-Höhe)") + var doc := SvmapDocument.create_new("Snap-Test", 1024.0, "mittag") + (doc["objects"] as Array).append({"id": "crate_wood", "pos": [10.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0}) + (doc["objects"] as Array).append({"id": "tree_pine", "pos": [-5.0, 0.0, 3.0], "scale": 1.0, "rot_y_deg": 45.0}) + _check(ObjectOps.snap_to_ground(doc) == 0, "flaches Terrain: alles steht schon (0 Änderungen)") + _flatten_terrain(doc, 65535) # Terrain überall auf max_height (50 m) + var h := SvmapDocument.terrain_height_at(doc, 10.0, 10.0) + _check(absf(h - 50.0) < 0.01, "Terrain-Höhe nach Heben: %.1f m" % h) + var changed := ObjectOps.snap_to_ground(doc) + _check(changed == 2, "2 schwebende Objekte erkannt") + var objects: Array = doc["objects"] + _check(absf(float(objects[0]["pos"][1]) - 50.0) < 0.01, "Objekt 1 steht auf dem Boden") + _check(absf(float(objects[1]["pos"][1]) - 50.0) < 0.01, "Objekt 2 steht auf dem Boden") + _check(ObjectOps.snap_to_ground(doc) == 0, "zweiter Klick: nichts mehr zu tun") + # Teilweise schwebend: nur das schwebende wird erfasst. + objects[0]["pos"][1] = 50.0 + objects[1]["pos"][1] = 60.0 + _check(ObjectOps.snap_to_ground(doc) == 1, "nur das schwebende Objekt wird gesnapt") + + +func _test_snap_to_ground_undo() -> void: + print("Testgruppe: Snap to Ground = EIN Undo-Eintrag") + var doc := SvmapDocument.create_new("Snap-Undo", 1024.0, "mittag") + var hist := ActionHistory.new() + hist.begin(doc) + (doc["objects"] as Array).append({"id": "bunker_concrete", "pos": [0.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0}) + (doc["objects"] as Array).append({"id": "fence_metal", "pos": [4.0, 0.0, -2.0], "scale": 1.0, "rot_y_deg": 90.0}) + _flatten_terrain(doc, 32768) # Terrain auf ~25 m + var before: Dictionary = hist.snapshot(["objects"]) + var changed := ObjectOps.snap_to_ground(doc) + hist.commit("Alle Objekte auf Boden gesetzt (%d)" % changed, before, hist.snapshot(["objects"])) + var objects: Array = doc["objects"] + _check(changed == 2, "beide Objekte gesnapt") + _check(absf(float(objects[0]["pos"][1]) - 25.0) < 0.05, "Objekt 1 auf Terrain-Höhe") + hist.undo() + _check(absf(float(objects[0]["pos"][1]) - 0.0) < 0.0001, "undo: beide wieder schwebend (1 Schritt)") + _check(absf(float(objects[1]["pos"][1]) - 0.0) < 0.0001, "undo: Objekt 2 auch") + hist.redo() + _check(absf(float(objects[0]["pos"][1]) - 25.0) < 0.05 and absf(float(objects[1]["pos"][1]) - 25.0) < 0.05, "redo: wieder auf dem Boden") + + +# ------------------------------------------- setzen/drehen/verschieben/löschen — + +func _test_crud_undo_all_objects() -> void: + print("Testgruppe: JEDES Bibliotheks-Objekt — setzen/drehen/verschieben/löschen + je Undo") + var manifest := _load_manifest() + var lib: Dictionary = manifest.get("objects", {}) + var doc := SvmapDocument.create_new("CRUD-Test", 1024.0, "mittag") + var hist := ActionHistory.new() + hist.begin(doc) + var all_ok := true + var first := true + for oid in lib.keys(): + var x := randf() * 100.0 - 50.0 + var z := randf() * 100.0 - 50.0 + var y := SvmapDocument.terrain_height_at(doc, x, z) + # setzen (+ Undo) + var before := hist.snapshot(["objects"]) + var obj := {"id": oid, "pos": [x, y, z], "scale": 1.0, "rot_y_deg": 0.0} + (doc["objects"] as Array).append(obj) + hist.commit("Test: %s platzieren" % oid, before, hist.snapshot(["objects"])) + var objects: Array = doc["objects"] + if objects.size() != 1 or absf(float(objects[0]["pos"][1]) - y) > 0.0001: + all_ok = false + hist.undo() + if not (doc["objects"] as Array).is_empty(): + all_ok = false + hist.redo() + if (doc["objects"] as Array).size() != 1: + all_ok = false + # drehen (+ Undo) + before = hist.snapshot(["objects"]) + objects[0]["rot_y_deg"] = 90.0 + hist.commit("Test: drehen", before, hist.snapshot(["objects"]), "rot:0") + hist.undo() + if absf(float(objects[0]["rot_y_deg"]) - 0.0) > 0.0001: + all_ok = false + hist.redo() + if absf(float(objects[0]["rot_y_deg"]) - 90.0) > 0.0001: + all_ok = false + # verschieben (+ Undo) + before = hist.snapshot(["objects"]) + objects[0]["pos"] = [x + 3.0, y, z + 3.0] + hist.commit("Test: verschieben", before, hist.snapshot(["objects"]), "move:0") + hist.undo() + if absf(float(objects[0]["pos"][0]) - x) > 0.0001: + all_ok = false + hist.redo() + if absf(float(objects[0]["pos"][0]) - (x + 3.0)) > 0.0001: + all_ok = false + # löschen (+ Undo) + before = hist.snapshot(["objects"]) + (doc["objects"] as Array).remove_at(0) + hist.commit("Test: löschen", before, hist.snapshot(["objects"])) + hist.undo() + if (doc["objects"] as Array).size() != 1: + all_ok = false + hist.redo() + if not (doc["objects"] as Array).is_empty(): + all_ok = false + if first: + first = false + _check(all_ok, "alle 11 Objekte: setzen/drehen/verschieben/löschen je mit Undo+Redo sauber") + + +# -------------------------------------------------------- svmap-Roundtrip ---- + +func _test_svmap_roundtrip_objects() -> void: + print("Testgruppe: svmap-Roundtrip — Objekte (ID + Pos/Rot/Scale) überleben Speichern/Laden") + var doc := SvmapDocument.create_new("Roundtrip", 1024.0, "abend") + _flatten_terrain(doc, 20000) # Terrain leicht erhöht (~15 m) + var objects: Array = doc["objects"] + objects.append({"id": "watchtower_wood", "pos": [10.0, SvmapDocument.terrain_height_at(doc, 10.0, 6.0), 6.0], "scale": 1.0, "rot_y_deg": 30.0}) + objects.append({"id": "crate_wood", "pos": [-8.0, SvmapDocument.terrain_height_at(doc, -8.0, -4.0), -4.0], "scale": 2.0, "rot_y_deg": 0.0}) + objects.append({"id": "tree_pine", "pos": [-20.0, SvmapDocument.terrain_height_at(doc, -20.0, 18.0), 18.0], "scale": 1.0, "rot_y_deg": 75.0}) + objects.append({"id": "rock_large", "pos": [24.0, SvmapDocument.terrain_height_at(doc, 24.0, -18.0), -18.0], "scale": 0.5, "rot_y_deg": 180.0}) + objects.append({"id": "bunker_concrete", "pos": [0.0, SvmapDocument.terrain_height_at(doc, 0.0, 40.0), 40.0], "scale": 1.0, "rot_y_deg": 270.0}) + var manifest := _load_manifest() + SvmapIO.sign_map(doc) # Prüfsumme setzen → validate_map (inkl. V13) prüfbar + _check(SvmapIO.validate_map(doc, manifest).is_empty(), "Map mit 5 Objekten validiert sauber (V1–V14)") + + var save1 := SvmapIO.save_to_file("user://test_library_roundtrip.svmap", doc) + _check(save1["ok"], "save_to_file ok: %s" % str(save1["errors"])) + var load1 := SvmapIO.load_from_file("user://test_library_roundtrip.svmap") + _check(load1["ok"], "load_from_file ok: %s" % str(load1["errors"])) + var doc2: Dictionary = load1["data"] + var objs2: Array = doc2["objects"] + var same := objs2.size() == objects.size() + if same: + for i in objects.size(): + var a: Dictionary = objects[i] + var b: Dictionary = objs2[i] + if str(a["id"]) != str(b["id"]): + same = false + for k in 3: + if absf(float(a["pos"][k]) - float(b["pos"][k])) > 0.0001: + same = false + if absf(float(a["rot_y_deg"]) - float(b["rot_y_deg"])) > 0.0001: + same = false + if absf(float(a["scale"]) - float(b["scale"])) > 0.0001: + same = false + _check(same, "alle Objekte identisch zurück (ID, Pos, Rot, Scale)") + _check(SvmapIO.validate_map(doc2, manifest).is_empty(), "geladene Map validiert erneut sauber") + var save2 := SvmapIO.save_to_file("user://test_library_roundtrip2.svmap", doc2) + _check(save2["ok"], "zweites Speichern ok") + var text1 := FileAccess.get_file_as_string("user://test_library_roundtrip.svmap") + var text2 := FileAccess.get_file_as_string("user://test_library_roundtrip2.svmap") + _check(text1 == text2, "save→load→save byte-identisch") + # Poly-Budget-Rechnung kommt aus dem Katalog (Σ poly × scale²). + var expected := 0.0 + for o in objects: + expected += float(manifest["objects"][str(o["id"])]["poly"]) * float(o["scale"]) * float(o["scale"]) + _check(absf(SvmapDocument.poly_usage(doc, manifest) - expected) < 0.01, + "Poly-Budget aus Katalog: %.0f (erwartet %.0f)" % [SvmapDocument.poly_usage(doc, manifest), expected]) + DirAccess.remove_absolute(ProjectSettings.globalize_path("user://test_library_roundtrip.svmap")) + DirAccess.remove_absolute(ProjectSettings.globalize_path("user://test_library_roundtrip2.svmap")) diff --git a/tests/test_library.gd.uid b/tests/test_library.gd.uid new file mode 100644 index 0000000..c259973 --- /dev/null +++ b/tests/test_library.gd.uid @@ -0,0 +1 @@ +uid://gcjtf6ekk4ub diff --git a/tools/gen_thumbs.gd b/tools/gen_thumbs.gd new file mode 100644 index 0000000..d32acc6 --- /dev/null +++ b/tools/gen_thumbs.gd @@ -0,0 +1,100 @@ +extends SceneTree +## Thumbnail-Generator für die Objekt-Bibliothek (Karte t_3a998ae4). +## Rendert JEDESM Bibliotheks-Objekt einmalig als 128-px-PNG nach +## library/thumbs/.png — die Bilder werden committed, der Editor lädt sie +## dann einfach aus der Datei (LibraryBrowser.load_thumb). +## +## Ausführen (braucht echtes Rendering, NICHT --headless): +## xvfb-run -a godot4 --rendering-driver opengl3 --path . --script res://tools/gen_thumbs.gd +## (auf Slys PC auch ohne xvfb: godot4 --rendering-driver opengl3 --path . --script ...) +## +## Exit 0 = alle Thumbnails geschrieben. + +const SIZE := 128 +const BG_COLOR := Color(0.16, 0.18, 0.21) + +var manifest: Dictionary = {} +var ids: Array = [] +var idx := -1 +var settle_frames := 0 +var vp: SubViewport +var mi: MeshInstance3D +var cam: Camera3D +var out_dir := "" + + +func _initialize() -> void: + var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(SvmapIO.MANIFEST_PATH)) + if parsed is Dictionary: + manifest = parsed + ids = (manifest.get("objects", {}) as Dictionary).keys() + if ids.is_empty(): + push_error("gen_thumbs: Bibliothek leer — nichts zu rendern.") + quit(1) + return + out_dir = ProjectSettings.globalize_path("res://library/thumbs") + DirAccess.make_dir_recursive_absolute(out_dir) + + vp = SubViewport.new() + vp.size = Vector2i(SIZE, SIZE) + vp.own_world_3d = true + vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS + root.add_child(vp) + + var env := Environment.new() + env.background_mode = Environment.BG_COLOR + env.background_color = BG_COLOR + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(1, 1, 1) + env.ambient_light_energy = 0.7 + var we := WorldEnvironment.new() + we.environment = env + vp.add_child(we) + + var sun := DirectionalLight3D.new() + sun.rotation_degrees = Vector3(-55.0, 35.0, 0.0) + sun.light_energy = 1.1 + vp.add_child(sun) + + mi = MeshInstance3D.new() + vp.add_child(mi) + cam = Camera3D.new() + cam.current = true + vp.add_child(cam) + idx = -1 # erst im ersten _process starten — look_at braucht eingetretene Knoten + + +func _next() -> void: + idx += 1 + if idx >= ids.size(): + print("GEN_THUMBS_OK: %d Thumbnails in %s" % [ids.size(), out_dir]) + quit(0) + return + var oid := str(ids[idx]) + var entry: Dictionary = (manifest["objects"] as Dictionary)[oid] + var col: Dictionary = entry.get("collision", {}) + var mesh := MeshShapes.make_placeholder(str(col.get("shape", "box")), col, 1.0) + mi.mesh = mesh + # Kamera diagonal aufs Objekt, passend zur Mesh-Größe (Kiste nah, Turm weit). + var aabb := mesh.get_aabb() + var center := aabb.get_center() + var radius := aabb.size.length() * 0.5 + var dir := Vector3(1.0, 0.65, 1.0).normalized() + cam.position = center + dir * (radius * 2.3 + 0.3) + cam.look_at(center, Vector3.UP) + settle_frames = 4 + + +func _process(_delta: float) -> bool: + if idx < 0: + _next() + return false + if settle_frames > 0: + settle_frames -= 1 + return false + var img := vp.get_texture().get_image() + var oid := str(ids[idx]) + img.save_png(out_dir + "/" + oid + ".png") + print(" thumb: %s (%dx%d)" % [oid, img.get_width(), img.get_height()]) + _next() + return false diff --git a/tools/gen_thumbs.gd.uid b/tools/gen_thumbs.gd.uid new file mode 100644 index 0000000..b984ca6 --- /dev/null +++ b/tools/gen_thumbs.gd.uid @@ -0,0 +1 @@ +uid://b37674pu4v4nc diff --git a/tools/parse_check.sh b/tools/parse_check.sh new file mode 100644 index 0000000..13edd0e --- /dev/null +++ b/tools/parse_check.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Parse-Check über alle geänderten/neuen Scripts (Karte t_3a998ae4) +cd "$(dirname "$0")/.." +GODOT=~/bin/godot4 +fail=0 +for f in src/ui/main.gd src/editor/library_browser.gd src/editor/object_ops.gd \ + src/editor/interaction.gd tools/gen_thumbs.gd tests/test_library.gd; do + out=$("$GODOT" --headless --path . --check-only --script "res://$f" 2>&1 | grep -iv "godot engine\|^$" || true) + if [ -n "$out" ]; then + echo "FAIL $f:"; echo "$out"; fail=1 + else + echo "OK $f" + fi +done +exit $fail diff --git a/tools/run_tests.sh b/tools/run_tests.sh new file mode 100644 index 0000000..40012e8 --- /dev/null +++ b/tools/run_tests.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Regression: alle Editor-Testsuiten (Karte t_3a998ae4) +cd "$(dirname "$0")/.." +GODOT=~/bin/godot4 +rc=0 +for t in test_ux test_editor_svmap test_terrain test_library; do + "$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1 + code=$? + line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1) + echo "$t: exit=$code $line" + [ $code -ne 0 ] && rc=1 +done +exit $rc