From bcbd4169606d0b634c1c687a7187e62c8b265a96 Mon Sep 17 00:00:00 2001 From: slyphax Date: Sun, 25 Oct 2026 22:47:15 +0100 Subject: [PATCH] =?UTF-8?q?Bauen:=20Geschoss-Wahl=20(Decken=20mitten=20im?= =?UTF-8?q?=20Raum),=201=20cm=20Wand-=C3=9Cberstand=20gegen=20Z-Fighting,?= =?UTF-8?q?=20Dach=20aus=20Einzelteilen,=20gr=C3=B6=C3=9Feres=20Einstellun?= =?UTF-8?q?gen-Fenster?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- library/kollisions/bau_dach_ecke_aussen.json | 1 + library/kollisions/bau_dach_ecke_innen.json | 1 + library/kollisions/bau_dach_giebel_ende.json | 1 + library/kollisions/bau_dach_plateau.json | 1 + library/kollisions/bau_dach_schraege.json | 1 + library/objects.json | 75 +++++++++++++ library/thumbs/bau_dach_ecke_aussen.png | Bin 0 -> 1083 bytes library/thumbs/bau_dach_ecke_innen.png | Bin 0 -> 1299 bytes library/thumbs/bau_dach_giebel_ende.png | Bin 0 -> 867 bytes library/thumbs/bau_dach_plateau.png | Bin 0 -> 1439 bytes library/thumbs/bau_dach_schraege.png | Bin 0 -> 1197 bytes src/editor/bau_katalog.gd | 106 ++++++++++++++++++- src/editor/build_snap.gd | 28 ++++- src/editor/interaction.gd | 63 +++++++++-- src/ui/main.gd | 12 ++- src/ui/panels/bauen_panel.gd | 19 +++- tests/test_bauen.gd | 90 +++++++++++++--- 17 files changed, 372 insertions(+), 26 deletions(-) create mode 100644 library/kollisions/bau_dach_ecke_aussen.json create mode 100644 library/kollisions/bau_dach_ecke_innen.json create mode 100644 library/kollisions/bau_dach_giebel_ende.json create mode 100644 library/kollisions/bau_dach_plateau.json create mode 100644 library/kollisions/bau_dach_schraege.json create mode 100644 library/thumbs/bau_dach_ecke_aussen.png create mode 100644 library/thumbs/bau_dach_ecke_innen.png create mode 100644 library/thumbs/bau_dach_giebel_ende.png create mode 100644 library/thumbs/bau_dach_plateau.png create mode 100644 library/thumbs/bau_dach_schraege.png diff --git a/library/kollisions/bau_dach_ecke_aussen.json b/library/kollisions/bau_dach_ecke_aussen.json new file mode 100644 index 0000000..29a04d4 --- /dev/null +++ b/library/kollisions/bau_dach_ecke_aussen.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,2.0,1.0],"aabb_min":[-1.0,0.0,-1.0],"dreiecke":6,"oid":"bau_dach_ecke_aussen","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_ecke_aussen.glb","vertices":[[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,2.0,1.0],[1.0,0.0,1.0],[-1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,0.0,-1.0],[-1.0,0.0,-1.0],[-1.0,0.0,1.0],[1.0,0.0,1.0]]} \ No newline at end of file diff --git a/library/kollisions/bau_dach_ecke_innen.json b/library/kollisions/bau_dach_ecke_innen.json new file mode 100644 index 0000000..0560d79 --- /dev/null +++ b/library/kollisions/bau_dach_ecke_innen.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,2.0,1.0],"aabb_min":[-1.0,0.0,-1.0],"dreiecke":10,"oid":"bau_dach_ecke_innen","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_ecke_innen.glb","vertices":[[-1.0,0.0,-1.0],[1.0,2.0,-1.0],[1.0,2.0,1.0],[-1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,2.0,1.0],[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[1.0,2.0,-1.0],[-1.0,0.0,-1.0],[-1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,2.0,1.0],[1.0,0.0,-1.0],[1.0,2.0,1.0],[1.0,2.0,-1.0],[-1.0,0.0,1.0],[1.0,2.0,1.0],[1.0,0.0,1.0],[-1.0,0.0,1.0],[-1.0,2.0,1.0],[1.0,2.0,1.0],[-1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,0.0,-1.0],[-1.0,0.0,-1.0],[-1.0,0.0,1.0],[1.0,0.0,1.0]]} \ No newline at end of file diff --git a/library/kollisions/bau_dach_giebel_ende.json b/library/kollisions/bau_dach_giebel_ende.json new file mode 100644 index 0000000..40da381 --- /dev/null +++ b/library/kollisions/bau_dach_giebel_ende.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,2.0,0.1],"aabb_min":[-1.0,0.0,-0.1],"dreiecke":8,"oid":"bau_dach_giebel_ende","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_giebel_ende.glb","vertices":[[-1.0,0.0,-0.1],[1.0,0.0,-0.1],[-1.0,2.0,-0.1],[-1.0,0.0,0.1],[-1.0,2.0,0.1],[1.0,0.0,0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[-1.0,0.0,-0.1],[-1.0,2.0,-0.1],[-1.0,2.0,0.1],[-1.0,0.0,-0.1],[-1.0,2.0,0.1],[-1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[-1.0,2.0,0.1],[1.0,0.0,-0.1],[-1.0,2.0,0.1],[-1.0,2.0,-0.1]]} \ No newline at end of file diff --git a/library/kollisions/bau_dach_plateau.json b/library/kollisions/bau_dach_plateau.json new file mode 100644 index 0000000..a37593b --- /dev/null +++ b/library/kollisions/bau_dach_plateau.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,2.0,1.0],"aabb_min":[-1.0,0.0,-1.0],"dreiecke":12,"oid":"bau_dach_plateau","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_plateau.glb","vertices":[[-1.0,2.0,1.0],[1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,2.0,1.0],[1.0,0.0,1.0],[-1.0,0.0,1.0],[1.0,2.0,-1.0],[-1.0,2.0,-1.0],[1.0,0.0,-1.0],[-1.0,2.0,-1.0],[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[1.0,2.0,1.0],[1.0,2.0,-1.0],[1.0,0.0,1.0],[1.0,2.0,-1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[-1.0,2.0,-1.0],[-1.0,2.0,1.0],[-1.0,0.0,-1.0],[-1.0,2.0,1.0],[-1.0,0.0,1.0],[-1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,2.0,1.0],[1.0,2.0,-1.0],[-1.0,2.0,1.0],[-1.0,2.0,-1.0],[1.0,2.0,-1.0],[-1.0,0.0,1.0],[1.0,0.0,1.0],[-1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,0.0,-1.0],[-1.0,0.0,-1.0]]} \ No newline at end of file diff --git a/library/kollisions/bau_dach_schraege.json b/library/kollisions/bau_dach_schraege.json new file mode 100644 index 0000000..38bddcf --- /dev/null +++ b/library/kollisions/bau_dach_schraege.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,2.0,1.0],"aabb_min":[-1.0,0.0,-1.0],"dreiecke":8,"oid":"bau_dach_schraege","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_schraege.glb","vertices":[[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,2.0,1.0],[1.0,0.0,1.0],[-1.0,0.0,1.0],[-1.0,2.0,1.0],[1.0,2.0,1.0],[-1.0,0.0,-1.0],[-1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,2.0,1.0],[-1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,0.0,-1.0],[-1.0,0.0,-1.0],[-1.0,0.0,1.0],[1.0,0.0,1.0]]} \ No newline at end of file diff --git a/library/objects.json b/library/objects.json index 1d7cd4f..883fd84 100644 --- a/library/objects.json +++ b/library/objects.json @@ -3491,6 +3491,36 @@ "kategorie": "bauteile", "name_de": "Brüstung" }, + "bau_dach_ecke_aussen": { + "poly": 6, + "collision": { + "quelle": "kollisions/bau_dach_ecke_aussen.json", + "shape": "trimesh", + "size": [ + 2.0, + 2.0, + 2.0 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_dach_ecke_aussen.glb", + "kategorie": "bauteile", + "name_de": "Dachecke außen" + }, + "bau_dach_ecke_innen": { + "poly": 10, + "collision": { + "quelle": "kollisions/bau_dach_ecke_innen.json", + "shape": "trimesh", + "size": [ + 2.0, + 2.0, + 2.0 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_dach_ecke_innen.glb", + "kategorie": "bauteile", + "name_de": "Dachecke innen" + }, "bau_dach_giebel": { "poly": 48, "collision": { @@ -3536,6 +3566,36 @@ "kategorie": "bauteile", "name_de": "Satteldach 6×6 m" }, + "bau_dach_giebel_ende": { + "poly": 8, + "collision": { + "quelle": "kollisions/bau_dach_giebel_ende.json", + "shape": "trimesh", + "size": [ + 2.0, + 2.0, + 0.2 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_dach_giebel_ende.glb", + "kategorie": "bauteile", + "name_de": "Dachstirn 45°" + }, + "bau_dach_plateau": { + "poly": 12, + "collision": { + "quelle": "kollisions/bau_dach_plateau.json", + "shape": "trimesh", + "size": [ + 2.0, + 2.0, + 2.0 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_dach_plateau.glb", + "kategorie": "bauteile", + "name_de": "Dachplateau" + }, "bau_dach_pyramide": { "poly": 64, "collision": { @@ -3581,6 +3641,21 @@ "kategorie": "bauteile", "name_de": "Pyramidendach 6×6 m" }, + "bau_dach_schraege": { + "poly": 8, + "collision": { + "quelle": "kollisions/bau_dach_schraege.json", + "shape": "trimesh", + "size": [ + 2.0, + 2.0, + 2.0 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_dach_schraege.glb", + "kategorie": "bauteile", + "name_de": "Dachschräge 45°" + }, "bau_fundament": { "poly": 24, "collision": { diff --git a/library/thumbs/bau_dach_ecke_aussen.png b/library/thumbs/bau_dach_ecke_aussen.png new file mode 100644 index 0000000000000000000000000000000000000000..b9d320bf7a6f285b8a723a98834f04e26eedb3ea GIT binary patch literal 1083 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>VE*dq;uumf z=k4s%Ww#7OT>BN0H{JT@a%l}~$%X&_TUs11&3~a>BD0`*fkN`h6Z~1P=WSO1$gP_` zw|0HX1CerIOU-Mlg1p{JWg!JTP>GUJJf3{N~5ic}aZg&Jfy9r^}VIDmUY$@(3) z|H}L}b88K2SX%w}M%uK7paz2){?#lcOh*oVH}G;$bqM|;XU4gLMW8f;<;2Q5{$TM1 z0tw7D{C$Fr(v#A=W9x&Cq|G0L8TB9UmnD4{wZ6S}qf5&-Wqdywv0lToNr{EyusqB38|&Ub>lIum!l|ZzVEb%? zn(+Hl8@RX3{yBSnIfI-*El}7Yev;tVNX@kU!4mHleu?slVu=5%_pwZGIrkHzlCRZr zAC_O;@qhO0jrj{Yk8fvf_?)?*i@9Kjf!`;o@ShXgPw)5_@%Q|XD``&xXOysiu-T_O zDR#ljfVwSv_v!V_(^RZkAi&tLn5DB=HvY4G|8|c5PG5g#{+FEp`MAu?Bo>B-<9C&A zWZ!3Pi=1S-zez{4W`O}C!-JJd2Rm1<-)_8W|CSGpo%&3{6`c3t1?K9$U!=Y7^1(T- z$-Vs_9&_#4@u}&%$aO}ibGseiB|n^SchQp%jfYqn8kXP9O|Ad1a>2r1-u&_g3Jd|$ z84etXJ&^XpVPSTIL|oI?n%lYa`UN9@L~`CsUc}rIvdeM4o*FMmIe%-u6{1-oD!M<2|&rw literal 0 HcmV?d00001 diff --git a/library/thumbs/bau_dach_ecke_innen.png b/library/thumbs/bau_dach_ecke_innen.png new file mode 100644 index 0000000000000000000000000000000000000000..cd24ae4425b6b5112cf48c8d5c891626ada7ac4a GIT binary patch literal 1299 zcmchX`!}0+0LH)Xn`k7MNMm)YL|s~@lFC+E8kbrVhnAAEVMmk(2UVArs*+cWu`*6n zV{So5y0tFF8kZ2KN>y8SuppF;w$dW>XsiwC?0?wV56|;C=lS6|=lSKy^F77T!5QHI z06Lyb55FC9|CQ#h9e+p<8{Pqw_zcE7>B$a@cX%pROJk+CP=XFA>kzaVCiZjFVvx(x4c7Obr#s9p2X$TiBu%TkxYH zWEv>N&=7pN4gMF6T-%w!zJ(t5WjG_9kZW9d`H(#dXk&frjy)e@Wk>PM3^ve47ccak z+|4JDy`BaeVTVQ?mi#3pz&gTK`x}w&RyUgvH3wCNxBNzFaP*p@-y9k!u3WX1Jb zZqiF@Pf^MS_#nFNQs+&fXjYfjkbJ`$G7Sg>g-3@&Ms&(Yf`5YH0rX^1nx12wp;-5v zlM3R&5r0{TUzn*5GQ?TmLVk9rNcjSSX<0>1L$N8rK}wn0*!hIUWKpNYUVwboJ11@- z5UKmQ)!1>Wy)G%yIfe!#0&vBTG5Xq{EN9Bzg}N(I^%~6m?<+;>f>Ofs)e=j|_X+&7 zO4KV2X0LX{J=kJOm%oS1t=vSioNm^8LGW!#14q8)HZ^zQxac&^8;7sNrDau?g`Fff z`!p_R=7_DyY!=rb@>f_^+SxYI_F!Teldnf^$6#29?bxGt)Gu6ZWBgX1n40otrsAc9 zR(O19Jg9KAu5p9Kw=S&jyxBsaCHgcfsaxX_*8c649lm-YY1*TD4AV zpJ)va03%`gnlr_`7F_ao{c$51D*hh!f!|%V^L!JPGO0il%3&dk;dqVF71p)z45g{|j6i-TR*Sh;x!F{LPZ!`J?mcv|G{;si z#b*29l!BrPkZvC*ngcLH>h>HL;}$y(J5bz8iRl6=#QobTdp{}FG9oj&4|78o4o}6d zazdFOzr6E=XMJtJsB1n~zd7|myv?ICT${lO^2H#2YfyV3zfCaSW-L z^Y)Hyp0J}#+sAT2Sys&#Cs;4Fm}J%p7O~H7HNK(#km-j|$+RS;pxK9_((T{Om3*9G z`u}^Sia?9P+7JHkdl;AQ++BaCJLk8-cjgOw7!TMm%wT8u%#a|*aE7^|lEHwV;Te?# zSFvx1xs$bX$K2nBtJY>Q+!dE&nI3A|@b+}P=kLen0#m|G8wzVq9=-%rS6lh^Z_Umh zR+lGViM+}%Yp1+|Lnw1bGOM0G3xnsaf0e%VLcOcx4(v>B5IwQFisAUIk6aB3u^bFB zZ2I~v4S8!EB14%Y^2`468?YV?BJ<#$G~w;T&O{%ukp+wrUcecT-**q+7kjZ7}hxKdd1lD&o&_?P z2){yT7V8~ueuqmQjP3`c#Ta&Q&pXf5pc3}L$BdyU?0{e+FN5bv(SkEv2Udy}*mJF6 z@CZ8GyCIx|;s5VhR_h%&7?y5i%4cP~Fl9YMKDR0>J_z{9#naDp ze0yslL(I#Va%-+b3=w~;u9=1%SZlzr^PdIh$z+1lz#s#|EjW@X>7$ z0=JkZ2xVXJxzE06O@nE}Hcgh?!0-jYCIyfDkd^m!u`*1a_=eE U30Z%h19J_7r>mdKI;Vst0J^14rT_o{ literal 0 HcmV?d00001 diff --git a/library/thumbs/bau_dach_plateau.png b/library/thumbs/bau_dach_plateau.png new file mode 100644 index 0000000000000000000000000000000000000000..997db212d260d2da0fe41f60fd0b13166d4788fa GIT binary patch literal 1439 zcmchX`#;lr0LDMxF`K0@*NTmhl4&_EDQA5}Y?MaMRLXKr33<6!s>OHKaY>P!&@0z; z$|baNIW;+)c4BSEqB6HM#ORP|?$_h|59f7WujhH*KRwTnPYT`BZ9Q%i4gj#8Ms@LC zBl54YDr@?f{_S)R7;>ToN>E~_qr}lDmox@2h@rQu6b4^jnno@D`y{3la4g6TM;j~Q3#(H~> zL}vX!!=zCrT+QkcZys}Z3g zZ?7J43wlKHYq^@8FyC+qKmCHv2(#LtLJZVnuMBN8@<+}uWM41+P&Cy$Fc{j@pi?wxr2(;#+J_sT(j$QOKb2fih%JNSo{uFB9 z$8);@&e|vK?km9bSe7B>x{trr_-+JJ+UNR!yu;r$&*wC`T2EYi41g0#O+P)_hPgA| zvM&CLc2@r<#gl84ggyGQhgcU?y6Lb<9*ei>4cT^tWaN(M<8)P6yajfFL=uci|MY#3 zF=-JR6)RBzZiV;)9^M_D8MvP-CxNK5M(p$P@7J`bMV>^P?SucL2B$%<@|c>mI@HGhTdSI?pKG zM<<)lWd1gq*MV=1`GP-{6dAN@5t>BtnJU*A(Csk{f1MO=mBFp9HKX2aFj;pbRV|J& zgCSt`LK3M^y@yk3GDMLIP_C8z^g zsnf;`!0g~V-pM0ows-B6;JGNZ?Ka7kVWQZ~kp>IrYVfWM{_3nBT%{HmKYTY-iHpkh zPk*c&Mb01ZuZp~Sqxw>6ai@P>+EK`|{D;O#7tbb(TbH#iIxY^G_#-Fk*)1vbm>Hp@ z5C=t-!Be}i&xjf;RRI?W@ z9Uo_gU@7zz0GqRA;SP1lneMC9nI2)o`^6QjGED$HeJQ5yRj4zSfA#y_vPBb14uZy2 zTK{Wvf6XqYpB(DXgo%uV?8WTl1-sI|)*RX(az_3UM`d-}XMM4N3#LOfc$SvONFf_2 z^ydJ`hleAEW(Tqp;28;lRR}>ykut#fvh6tPK6@|`0cNq!_Z3{oyV##ISA&oB(9$Us zyW6&uqSD$w9hb9J&_EY|&4aaYu;@>z(oGzgGR?8xaYDGl;HcZ!tkrj7nimz)mym>s z)GCDs*P^BzqAT#Mn6~({aUVqjEN4FeyLsnnWEMe$n9qR>fE-S^(wU%sOBE=al37id zO$EvePw)m4qz1F4Wdc)5`UQ2gHB4RgoYdlTIMWMi`42Tu?1|(FR2``NwZ{dt1D-CG I`%fkO1O82QmjD0& literal 0 HcmV?d00001 diff --git a/library/thumbs/bau_dach_schraege.png b/library/thumbs/bau_dach_schraege.png new file mode 100644 index 0000000000000000000000000000000000000000..8475dd5ec86ec251e38e8b2524ef11f5dccb44c7 GIT binary patch literal 1197 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-HD>U`h9MaSW-L z^LFmZpvMLxuIq)aT{!ITP#(HK{O|wy2Lf3C3fnSdPx_*yabsi7>1{&xFYj$`;Bhkh z+P7XMeZ%gkzjvH2Y?@Kceq@gaLy-!DrBH(mr$Zl$f;-a$WyTW|8J^HXc+TdXM$qaW|L)K3ubG}J$!xccblf8P#2yWCS9 z#Ae1?@WbNCZTU$&oEtO`aC|e$X>4Dc(s8th;rr|ETV4uB^jMuD1Ja+)%l@wFVx7j4 zaUuSB#wsU|HmhYU9zmby`KdV1eRGawS4-{MJH~p~_X~fUA-|2)Bl0cZ3{?ZQhVC_$ zRmKrOmV?aRr}8h^7qcv5*%3XT*_l*Za2 z$<{ZMQAbEZ^nlwQV~>Q82HC$-U$)G6HIZ4*Jk>kFk72)1hQVQj2bz&g#Y{C1wCsQ1 zmYse2?EJ|&&#y>`Da`X@J@!j*Mr)j?hGQJl84L3-v-NzL9y1-`XZ8$Ukj@)5iRBn8 zhk-L!i&5gQKxj%D%Uz4wYW1_y_Bsj8~}oVz*mC$prn?x=ko$?0=&>z%xNy|0xW^n_-J9tiel zyuN3u0c+d7vujrGW&R-hP1QjEKuDCgNvfCNUb(Fwk4@9tB#_|6<&v-b@#6cRef7=1 zY)zNhNmq2tXa8eSotrECx$@98vvuFJ`NV(kWNCM=k-PL+CuJhT_Wb8-H7|S(*d*-1 zeCv05k%rxbPKE=!S#;0)TDsb3{HpG&vwvg%$h&5~@#J0xhJxtoi5V4KOJ+_go!@k3 ziG^X75QE1Yrc^Qc6D52ChrA0m z*GQV5Yv=I!VxP#XU&G^b;XV|8cR?7 guT`X82L8kDy>1oj+Rtx;f#nZ_r>mdKI;Vst0Q0cvU;qFB literal 0 HcmV?d00001 diff --git a/src/editor/bau_katalog.gd b/src/editor/bau_katalog.gd index c960e1c..75a71fc 100644 --- a/src/editor/bau_katalog.gd +++ b/src/editor/bau_katalog.gd @@ -52,6 +52,8 @@ const GRUPPEN := [ {"key": "dach", "label": "Dach & Giebel (klein 2 m)", "oids": ["bau_giebelwand", "bau_dach_giebel", "bau_dach_pyramide"]}, {"key": "dach_mittel", "label": "Dach & Giebel (mittel 4 m)", "oids": ["bau_giebelwand_4", "bau_dach_giebel_4", "bau_dach_pyramide_4"]}, {"key": "dach_gross", "label": "Dach & Giebel (groß 6 m)", "oids": ["bau_giebelwand_6", "bau_dach_giebel_6", "bau_dach_pyramide_6"]}, + {"key": "dach_teile", "label": "Dach aus Einzelteilen (45°)", "oids": ["bau_dach_schraege", "bau_dach_ecke_aussen", + "bau_dach_ecke_innen", "bau_dach_plateau", "bau_dach_giebel_ende"]}, {"key": "treppe", "label": "Treppe", "oids": ["bau_treppe"]}, ] @@ -139,6 +141,29 @@ const KINDS := { "label": "Pyramidendach 6×6 m", "info": "6×6 m, Spitze in der Mitte", "snap": "zelle", "traeger": false, "typ": T_DACH_PYRAMIDE, "punkte": QUADRAT_6, "rise": 3.0, "farbe": Color(0.40, 0.25, 0.18), }, + # Dach aus Einzelteilen: alles 2×2-Zellen mit 45° (2 m Lauf = 2 m Höhe), massiv + # (rundum geschlossen, keine Lücke von innen). "profil" → _dach_mesh(). Tiefe + # Kante = -Z lokal (R dreht), Ecken: tiefer Punkt bei (-X,-Z). + "bau_dach_schraege": { + "label": "Dachschräge 45°", "info": "2×2 m, 2 m hoch — eine Seite des Dachs; die tiefe Kante zeigt in Richtung der Vorschau (R dreht)", + "snap": "zelle", "traeger": false, "profil": "schraege", "farbe": Color(0.40, 0.25, 0.18), + }, + "bau_dach_ecke_aussen": { + "label": "Dachecke außen", "info": "Walm-Ecke (Grat): zwei Schrägen treffen sich an einer Hausecke, die tiefe Ecke zeigt nach außen", + "snap": "zelle", "traeger": false, "profil": "ecke_aussen", "farbe": Color(0.40, 0.25, 0.18), + }, + "bau_dach_ecke_innen": { + "label": "Dachecke innen", "info": "Kehle: zwei Schrägen laufen in einer einspringenden Ecke (L-/T-Dach) zusammen", + "snap": "zelle", "traeger": false, "profil": "ecke_innen", "farbe": Color(0.40, 0.25, 0.18), + }, + "bau_dach_plateau": { + "label": "Dachplateau", "info": "2×2 m Block, 2 m hoch, oben flach — Mittelstück zwischen zwei Schrägen (breite Häuser), Schornstein-Sockel", + "snap": "zelle", "traeger": true, "profil": "plateau", "farbe": Color(0.40, 0.25, 0.18), + }, + "bau_dach_giebel_ende": { + "label": "Dachstirn 45°", "info": "Senkrechte Dreieck-Stirn (2 m breit, 2 m hoch) — Giebel-Abschluss unter einer Schräge, wenn das Dach an der Seite offen bleibt", + "snap": "kante", "traeger": false, "profil": "stirn", "farbe": Color(0.62, 0.53, 0.38), + }, "bau_treppe": { "label": "Treppe", "info": "4 m lang, 2 m breit, 3 m hoch — führt in ein Geschoss darüber", "snap": "treppe", "traeger": false, @@ -190,7 +215,13 @@ static func mesh(oid: String) -> ArrayMesh: var kind: Dictionary = KINDS.get(oid, {}) if kind.is_empty(): return null - var roh: ArrayMesh = _box_mesh(kind["box"]) if kind.has("box") else _addon_mesh(kind) + var roh: ArrayMesh + if kind.has("box"): + roh = _box_mesh(kind["box"]) + elif kind.has("profil"): + roh = _dach_mesh(str(kind["profil"])) + else: + roh = _addon_mesh(kind) var fertig := _normalisieren(roh, kind["farbe"] as Color) _mesh_cache[oid] = fertig return fertig @@ -204,6 +235,79 @@ static func _box_mesh(size: Vector3) -> ArrayMesh: return out +## Massive Dach-Teile aus Dreiecken (Zelle 2×2 m, lokal x,z ∈ [-1,1], 45°). +## Oberfläche h(x,z): Schräge = z+1, Ecke außen = min(x,z)+1, Ecke innen = max(x,z)+1, +## Plateau = 2. Dach-Stirn: dünnes Dreieck-Prisma. +static func _dach_mesh(profil: String) -> ArrayMesh: + var tris: Array = [] # je Dreieck [a, b, c] als Vector3 + var V := func(x: float, y: float, z: float) -> Vector3: return Vector3(x, y, z) + var quad := func(a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> void: + tris.append([a, b, c]) + tris.append([a, c, d]) + var tri := func(a: Vector3, b: Vector3, c: Vector3) -> void: + tris.append([a, b, c]) + var mitte := Vector3(0.0, 0.7, 0.0) + match profil: + "schraege": + quad.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 2, 1), V.call(-1, 2, 1)) # Hang + quad.call(V.call(-1, 0, 1), V.call(1, 0, 1), V.call(1, 2, 1), V.call(-1, 2, 1)) # Rückwand + tri.call(V.call(-1, 0, -1), V.call(-1, 0, 1), V.call(-1, 2, 1)) # Stirn links + tri.call(V.call(1, 0, -1), V.call(1, 0, 1), V.call(1, 2, 1)) # Stirn rechts + quad.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 0, 1), V.call(-1, 0, 1)) # Boden + "ecke_aussen": + tri.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 2, 1)) + tri.call(V.call(-1, 0, -1), V.call(1, 2, 1), V.call(-1, 0, 1)) + tri.call(V.call(1, 0, -1), V.call(1, 0, 1), V.call(1, 2, 1)) + tri.call(V.call(-1, 0, 1), V.call(1, 0, 1), V.call(1, 2, 1)) + quad.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 0, 1), V.call(-1, 0, 1)) + "ecke_innen": + tri.call(V.call(-1, 0, -1), V.call(1, 2, -1), V.call(1, 2, 1)) + tri.call(V.call(-1, 0, -1), V.call(1, 2, 1), V.call(-1, 2, 1)) + tri.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 2, -1)) + tri.call(V.call(-1, 0, -1), V.call(-1, 2, 1), V.call(-1, 0, 1)) + quad.call(V.call(1, 0, -1), V.call(1, 0, 1), V.call(1, 2, 1), V.call(1, 2, -1)) + quad.call(V.call(-1, 0, 1), V.call(1, 0, 1), V.call(1, 2, 1), V.call(-1, 2, 1)) + quad.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 0, 1), V.call(-1, 0, 1)) + "plateau": + var bm := BoxMesh.new() + bm.size = Vector3(2.0, 2.0, 2.0) + var arr := bm.get_mesh_arrays() + var out := ArrayMesh.new() + out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arr) + return out + "stirn": + # Dünnes Dreieck-Prisma (Kante entlang X, 2 m breit, 2 m hoch, 0,2 m dick) + var d := 0.1 + tri.call(V.call(-1, 0, -d), V.call(1, 0, -d), V.call(-1, 2, -d)) + tri.call(V.call(-1, 0, d), V.call(-1, 2, d), V.call(1, 0, d)) + quad.call(V.call(-1, 0, -d), V.call(-1, 0, d), V.call(1, 0, d), V.call(1, 0, -d)) + quad.call(V.call(-1, 0, -d), V.call(-1, 2, -d), V.call(-1, 2, d), V.call(-1, 0, d)) + quad.call(V.call(1, 0, -d), V.call(1, 0, d), V.call(-1, 2, d), V.call(-1, 2, -d)) + mitte = Vector3(-0.33, 0.67, 0.0) + var pos := PackedVector3Array() + var nrm := PackedVector3Array() + for t in tris: + var a: Vector3 = t[0] + var b: Vector3 = t[1] + var c: Vector3 = t[2] + var n := (b - a).cross(c - a).normalized() + # nach außen drehen: Fläche muss vom Körper-Mittelpunkt wegzeigen + if n.dot((a + b + c) / 3.0 - mitte) < 0.0: + var tmp := b + b = c + c = tmp + n = -n + pos.append_array(PackedVector3Array([a, b, c])) + nrm.append_array(PackedVector3Array([n, n, n])) + var arrays := [] + arrays.resize(Mesh.ARRAY_MAX) + arrays[Mesh.ARRAY_VERTEX] = pos + arrays[Mesh.ARRAY_NORMAL] = nrm + var m := ArrayMesh.new() + m.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) + return m + + static func _addon_mesh(kind: Dictionary) -> ArrayMesh: var geo := Node3D.new() geo.set_script(load(GEO_SCRIPT)) diff --git a/src/editor/build_snap.gd b/src/editor/build_snap.gd index e9d802e..36997a3 100644 --- a/src/editor/build_snap.gd +++ b/src/editor/build_snap.gd @@ -21,6 +21,8 @@ class_name BuildSnap const TOL_M := 0.35 # so weit darf eine Ebene ÜBER dem Klickpunkt liegen const SHRINK_M := 0.05 # Grundflächen-Schnitt: minimale Überlappung ignorieren const SAME_M := 0.08 # Toleranz für "gleiche Stelle" +const UEBERSTAND_M := 0.01 # Wand-Außenseite steht 1 cm vor der Plattenkante: keine deckungsgleichen + # Flächen (Z-Fighting/Flackern an Boden↔Wand) ## Ergebnis-Wörterbuch: @@ -36,8 +38,12 @@ const SAME_M := 0.08 # Toleranz für "gleiche Stelle" ## gelaende: Callable(x: float, z: float) -> float ## dreh: Nutzerdrehung in Grad (wird auf 90° gerastet) ## kamera_dir: Blickrichtung des Klickstrahls (für Zell-Wahl an Wandkanten), optional +## ebene_fix: feste Geschosshöhe (Geschoss-Wahl im Bauen-Tab); NAN = automatisch. +## Dann hängt die Höhe NICHT mehr an Wänden/Böden darunter — Decken und Böden +## lassen sich auch mitten im großen Raum setzen. static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array, - gelaende: Callable, hit_ist_objekt := false, kamera_dir := Vector3.ZERO) -> Dictionary: + gelaende: Callable, hit_ist_objekt := false, kamera_dir := Vector3.ZERO, + ebene_fix := NAN) -> Dictionary: var art := BauKatalog.snap_art(oid) var gr := BauKatalog.groesse(oid) var q := int(roundf(dreh / 90.0)) @@ -97,11 +103,11 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array, var v_mitte := Vector2(roundf(fx) * r, _raster_mitte(p.y, laenge)) # Kante entlang Z if p.distance_squared_to(h_mitte) <= p.distance_squared_to(v_mitte): mitte = h_mitte - mitte.y += halbe_dicke if p.y >= h_mitte.y else -halbe_dicke + mitte.y += (halbe_dicke - UEBERSTAND_M) * (1.0 if p.y >= h_mitte.y else -1.0) rot = 0.0 else: mitte = v_mitte - mitte.x += halbe_dicke if p.x >= v_mitte.x else -halbe_dicke + mitte.x += (halbe_dicke - UEBERSTAND_M) * (1.0 if p.x >= v_mitte.x else -1.0) rot = 90.0 rot += 180.0 * float(viertel % 2) # R dreht die Wand um (Blickseite der Teile) rot = fposmod(rot, 360.0) @@ -133,7 +139,7 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array, ebenen.append((t["pos"] as Vector3).y) else: ebenen.append(top) - var ebene := _waehle_ebene(ebenen, hit.y) + var ebene := ebene_fix if not is_nan(ebene_fix) else _waehle_ebene(ebenen, hit.y) # Platz belegt? Gleiche Art + gleiche Stelle + gleiche Ebene → ersetzen/ablehnen. var ersetze := -1 @@ -234,3 +240,17 @@ static func sammle_teile(doc: Dictionary) -> Array: "rot": float(SvmapIO.objekt_rotation(o as Dictionary).y), }) return out + + +## Höhe des untersten Geschosses (= Fußpunkt der tiefsten Platte/Wand im Umkreis von +## `radius` um `p`), sonst das Gelände bei `p`. Geschoss k liegt bei Basis + k·WAND_H. +static func geschoss_basis(teile: Array, p: Vector2, gelaende: Callable, radius := 40.0) -> float: + var tiefste := INF + for t in teile: + var tp: Vector3 = t["pos"] + if Vector2(tp.x, tp.z).distance_to(p) > radius: + continue + var oid := str(t["oid"]) + if BauKatalog.ist_platte(oid) or BauKatalog.snap_art(oid) == "kante": + tiefste = minf(tiefste, tp.y) + return tiefste if tiefste < INF else float(gelaende.call(p.x, p.y)) diff --git a/src/editor/interaction.gd b/src/editor/interaction.gd index 4f98c15..773f27e 100644 --- a/src/editor/interaction.gd +++ b/src/editor/interaction.gd @@ -52,6 +52,8 @@ var build_group_id := -1 # laufendes Gebäude; -1 = nächstes Teil be var _build_last: Dictionary = {} # letztes BuildSnap-Ergebnis (pos/rot/ersetze/ok/grund) var _build_hit: Dictionary = {} # letzter Treffer {punkt, obj, dir} var _build_hint := "" # letzte Statuszeile des Ghosts (kein Spam) +var build_geschoss := -1 # -1 = Höhe automatisch, sonst festes Geschoss (0 = Boden, 1 = erste Decke …) +const BAU_GESCHOSS_MAX := 8 # Terrain-Höhen-Cache (Karte t_4092e3a0): der Objekt-Cursor trifft das GELÄNDE # (TerrainField.raycast, wie Terrain-/Bauen-/Spawn-Werkzeug), nicht mehr nur @@ -252,6 +254,7 @@ func cancel_placement() -> void: _ghost_active = false _build_last = {} _build_hit = {} + build_geschoss = -1 main.call("_highlight_library", "") main.call("_base_tool_status") @@ -449,6 +452,8 @@ func _build_ray_hit(viewport_pos: Vector2) -> Dictionary: var origin := camera.project_ray_origin(viewport_pos) var dir := camera.project_ray_normal(viewport_pos) var ground: Variant = _ground_point(viewport_pos) + if build_geschoss >= 0: + return _build_ray_geschoss(origin, dir, ground) var best_t := INF var out := {} if ground != null: @@ -476,6 +481,55 @@ func _build_ray_hit(viewport_pos: Vector2) -> Dictionary: return out +## Feste Geschosshöhe: der Strahl trifft die waagerechte Ebene dieses Geschosses, +## egal ob darunter Wand, Boden oder nur Gelände ist (Decke mitten im Raum). +func _build_ray_geschoss(origin: Vector3, dir: Vector3, ground: Variant) -> Dictionary: + var xz := Vector2(origin.x, origin.z) + if ground != null: + xz = Vector2((ground as Vector3).x, (ground as Vector3).z) + var y := _bau_geschoss_hoehe(xz) + if absf(dir.y) < 0.001: + return {} + var t := (y - origin.y) / dir.y + if t <= 0.0: + return {} + return {"punkt": origin + dir * t, "obj": -1, "dir": dir, "ebene": y} + + +func _bau_gelaende_fn() -> Callable: + return func(x: float, z: float) -> float: + if _res >= 2 and _heights.size() >= _res * _res: + return TerrainField.height_at(_heights, _res, _sx, _sz, x, z) + return float(main.doc.get("terrain", {}).get("min_height_m", 0.0)) + + +func _bau_geschoss_hoehe(xz: Vector2) -> float: + var basis := BuildSnap.geschoss_basis(BuildSnap.sammle_teile(main.doc), xz, _bau_gelaende_fn()) + return basis + float(build_geschoss) * BauKatalog.WAND_H + + +## Bild↑/Bild↓ bzw. Knöpfe im Bauen-Tab: Geschoss wechseln (0 → zurück auf Automatik). +func bau_geschoss_aendern(delta: int) -> void: + var neu := build_geschoss + if delta > 0: + neu = 1 if build_geschoss < 0 else mini(build_geschoss + 1, BAU_GESCHOSS_MAX) + elif build_geschoss >= 0: + neu = build_geschoss - 1 # 0 → -1 (Automatik) + build_geschoss = neu + _build_hint = "" + if placing and not _build_hit.is_empty(): + _bau_ghost_neu() + main.call("_set_status", bau_geschoss_text()) + main.call("_refresh_bau_info") + + +func bau_geschoss_text() -> String: + if build_geschoss < 0: + return "Höhe: automatisch (Gelände bzw. Oberkante des Teils darunter). Bild↑ wählt ein festes Geschoss — dann sitzen Böden/Decken überall im Raum." + return "Höhe: festes Geschoss %d (%s). Bild↑/Bild↓ wechselt, bis „automatisch“ erreicht ist." % [ + build_geschoss, "Erdgeschoss-Boden" if build_geschoss == 0 else "%d × %d m über dem Boden" % [build_geschoss, int(BauKatalog.WAND_H)]] + + func _bau_ghost_update(viewport_pos: Vector2) -> void: _build_hit = _build_ray_hit(viewport_pos) if _build_hit.is_empty(): @@ -490,13 +544,10 @@ func _bau_ghost_update(viewport_pos: Vector2) -> void: func _bau_snap() -> Dictionary: if _build_hit.is_empty(): return {} - var gelaende := func(x: float, z: float) -> float: - if _res >= 2 and _heights.size() >= _res * _res: - return TerrainField.height_at(_heights, _res, _sx, _sz, x, z) - return float(main.doc.get("terrain", {}).get("min_height_m", 0.0)) return BuildSnap.berechne(place_oid, _build_hit["punkt"] as Vector3, place_rot_deg, - BuildSnap.sammle_teile(main.doc), gelaende, - int(_build_hit.get("obj", -1)) >= 0, _build_hit.get("dir", Vector3.ZERO) as Vector3) + BuildSnap.sammle_teile(main.doc), _bau_gelaende_fn(), + int(_build_hit.get("obj", -1)) >= 0, _build_hit.get("dir", Vector3.ZERO) as Vector3, + float(_build_hit.get("ebene", NAN))) func _bau_ghost_neu() -> void: diff --git a/src/ui/main.gd b/src/ui/main.gd index 6676497..34f03c8 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -485,7 +485,12 @@ func _open_settings() -> void: if settings_dialog == null: return settings_mod.call("refresh") - settings_dialog.popup_centered(UIScale.v2i(560, 520)) + # So groß, dass alles ohne Scrollen sichtbar ist (Inhalt + Titel/Knopf), begrenzt aufs Fenster. + var inhalt: Vector2 = settings_mod.get_combined_minimum_size() + var fenster := Vector2(get_window().size) + var breite := clampf(maxf(inhalt.x + UIScale.px(60), UIScale.px(640)), UIScale.px(640), fenster.x * 0.95) + var hoehe := clampf(inhalt.y + UIScale.px(110), UIScale.px(520), fenster.y * 0.95) + settings_dialog.popup_centered(Vector2i(int(breite), int(hoehe))) func _build_dialogs_and_overlays() -> void: @@ -1516,6 +1521,11 @@ func _unhandled_key_input(event: InputEvent) -> void: _test_start_apply(float(tp.get("x")), float(tp.get("z")), str(tp.get("hinweis"))) get_viewport().set_input_as_handled() return + if (key == KEY_PAGEUP or key == KEY_PAGEDOWN) and not ctrl and interaction.placing \ + and BauKatalog.ist_bau(interaction.place_oid): + interaction.bau_geschoss_aendern(1 if key == KEY_PAGEUP else -1) + get_viewport().set_input_as_handled() + return match key: KEY_T: # Karte t_96e3c4ee: T schaltet das Test-Start-Werkzeug um (Editor). diff --git a/src/ui/panels/bauen_panel.gd b/src/ui/panels/bauen_panel.gd index c485d1f..7b6e179 100644 --- a/src/ui/panels/bauen_panel.gd +++ b/src/ui/panels/bauen_panel.gd @@ -8,6 +8,7 @@ extends "res://src/ui/panels/editor_panel.gd" var bau_buttons: Dictionary = {} # oid → Button var info_label: Label +var geschoss_label: Label var _group: ButtonGroup @@ -22,11 +23,25 @@ func panel_title() -> String: func build() -> void: _group = ButtonGroup.new() _group.allow_unpress = true - add_child(_muted_label("Wähle ein Teil und klicke in die Karte. Wände rasten an die nächste Zellkante, Böden und Dächer in die Zellmitte (Raster 2 m). Klick auf die Oberkante einer Wand stapelt das nächste Geschoss, Klick auf die Seite ersetzt das Teil (z. B. Wand → Tür). R dreht, Esc beendet.")) + add_child(_muted_label("Wähle ein Teil und klicke in die Karte. Wände rasten an die nächste Zellkante, Böden und Dächer in die Zellmitte (Raster 2 m). Klick auf die Oberkante einer Wand stapelt das nächste Geschoss (oder Bild↑/Bild↓: festes Geschoss, dann sitzen Decken mitten im Raum), Klick auf die Seite ersetzt das Teil (z. B. Wand → Tür). R dreht, Esc beendet.")) for gruppe in BauKatalog.GRUPPEN: add_child(_field_label(str(gruppe["label"]))) add_child(_knopf_raster(gruppe["oids"] as Array)) add_child(_separator()) + add_child(_header_label("Höhe / Geschoss")) + geschoss_label = _muted_label(main.interaction.call("bau_geschoss_text")) + add_child(geschoss_label) + var hoehe := HBoxContainer.new() + hoehe.add_theme_constant_override("separation", UIScale.px(8)) + for spec in [["▲ Höher (Bild↑)", 1], ["▼ Tiefer (Bild↓)", -1]]: + var hb := _aktion(str(spec[0]), "Festes Geschoss wählen: Böden und Decken sitzen dann überall im Raum auf dieser Höhe (je 3 m). Ganz unten = automatisch.", + func() -> void: + main.interaction.call("bau_geschoss_aendern", int(spec[1])) + geschoss_label.text = main.interaction.call("bau_geschoss_text")) + hb.size_flags_horizontal = Control.SIZE_EXPAND_FILL + hoehe.add_child(hb) + add_child(hoehe) + add_child(_separator()) add_child(_header_label("Gebäude")) info_label = _muted_label("") add_child(info_label) @@ -92,6 +107,8 @@ func refresh_info() -> void: if info_label == null or main == null or main.doc.is_empty(): return var gebaeude := BauOps.gebaeude_gruppen(main.doc) + if geschoss_label != null: + geschoss_label.text = main.interaction.call("bau_geschoss_text") var text := "%d Gebäude, %d Bauteile in der Map." % [gebaeude.size(), BauOps.teile_zahl(main.doc)] var gid := int(main.interaction.get("build_group_id")) for g in gebaeude: diff --git a/tests/test_bauen.gd b/tests/test_bauen.gd index 68fed9d..41e6c06 100644 --- a/tests/test_bauen.gd +++ b/tests/test_bauen.gd @@ -45,6 +45,9 @@ func _run_tests() -> void: _test_snap_cells_and_edges() _test_snap_levels() _test_replace_and_stack() + _test_dach_teile() + _test_geschoss_fix() + _test_wand_ueberstand() _test_ops_group_undo() _test_duplicate_building() _test_save_load() @@ -55,7 +58,7 @@ func _run_tests() -> void: func _test_catalog() -> void: print("Katalog") - _check(BauKatalog.KINDS.size() == 18, "18 Bauteile im Katalog") + _check(BauKatalog.KINDS.size() == 23, "23 Bauteile im Katalog") var in_gruppen := {} for g in BauKatalog.GRUPPEN: for oid in (g["oids"] as Array): @@ -99,7 +102,7 @@ func _test_meshes() -> void: print("Meshes / Fußpunkt-Normalisierung") for oid in BauKatalog.oids(): var m := BauKatalog.mesh(oid) - _check(m != null and m.get_faces().size() >= 36, "%s: Mesh vorhanden" % oid) + _check(m != null and m.get_faces().size() >= 18, "%s: Mesh vorhanden" % oid) var aabb := m.get_aabb() _check(absf(aabb.position.y) < 0.001, "%s: Unterkante y = 0" % oid) _check(absf(aabb.position.x + aabb.size.x * 0.5) < 0.001 and absf(aabb.position.z + aabb.size.z * 0.5) < 0.001, @@ -186,13 +189,13 @@ func _test_snap_cells_and_edges() -> void: _check(s["pos"] == Vector3(1.0, 0.0, 1.0), "Fundament: Zellmitte (1, 1)") s = BuildSnap.berechne("bau_fundament", Vector3(-0.3, 0.0, -2.9), 0.0, kein, _flach(0.0)) _check(s["pos"] == Vector3(-1.0, 0.0, -3.0), "Fundament: negative Zelle (−1, −3)") - s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 0.0, kein, _flach(0.0)) - _check(s["pos"] == Vector3(1.0, 0.0, 0.1) and float(s["rot"]) == 0.0, "Wand: Kante entlang X, Drehung 0, 10 cm in die Zelle gerückt (bündig)") - s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 0.0, kein, _flach(0.0)) - _check(s["pos"] == Vector3(0.1, 0.0, 1.0) and float(s["rot"]) == 90.0, "Wand: Kante entlang Z, Drehung 90, 10 cm in die Zelle gerückt") - s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 90.0, kein, _flach(0.0)) - _check(float(s["rot"]) == 180.0 and s["pos"] == Vector3(1.0, 0.0, 0.1), "R dreht die Wand um (180°), bleibt an ihrer Stelle") - s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 90.0, kein, _flach(0.0)) + s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.09), 0.0, kein, _flach(0.0)) + _check(_nah(s["pos"], Vector3(1.0, 0.0, 0.09)) and float(s["rot"]) == 0.0, "Wand: Kante entlang X, Drehung 0, 9 cm in die Zelle gerückt (1 cm Überstand)") + s = BuildSnap.berechne("bau_wand", Vector3(0.09, 0.0, 1.0), 0.0, kein, _flach(0.0)) + _check(_nah(s["pos"], Vector3(0.09, 0.0, 1.0)) and float(s["rot"]) == 90.0, "Wand: Kante entlang Z, Drehung 90, 9 cm in die Zelle gerückt (1 cm Überstand)") + s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.09), 90.0, kein, _flach(0.0)) + _check(float(s["rot"]) == 180.0 and _nah(s["pos"], Vector3(1.0, 0.0, 0.09)), "R dreht die Wand um (180°), bleibt an ihrer Stelle") + s = BuildSnap.berechne("bau_wand", Vector3(0.09, 0.0, 1.0), 90.0, kein, _flach(0.0)) _check(float(s["rot"]) == 270.0, "R: Z-Kante 90° → 270°") s = BuildSnap.berechne("bau_pfeiler", Vector3(1.8, 0.0, 2.3), 0.0, kein, _flach(0.0)) _check(s["pos"] == Vector3(2.0, 0.0, 2.0), "Pfeiler: nächste Zellecke") @@ -205,8 +208,8 @@ func _test_snap_cells_and_edges() -> void: var p := Vector3(-9.0 + 0.31 * k, 0.0, 7.0 - 0.27 * k) var w := BuildSnap.berechne("bau_wand", p, 0.0, kein, _flach(0.0)) var wp: Vector3 = w["pos"] - var ex := absf(absf(wp.x - roundf(wp.x / 2.0) * 2.0) - 0.1) < 0.001 # X nahe einer geraden Linie - var ez := absf(absf(wp.z - roundf(wp.z / 2.0) * 2.0) - 0.1) < 0.001 + var ex := absf(absf(wp.x - roundf(wp.x / 2.0) * 2.0) - 0.09) < 0.001 # X nahe einer geraden Linie + var ez := absf(absf(wp.z - roundf(wp.z / 2.0) * 2.0) - 0.09) < 0.001 var ox := absf(fposmod(wp.x, 2.0) - 1.0) < 0.001 # X auf Zellmitte var oz := absf(fposmod(wp.z, 2.0) - 1.0) < 0.001 if not ((ex and oz) or (ez and ox)): @@ -223,7 +226,7 @@ func _test_snap_cells_and_edges() -> void: var ra := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wa["pos"], "rot": wa["rot"]}) var rb := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wb["pos"], "rot": wb["rot"]}) _check(ra.has_point(Vector2(0.01, 0.01)) and rb.has_point(Vector2(0.01, 0.01)), "Ecke: äußere Ecke (0,0) ist von BEIDEN Wänden gedeckt") - _check(absf(ra.position.x) < 0.001 and absf(ra.position.y) < 0.001 and absf(rb.position.x) < 0.001 and absf(rb.position.y) < 0.001, + _check(absf(ra.position.x) < 0.0111 and absf(ra.position.y) < 0.0111 and absf(rb.position.x) < 0.0111 and absf(rb.position.y) < 0.0111, "Ecke: beide Wände beginnen bündig an der Außenkante (0,0)") # Große Teile: Zellzahl bestimmt die Rasterung s = BuildSnap.berechne("bau_dach_giebel_4", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0)) @@ -231,7 +234,7 @@ func _test_snap_cells_and_edges() -> void: s = BuildSnap.berechne("bau_dach_pyramide_6", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0)) _check(s["pos"] == Vector3(3.0, 0.0, 3.0), "Pyramidendach 6×6: Mitte einer Zelle (3, 3)") s = BuildSnap.berechne("bau_giebelwand_4", Vector3(3.0, 0.0, 0.5), 0.0, kein, _flach(0.0)) - _check(s["pos"] == Vector3(4.0, 0.0, 0.1), "Giebelwand 4 m: Mitte auf gerader Linie (4), in die Zelle gerückt") + _check(_nah(s["pos"], Vector3(4.0, 0.0, 0.09)), "Giebelwand 4 m: Mitte auf gerader Linie (4), in die Zelle gerückt") # Gelände: höchste Höhe unter der Grundfläche zählt s = BuildSnap.berechne("bau_fundament", Vector3(1.0, 5.0, 1.0), 0.0, kein, _flach(5.0)) _check(absf(float(s["ebene"]) - 5.0) < 0.001, "Gelände-Höhe wird die Ebene (5,0)") @@ -441,3 +444,64 @@ func _test_migration() -> void: var leer := _neue_doc() leer["gebaeude"] = [] _check(BauOps.migriere_gebaeude(leer) == 0 and not leer.has("gebaeude"), "leere Sektion wird still entfernt") + + +func _nah(a: Vector3, b: Vector3) -> bool: + return a.distance_to(b) < 0.0005 + + +## Dach-Einzelteile: massiv (geschlossen, Volumen > 0, Normalen nach außen), 45°. +func _test_dach_teile() -> void: + print("Dach aus Einzelteilen") + for oid in ["bau_dach_schraege", "bau_dach_ecke_aussen", "bau_dach_ecke_innen", "bau_dach_plateau", "bau_dach_giebel_ende"]: + var m := BauKatalog.mesh(oid) + var f := m.get_faces() + var vol := 0.0 + for i in range(0, f.size() - 2, 3): + vol += f[i].dot(f[i + 1].cross(f[i + 2])) / 6.0 + _check(vol < -0.05, "%s: geschlossener Körper, Volumen %.2f m³ (Godot: Vorderseite im Uhrzeigersinn → negativ = nach außen)" % [oid, vol]) + # jede Kante kommt in genau zwei Dreiecken vor (wasserdicht) + var kanten := {} + for i in range(0, f.size() - 2, 3): + for k in 3: + var a := f[i + k] + var b := f[i + (k + 1) % 3] + var key := "%s|%s" % [str(a.snapped(Vector3.ONE * 0.001)), str(b.snapped(Vector3.ONE * 0.001))] + kanten[key] = int(kanten.get(key, 0)) + 1 + var offen := 0 + for key in kanten: + var teile: PackedStringArray = (key as String).split("|") + if not kanten.has("%s|%s" % [teile[1], teile[0]]): + offen += 1 + _check(offen == 0, "%s: wasserdicht (keine offene Kante, %d)" % [oid, offen]) + var sch := BauKatalog.groesse("bau_dach_schraege") + _check(absf(sch.x - 2.0) < 0.01 and absf(sch.y - 2.0) < 0.01 and absf(sch.z - 2.0) < 0.01, "Schräge 2×2 m Grundfläche, 2 m hoch = 45°") + # Haus 4 m breit aus zwei Schrägen + Gegenschräge: Firsthöhe 2 m über Wandkrone + var kein: Array = [] + var s := BuildSnap.berechne("bau_dach_schraege", Vector3(1.2, 3.0, 1.1), 90.0, kein, _flach(0.0)) + _check(bool(s["ok"]) and float(s["rot"]) == 90.0, "Schräge lässt sich in 90°-Schritten drehen") + + +## Geschoss-Wahl: feste Höhe, unabhängig von Wänden darunter (Decke mitten im Raum). +func _test_geschoss_fix() -> void: + print("Geschoss fest (Decke mitten im Raum)") + var kein: Array = [] + # Raum 6×6: nur Außenwände, in der Mitte steht nichts. Klick auf dem Boden (y=0). + var s := BuildSnap.berechne("bau_boden", Vector3(3.0, 0.0, 3.0), 0.0, kein, _flach(0.0), false, Vector3.ZERO, 3.0) + _check(float(s["ebene"]) == 3.0 and _nah(s["pos"], Vector3(3.0, 3.0, 3.0)), "Decke auf 3 m mitten im Raum (kein Teil darunter)") + var ohne := BuildSnap.berechne("bau_boden", Vector3(3.0, 0.0, 3.0), 0.0, kein, _flach(0.0)) + _check(float(ohne["ebene"]) == 0.0, "Automatik bleibt wie bisher (Gelände)") + var teile := [{"i": 0, "oid": "bau_fundament", "pos": Vector3(1, 0.5, 1), "rot": 0.0}, {"i": 1, "oid": "bau_wand", "pos": Vector3(1, 0.5, 0.09), "rot": 0.0}] + _check(is_equal_approx(BuildSnap.geschoss_basis(teile, Vector2(5, 5), _flach(0.0)), 0.5), "Basis = tiefstes Teil in der Nähe (0,5 m)") + _check(is_equal_approx(BuildSnap.geschoss_basis([], Vector2(5, 5), _flach(2.0)), 2.0), "Basis ohne Teile = Gelände") + + +## Wand-Außenseite steht 1 cm vor der Plattenkante (kein Z-Fighting durch deckungsgleiche Flächen). +func _test_wand_ueberstand() -> void: + print("Wand ragt 1 cm über die Plattenkante") + var kein: Array = [] + var w := BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.3), 0.0, kein, _flach(0.0)) + var b := BuildSnap.berechne("bau_fundament", Vector3(1.0, 0.0, 0.3), 0.0, kein, _flach(0.0)) + var wr := BuildSnap.teil_rect({"oid": "bau_wand", "pos": w["pos"], "rot": w["rot"]}) + var br := BuildSnap.teil_rect({"oid": "bau_fundament", "pos": b["pos"], "rot": b["rot"]}) + _check(wr.position.y < br.position.y - 0.005, "Wand-Außenfläche liegt vor der Plattenkante (%.3f < %.3f)" % [wr.position.y, br.position.y])