From 4b684574c6142fcd252e4ee87eca1672b5687563 Mon Sep 17 00:00:00 2001 From: "martin.fencl" Date: Thu, 29 Oct 2026 00:39:14 +0100 Subject: [PATCH] fix: update model version in app-config.json and add apple tree generation script --- locenice/data/app-config.json | 2 +- locenice/models/dum.glb | Bin 21836072 -> 21869060 bytes locenice/tools/apple_tree.mjs | 193 ++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 locenice/tools/apple_tree.mjs diff --git a/locenice/data/app-config.json b/locenice/data/app-config.json index a9420c6..17990bf 100644 --- a/locenice/data/app-config.json +++ b/locenice/data/app-config.json @@ -1,6 +1,6 @@ { "version": "locenice-web-01", - "model": "./models/dum.glb?v=20261026e", + "model": "./models/dum.glb?v=20261029", "renderer": "auto", "threeModule": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js", "threeLoader": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/loaders/GLTFLoader.js", diff --git a/locenice/models/dum.glb b/locenice/models/dum.glb index d5614448b08800160fb1440592d434f71fbd97b0..1fe7ab422ef3527a24ada9ee4cca8ec3349f3735 100644 GIT binary patch delta 30804 zcma%D1zZ(d_Z>ct;O2=tUY-q?#vnE9`ChT^fbf9VfdJCEloL&Gf!`C@;-3B!(s=M z#SW&69nAJQnExoXVNiTYQpH`uKRNJuxGN!lS+?=AiV!JQOO0%F9!uR?9gb`9HNv<)zVh zd#SamKbHti3JsQ43C5~0)JEg#}dt4Yi47j3b{1Wq;yE-V`jD{iVczB)l5_y z(!wg6dTB$POijxOSZ#$$rP8Ps-d>&xg_qK!zOsQzrPg};sJy(rRNiV&l{{k~m0G1# zD!jZ^T9x;oizsh2FO?@25zFuGqu*$1FK@M%rxIDMC$^;$TTQFf$Quls!V7yysr_TC z4Ga0DHnB2MZrE|9%6}HgO~A}N62neeXu9xDQgwj(^yVy4Id?@ zs>Xiv^ir$Tia(dDXsdW>u}(f}wOXN67OjGhk4CHY@ls-8FcoiktrVCj_NS+(O6!G9 zEH76`Ee{iWQ>7~8L;3$~xR5*NO{&TJb;HE?%71P)OxQ=`sqn;(^YPN6yQmsyytSTQ zSYjWo7E_eFn-X13t5W%3Ns9XApG$}>r}p+zD1Feg)Jlz)x7^*(_fYSx)Oae<6ExoP z1iUr!CRcg;_-MVoy_EVb7m{7Xv*Rz+ELj(oKKWqxBuPM%^oKe-RfT_2qRrvfi8Oj|zBX*{(WFAchz56*M) z{8T;~oG(-wrI*%Ag$j9o9wC$7SyZc`P^kWPW`hI94xT!B{Ir0|Q)UbanOMI>b1%#R z^HJk)#iTJ0Oj)hgC~Kua~!+L!^~Y2yo(n)OZ)IoQXE1sk%fB6Lsc5O^FZI zb{<-d8qfgR;7(K`vlCgCnp6!dJvMOih^mdWIII4-{s#Rc#L3s(C4@Fu+J@u|FX0-p zV`>R|n*g~#4xA!)$jG)OT(SOY{aXDoHT6GCt*EyJP7YCQuq^w}(`$$&HJlzg$sc>i zVCe?x``N!w&I~IZ5|CzD#ljmKRT~_fW(inj2B%rC=@n94vUCpVblcj>9G4i+kVjTF zmCdl-Lk?$Hy8b0^uEnkox%An(Ldc73YnPCVZ>=3I(Bo9v5XC9I+d4$*W91a`&dSEw z45xySK@Y7fhOjX6R+Epd5xZfza%ej)1LTiH07 zd278wF5a=O^#3VSZ?BL?KdjwD3I&+tYD-jm;Z&39YGX62gH`2k{>@DIHXSn8+_&j$ z5}e!H2CxO}0DHgza0E&MPJlDu0+a$=fzm)3zzrx1lmp5G6@ZFBCBPl13{(NC0@Z-( zKnYzT2m)pR zGl5yaY+w#B7Z8AXzs| zn}HBu3$PW~25bjHfiNH(hyZo~k-$zM3Wx?`fLLG`5C`lA;(-Jp5l8}(ffOJWNCVP= zJ-}XIACLiL0{ejjz(L>;kOgD|BESF%I1C&CjsnMk}kk0I!n)t0G_Om$_dCzFRv^<`=xQ$v|NWm3qb zlu0F%S|*K5TA93L@|MX*rbaR~mdRHpKbdqgkxWfwYAO>iQ!|;G%hW=qmNK=HskKaP zWNIsuzX`*s#H7g$VfQHwe=5C~)PDO;vS1qi9dzEns3N^;UoWZL%V?54=KFg|@kue% z{;Gz*m)T3QMPAlT;JFLG{TU7K{M=uPbc`a^VGSSf%wNhluGhIQ{H0@xm?BS=*Zxx0 zix|p9?d8}0f)s}aIq-k!FKt7CcwWP=DofI)6;YIYT*L1#LsAu0bdmmwe8~kdMIQS~ zBu%OlOBZfv_{Z)fy_g?Uq<2;%>66Qx^Ws*EGEPm95mqFPf6DKzm->O6%hF+70=_ zMKML5!soV9jrdiyl1H>q6i8^;xiX6SW@>l~>oe@;q@8qekA`1ue}-+^ ztJm+X&ae$$dQVQ_X?8Uzik=j z6os{)e%DR1x)V+Q@N{1-xJkVZL{kttpyxC<>DZ(g+N09)FPphYZx6-M1}`mtVup*< zadaFdL8}5?B>Mxq=sr9VRb8a{+jr3jg_gH-aFOOUj-wuHwfww!_R_@(36!`&%f}_! zOAG2J&>`s4+wCQfka!vkk2cU=S~n-2x(TbbeD7}dl8bo)eaqAG9-}R#^cu;O{Z7lf zt+tdVy-T8_&`0K3N>!&Ok?9*Pzqz}mbZTG{wSKMTskNn)dp3!-wejNf%uJ*Ww^Qk` zzZXBQl8I!JmP*HrZ%?46D{-iMxJrY*#?$1+8i7A`!AV+s zDwW(CYxoQ^XX#R-G*aj^{KI!nQr_+~n$|?ar=>Va-RGpyG34)TcajqFQ)%*BHShM= zUh=$L%j+DiL}$50H8>Du+|q@)YcH10Ca6Ha!L=k{pYco7|q*-B@eV<;9|)Hc>uQmu=j z5d}Ekh?Y`folwgBg+o#JYAFRpZ>OGLHT>*HmeSqn+iCA-4Zmo+r6fGsPR>6x{IkWD z(vXg!RKKd0pT(O=5wF+Nxyo98&jd4RX3%8xMO zPFO}Sc`e^+$1ld6T}B5+YWe=V^VyQ|^XT~q^zeuI%%#RWvK^-7C!Wb?yyHA78>r>m z1m`oKcJpZ9a4qjMDWB~Ro<|C733+Rc^pBxiILh95*hv?c$IudN#jDPCf>gU+3A1PHt4=1xnQ3{KJ?7G# zM;Ub3RM7HiLFUrR`59CO`)1cLbLrBm403YT@-Y)R>E3BZPfBU|gPS?&##csOZd!ir zQcjwj#b`|#+Gf$p*qlAuYP5-gzh=yTSv?1ieK61zByDy-dtV*RG6hnt`-WKouX46zLl!yz7Hug8$jMM6?RT}>0rEhHMm`IwjT*G&2|DEkU z7)d9WY4}=|zOz!JcG4*ve#;hrW9^piq+V-q{vXui8{6_alJ@xHTAq{7g5HGFuGShp z(c>dKQ9pv*p*xlM$ZU^9(9O0QzTxtGHt=2qIkeaCSBB)Xg5D7{+gih4ESJYdjtiyd zB{clk;~2Gg#@d$NMxT$V`L=JLvhkC*(Zv&L{!}zB9!s{7E=SEfuYStLs<+WKT>a#& zrEVER-Oz7VW)`p^Y0)$l`J^QU?B2I1^2C-~lKY!Qmyf2=(7Q5!vpC$m3@NGQZyy)F zFiZC+3dR0zuKdd0hwY^DXt-nbm7OVwr0#YwE%?HMEAFHP&=Y%qVeuDs(uYp)r@m#k zzwV&CE?T~d)jQT=`3^dc{QBo_*=F+{G_}2!|1#q(Tj{=o)kD zXDsPrI5iY-VOZJtIWwIePQ%gAp~`c%(s>Z)cPI_IqUFb)yUW@S4W+_MxF8+4%PgEi>GB0FzhuT;CRGik+tB+1@3PwJ}`Y)IcI z>KU)$y%*kNLk`B%z(kyq^X@V8rU{grrr}2%yT@{SCQw^kvaR`hZ0v)03QyMXtn5AJ zGB=)lHf#8;$F8xm*VE`(h=xB@_By-SHl5am;{5OR{TjR0FP%<>Y4~4jt})HWG`hDH z9cTPC_C6ww$}iCH)d!zr7eDN$2@5s+c}Z0OPb^n0v^ztQ&?TVgK}H(tY^UUiHqdQ0?fBCa(vkFnuH1c?q!((vDW zk1@MS68TTi@K5R-W8ZTb;e7!Y{Ij>O>W9FVruS#lhqd>8|E^G!wPoiv2OFDm)X&pC z2A(W<2zi1h3m%=U;b{a9lc%FyJj6HoyTE3o^k*Ypto3z&UMR|XxKMLOPZm5y)A<@%+4uG@feCTD`muk z@MOWGlQld-7EDDGYSJq1P&G_w@Sgsxa@`Y$&ipDAWo=ew{K0IFqZioJ@HB>pkSBN= z!=sZmJdNNfnoil_6SDHZ39MpNfA+OgZq|Z}g`%vlH5!zaR+6LTHSJ^I@r8#fMhX21 zCSRCzFcD0?FcnQ`Z_=@>3U35fb}qKcRR8Sd(+Wjduk*c}bwk0?HwXI|czod@ zcyzLc#}}TW>C_1_&1Uxn_N8}!_Oa)p?4B-#qO7yMN@TkT-8uU7)h-4mKbQy&1d|_3 zI$6Wy=h9t2q3yj(X1Bc|u%YexGZV)(*$wpivx|Yp4<16E;PHb; zCu?~8;9>77Ocdn~+5cP+ov3Y3g0Mlwd=xKqzZbdCv#<9=_kIfYGQX7wt$U#fVCP0E z89by(b<-6ue!Sa}vKuJa{o%PfFDF-;ZF)zn^2?K5s@6e-);-@8z;a)y89elgFKtj; zF`yKcndQN*roIw%U4NFLbmcX%e=ASc^lWbtTDQCHU{-CX4}*ycQk@Kn4;^%)@-J$! zacA>%8xzWr&GHLk_>P9G<*K0~v~I+Q!R*LSKL!t-o?&lLE1f7u8M|w+UZ>yaCcG<8 zZH^0P#oApOvi+eW#Wu&!>h9AJcG9v5gNeS*vDLl!U{SOWgX{ER`vaX=!^AGE*_k0( zQ+s&o@>Hg5=$D=p7VSaMWM^efo2Jgx!9#~yn6cuuUn;sXFV{}&avN+mYaY$}W*>3YWgqwU@m ztvmS9oGKn#uY-qXJg_#X{SjN0JzCR(b*k{lH!#^k*GTnAT)tocMRxEaXr1tUmML|} zj?lqGXYSb>6#ves!G`AX>`9b^-#G7Iel6d>7p)Qpl84HNpmoE$m{8YyF*FKXx(htT<+#BdeA1QtbWMlLf~Qk+tsio(EYepOu0yOw@j@ z2Nf^QUt~wa)s5Je$Ct(ND;?P2I?u)1=ap<>_)uBv&^fYfm+oi`4=Iuw8q_`xbfk~V z)l8ayU;KH@jvcG>Kx{Nj%f1E5GLe{!5+q(Hqut~{+hrW^@>2y;WCt!Pp-dV4ky`Y4cT#je_oN9D=>L7A_^G=-d z;;!G%-X5~njUMC79zJ?4d#ILIo>)};pW1{&CgPAsI@V%lEjpqMp!e@zi`ygK``L!o zleKQblTxfri?_0eJQSY{YK3{yhwQ=gz1iFc4ajZXU`lj+Dwf}3quZyeD{I}tsbyI~ zdcFt`U6^CSiqH7)(MQ?NEtIUrN)5Fj|}Gt@}F1olRWvO@xVZn%J_3Ew}2h z!dxGI3aho}gxGX(2iCvtQ?ZGS4g1}qP+TL`rvaM=`9bTFr}rm^OlN|J&M$Bgi`Slb zcTwEx?9a9jdLcTi1$*|*^rskf#Dl!IF7t!d9h*9U23#vcFj1G%_6Eh9pFI&zRcp?c zc6}rMaCBh}CgzKGU)LoeXult{Zcw{H)S^=rf`@`UO$=(KkDtW%etv99Qog9_?#6;% zy%d)U&Fc`q^RXWS-QJG@)b~amf{EyRUUu=N{&?Y+=y1)4ovr>^xlLv(oJy-dWe*{i=A%dI*~wT#iBOt`2G-Fw;9O z3?`bf^sPa0;=LvmTek+QS<6&6Q!z-_sP8A*1aBIpAETQmcc_V8$TG- z{@C7(tn8|?y88?K;=d2mxwg*{8;l&vKD>5k(7N#^^;z1+3e3t*s7j&V^L4F#ClsCM ze?LtX%fB&WofQf;w4Vv}-BXDYE$)b`0&A1PbeODl=h~dkZV|938y@m0;mwNIPU9`u z(i!#H+Mnh$|0|dXTw8*F^ySkq1cSOrC2g;i<&HN zoh>B}szAcxbK<$Z4d_v~5wg~uIr=NxBBEV3JR~*d4Q5>PvNfAI%$?O6=tP+g0Xy;D#Jbt=5NF-`&ssrVWAU^Ni*~EP2Ah_mQd7&3Q@$kH^u8F8JYHERNA*zC!+ThxVs-mV2 zxDFz@x;nTzqUxxr2d>A}g{sMUfISciHBnR_iTVg@p{N140ixO{Y6xzKNG`7nu8XKH ziWFc4=LzM(DZxra9;i`)Rfy`NMh#XYYJeIISc6Ee_5^z(@j3V6s11A_!5tCF75-pw~26q;?PLS=nF5oUmwTH1QxGSO#Fm?lX zLnN1U0(U~xo$JB%1^4CpK=tJMf%_rq$@Sv;gZm@u#r5U}fCnJz4R0TCA4GlN8w4K2 z4TS2)2?6*MfUqBogTaFl^@ni?cnBi7WFUATqJi)Qf&;msPyyU9@GwLH@C^qKM>H6| z5#SMsD+AaY(#QN5I6`?5R7xdbGbQCGjY2%1DFdni<<|YhiDdj^TG2G$rW?Ja}dpe zZy|Ugw*X4u7J(Nb65v}5UW{m-0OJxQmLQZ%7JwHZ3g(t_tH7(cl~BvL)!@~LmT}9u zHQ+UfmUAn(wcxdgR=~Rwyb{q$_|}8hbL*g1a~r@L5Uqx9BX}dCHSlc$3!4ziCF{WJ zkXi?02snh>47GvV0^WjX1AJS-TM=!9ZyR_UBDrEScr&8S@P&dyx$RI}xG-=SqAl=+ zgToPRg)agefd~}>tlNMHZU-01#e!qG7^t1xF7PfyJGm$>4jhLlii_rUgLfl}hBpQr zgD3{R1aJZu54DR+1ScZe1z!?42~iw;$>3x}az#8io)eNm30x{Tl}mw2G16b??)t8>;>;dbbvd^ zF#&%VCqf?LB(Q|&5SPUr1|LS0#bt9xz(){e!z+SCL?V30z{j|wP!e|>d>oMk-wE&u zM2F!!2|kHPt~d%lis&eOr@*JU98Nd}<7p&LBRme{8SoiICty4aK8r{$$pPme%7O1Z z_&j$G>NIx&d;!sE_%4DkB02-#CGaIga>Y6DIYgJaE8I=+P3{KNRqhsdMF8D|yvE%I z-$rzeyUyJK-$8U8<{RJ}h;G1l4}6ci3w4XT55AA+7JLuD4-nmk?;-dhBDvx&_%5Qm z@I3}U<{m-a=bnI{AiB>954flJ^AzC&7@vWkA(BfTfgd4y1mg?v3+_486D}8=i|7e_ zFTpPnJ%#TT_!T0#;yL&^BKcLV{PrWWObi9#6;DixwKV$0PRjj&C#&B|Nd1JlVWc>x zZ%|C!QS{#SkAi#Yc#`?bTAC9cP0>3v{K;9C5_8^7ad<>4=bdZrrXF~u5}sf!bufvc zmHRb()ytMriQBPsG+V=uTVo-uX`etB@YYk#hh4!7%3}T{cyl7$C_n>Vy*A9#@HzPw zl4@E6h2inJoIe*7NoDY!H)@=v^z2nMJz)pc~;0?wug@rWhYBZg^hqat< zCcSvIi$uJ?c{|ZWdOlzmEyBXd`3aWM2L97WH2n7xPtFI$;57=iR%#|E?cNnlQOEEEyh#De>>5dXqww0j z?+>=3TO^&pu9WlJen%Vl>j_UZd_qmUQhyao;%^OKt=bP((ISRgJ=5n$9oSj)o?d>- zu_`2*u3&dWF8jegPu@X4FwyQdU)Y*=Vbm*CpI?=2=--HbFz@Ggme)6q2I6Jq-1DDU zldlOxSTQ*-RG7AlHk`$?dht8^I4znAAL4O);b#`6h@|=0Xa1u;FjhW{3edUa{P2pA z2L5UoqR}e-Gb=qdj@+=&bJ%;f?L<7ezS8GkG>D~rr!gX;#b?&1M>PGyyM}3+4{Yb) zIO_Qd&;PIb<*^~<;_2B-eZ#z&F$M+s`|$3r^apmP=?<#29PjtOy=IfKa6Mx5`FWdT z=sG&1^||-#um%GY(1)DHzhN8t#gZ0>+^WtmSsOl@Zv7{(DYvtjUx=m!=oEot-muca zJ1Bjdh7a?9$sG6{6t!EQf7(5ozMatU4Z7vATCUNQo1o#xH-E`GoQk4y;TnGNl4tC8 z%U#q2n_AAlZL-V2zXeCZAlqD)o)b%^rds~``N!;+V>Hc_^8y+^_lu;m*otwMowREBWT)u!RZVso`177(-*FAzRR9Zjw1`~d%5A~DE)Yi zKMHIPX!!W%kJvDe9rSA>-t~FhV*xKCsDHFR?^1|`#+aYH8V}i)y;0Nv{VDm=9hU4G zLEEquKU}@eHpFbF;gS0M8Ry79{6bN|1#D7_u)AzZjaYKT%q;zHvK!xbQwk0(IX^Ww zmPX^GaWm--Yl4%N3cGA{ubXU0LIhpHaooGbRrbX-oCM53&c`j-Y2Y7$^Mrc#O?DwJ zmX_Ve$!gqo}yt{z7-+v24phHjieXMEpE z%g?tgDfLN?qG6u;{N!da27Wb0&jb&4mhPA)QMvwF-m#;TB>JTaw7-YG!EbFmW%z6P z!s|}b`)hG@X|k5z+TKZe@N+jU-KOQ^V@gUnU1Mp*D1E-I{me@`CbG7`AvNlqDNGwgjXbL$$zGfUv5{eaEU#{izH#$hU0m)>afdK`s z4w5=Fg|>z3^H*}?snc4F`B`f(oqQQjp}VxaZ%2E{{Z0~vpV#sy3+<%H#Yt2qO`o6F z+0ZY>Y5BRW61|gCpVankE3>Y2`!Ic8v7xh7Ov9r2WHty zpUTG5lcjj^ywX~_<&adAU-89Os-K!<;GcU?%a7V`CrN!0=;0kLfA=T`slAP*MhW_S z;_FyigN0sYZ6|Tz3DobXR^VqxT1(&G?56Wr<3*_@q>R@wG;6E2D8H_LoI%0VWG!E= zx2-hDB9&g??e~QFHd4i)bXuRI&vy+-qD$H6AM0(T^=%WWTcVa>HGN*WO-L~)Sa%ou{<4+S>`XkF-^cilloFD;eJt5!>GP#L;;7VBEgxIn zTB`9ikw#;)kF8ij3W-Uec_+2J&dW^t^*yC1-{EWtX-Yw=fq&^Ij14(xC0%k(C9#?p ze_3d5CHY@TrY=AA4JJzysK+ZUpB`-~Wkw~@YEv)1_hk#|pe~Ub{lYMuEoM^TnmF=) zjB#Vgf0-6<;7_@Sj;t#orQb^>yL>HQW2w0`qQ+i&`&*yC(>#?Vj3JZ9nJL6163)HQ z@W9;M6$2##SdL?B5moqi^5Fwc_C{zl_~4RA0A~cHFQX!sXku(f~%&IA~A`2 zkQe{!2`61V5l1!3>+>s4#T)p~V?jgPnMyZLr;*N8z>+=!S4E=dsy!ga1rqY53X=Fav zi@$Q&M52i)^tQ7;f3ZUnjce}3_Z(#+)fWb!C-?W_Cmbqdfz~PH4W0MlC+l4*ftokf z=NG+C{Qm;@bFkS5^($o6`8`y(g%_Vx=_mW0nL%O?eSVT+4;A1}NgkHBv2_By7_H%t z^mLW1`s}8{78-sXxk`gOB+xJ91-YS@Z<0Yl=~=kf^K+4|980FNn>2j%`jS%V8)>uy z*LyktJ3WcUPR5n6ODSn{nIy^`q2aSXIZKJBcT?@wxUFvEBwd`mn@oD@^DinI`n~#L z{79;cR6Q5x|CC|4wR-I=1q{oedbo7T4J&u0(cQkd5Abo3wmeU!5A8Jkt(#8L#2M+- zI{*Vt>XwwI!M}gJJ|E$dZs0$Sg_u~wSvpDy^w}N1kgk@L6mAJ*;iJz_tDHjq%{BZ{ zcfncOwl9?)>hKdR*-@&Ikwi_rG1O|RomAmyN>P6MbVupp?ezZ?=>5B^Y54pfPSTbS zd#Fzp+~J(V)y(xE&BW%F^Vu)=(H1|9ti~0xnNJ%1#xxIwJ4#abWa_8FkgX^?X{>oF z{T9%0pqHa`xx^lWg0!+4e&b^&>0DYmHL=3J{O%-~Ud^Q03VnWP`aX)RiNVFVDh~UV zPN&Oh_~R`drQo;w=y!b#X5D5leR!~++_1)SzGP2B|8Ga!G2e8Q1oNa6D)U0k7v6P{ z;@+iDX>)x;#Z`MK0Vd~Jj?!|rm)x)r6`I;h=i8*wjRG~_u+Un%7?oa>Uv}J1THGYR}|Vyic%uoz=Fzo_W=iJ-#4|uXLfRsk{4u9tNUuc-Y0wM zQpy3k@KDX$FSM6(T^U9G*5}FokX-Pm{~N}3zTILcovgNp_FPf(qXio&@>DuiyQj}z z56&P3#)8UYMDsE3PmO8b-)JZ0KR8G~KC1c6zilL+N&=%0Gfh#$me7L+1x;_M`RH+W zQu^j}%86C;QE@iX#^LGYh(T&{K7L9jU56)s7sh-(+D{qh)V$YE8|hKOA!_$j&1>FS zNzaaoH0Yf^ztr)Nf&VQAwUrQt*h)!X_mVDD%|E+lEtwqOOC9&20r{*l2WaL|HGg)j ztu$`SLA**+^Y8oOavYgOBUh^V3N3A=+iQ=I{Q-Tx^&w{9--D5L^HQh7GYPXud zdC@`|Ws^x$cj@!#_Yacbo}}huR5ns#$1JkMTE=#@l6qjq{le7zv`H4ySd)WA`TA!q zrPEWgskndJay7s3sj`F)K|q~8H+ z$;({JKfLsl^&GjL0E3 zmQtaOKA*!@5mR9(~;%fqkR&0R)^G0M7O+IQCJ)gn41U_i0z$Y+*vWC7L1;UPCznJqOaP<7DqlMBAF zUc*;X5Pm$EXg;w*^$Hr&N1u1Cyp)<@fN4wfAFS)MCG@T(?uPe%X37T(=sfoK+HvpM z>dgyiCI%hL`7buXfBFTv;KNiczp2Gn_WayZN{Z0(S+71aYmdcrc$_{TGJYYg?y2Rc zyMJZ9+byEsv(OLTerEPAizs9hZlz~@VjC6-^l6kne;VV&i~B!x*77$Se`Hy&=FvqZ zZggk9WtwmEsGKkE|K$ejltpwKoh7c(CpK`;eEKv(%O{u5X9Ly*(X9^xnxoGyJq^4ZpY@ zT9l9``v^(&9uE_`#FQJjvxi3DA(xzYUAI^77yeU_ zFQ3KkT1Xky_fU(fIPQCyOKtb3(P|uKa{kgx+=zbH@P214r1jih%6qKg?Sd>MuO^w~ zh>6y6vXJ^U$s$J+3^+%AdHwwc{^od^s`_9im3x#%PYW^pdX0%xe#IUVS}o zePo3*pgd&0(!~sV@eI#-mYGWLPw%7ZxA1_%nv+sm9-umSq$cMt*c~+RH*wPP(l;}S zJ9?0EaRjA%n@N|m4$(OrL2~}SeLc`5R{#{5b#rQP2TP`o_#eS~*stXdH5fe-^Uo z@e;j6kCpS*_*TRk3$bc8C;i^Ys2xtRUvK_mpC@J0JoJMHN4~Q3x%&LI^S{`qorewl z!Bw=pzbz*{9Da%9j(uw zANiF%m~m1tC~(EuFc43-_hg=?;aWUEIPjgVaJfh|aLSbPvD;43N?b2$^!>&hYvfQl z?1HI#KC&(wPSA7o{7k+c9hwCWo+v~4WioT|^eJisSh zz45_Ol~1f|K?EJ?jQ2nZAK7etW0dcq;Zw?gWS7csr3?7oA?JnM8sP>7dwbv;zjN={ z<}u;4yQ_wGv3t!1IpS>w@}pb7XBQUlq#14X--;|96+xdHY52_{`D}E?c3N3V!&mH= z&(^NlMpNtP^WRvwfq#_0Jlg&p%WDuuYun-Z|6t)2qpjg2H?aF}S#%q?nYBN3fsV>Sd9qhS1KY4`gP>zEfw&Q2PBsep!n{yPi`w&5pOol38n-M%e!{E3<$@bV?Q zZo8Sfy;c|H$KKpl^p#uBsV~_W+*YUDR`aespR=0>Luu(zzlJZEN`aZ8RLDsuk*=uiW{DQdN<$83kNm0WMA`5v9`F}vxZBn1h6^Xi<-x-JMW%FkYfn~@ih2L29RwS3ClckHZC zI)*yW$ES8{->{Z=qWyD-zG2(19rU)N{v)GnK|84Z04@LV=qq;jLl|9Xt>veDd&WlO zY^Rzy&&&BSC&LZ=7jd6h<6|C6N{*&e+i}^w@`?o=kESj&_4zebcF@y6od0tUzGmN# zM^NA@oWozgV*P7JQusbxJl?!yg{Q)(`VxJ9*tXE3fWK%e z+MP%md|J!@%)i5=oG7wEt(jA6n6h`OvX!&EWZZqYwt>lbH=ILLq zu+h5hMfs<$x0(ItaLO;{_r8qp#bWQXt9cPL_!}NdH@L?t4~(F%xApmHtwYK6B94wT zcNrIm!|yD<``mDo9f{vYmM69RhLcyA@Y^k{sA1u=o6PrcxIw|HuehtZa)+Im7)JFg zdhyvYw^_*9tyK6%pXW`tQZByplD`Y9^FE%G85({-&}|mvl1w&Nag~#Te-PmKEp~lO3Po?lTPoy-eoH1(ybXQ0;Wk@^2hOTF z8h(3^TWqUq3bh!4X8@OPurYaism~UDe#pUe1OH~c6~EwjogFBjMpZ6pc*hSHSxWCz z>WR0Za(?S(A(1LX;5;0CgMDb1O8jgMAAJ5gOS+j(_aIys9eGgT^OU9cg&awG#(kOGEK40=sGCAQMLjL+~uvt1ST#UDA1Fo{1&Y83_ z6qkD;?lLQk&ZgT~Qn}$ezF;n1kh=kI#hos*_8ut|fE$x(DQDUD&Z#8lEBIex!@A)f zemy=~+kcsL*|e9eu%PpMTwq5JWKx3zxUi|tur{kQ>13ilf2+b?1HZ5sYkA}0MHZgF zheC1V@GRmy3(#g#tGW98$xWH`1`m@xk}fd5RwkX_h)ZgZGpy?M17sPG%dzzdcDDq! zP?$cyqv<{a|FV&I7+LomOE|la&bGlXEw|IGj?DquJFzG)uxaXp)CikJ{>E>6=>zn7 zt%hF_n#1fSW{?KEW=7Nzw)@ilqI}iaIjrK`Lk9kyL-Ab5>nz^qW|9A34d3F-Y1X~_ z5lY1Enw&4!Or&q(kFXYNk5Uevbj$fc z2}kJk^=L65wg$C%<7t%c)3ydx?NXa;ubvS9O$&dR3VQC)rEjZF5qU~xjILJufau$% z2ED8ALB{C379P6teXG&So=sRWEffz7bgkZTe&P3P(@7#SMsFOUVI>|^qTHtmqA~h; zhx%l?K&VH(%|mti;NP{&JTH=Z)*}yXYn?GVy;5_wxtcqz@u?;LqZW#pKD4Mx&4aNJ z=Z@=)(UEssG388m3T)OU+Zf%^HAxrr(u3?z)S^GMP(1jn-s)%}2J!B+A+`lGMr*G( z&8invhd$mSGDf!uNEO7eF%{`ah=w8fpE~8oQ*rv<3UnyHCNoAaT;G5?O>aQgzXj|5 zUJHf|gZi~)?M_#qyv*B&jM4m(ax{Bx15zKpqBBO%npc%rW*rLjIU^Qnp?L5&TG_2F z`+2GuuQtVo`0yVANh#8~%U2&tNik=kH>5A^JTKKz$eIe_`@O~92{iiQ8MlZ`NNns^C zY2)c!oiTcyk0{McWkF$-@-k#>-wXiuP^r)oB{v-G|y{|QilP4b3xo#Xu#_0Vo z=TJz6Uw+|FO8EVY7XCEt_1_{cQ%|BqH#40v`cCXB(Khpku5w~mGDi2C)K%4 z!A-Z|SjYbCZ?*832B*>e z#E^m+^dXZqHbyV+aagRQtxFSsRwrZhiaB0c*N;@Cqzx^YtcBvi-)iS-el}+X8no=r z{uM^(ou+lgsA*1Avug)tgvLLMv9BbV1T?1p$B*cY(JiFrepbV#;-Tak(HOnRcXalA zu_cvHt<3&g3;*3P`}rf?#fbxGTYR9-7+vnv96J5RLU*ycD$5vsKk%BbYN0@#O9W=$ z7m5e}pr@6YO9x+n_1on&Jj)ne$M1C3n41D6ch2$sUs{k&gReiz&M2cIlPoneMwdFW zU-!A}7;=1KuQNt3*)WTGS8b#VujH8h7cKm4L-z@Re&hN~A^*>tL}TqUue_%Vy~QRSW+y zMTB^W_EoD;Nd?c0(Z?Em)~zn=L9S01=#0_l)`a;+JP{~pLPl0`Eff!mYo$eH+NnLG zb1dmW#^}jkW>VQn&2=vq>#~i}<4ex-JMBJ4pvUI6;(vmF)#>gkQJg)Kst$YTXN(S9 zHJgt0s-=50{QjYT*24dr>R0T`QgN=1i8?A8qhD#d`UX~*MOzNF)ET3lTFj*CCwu6c zE(y;1Qwzm|zvv9pCfS$D3sgt5%Fh_BJ)fU7p|L<6My>ZVM$bPGM8anGiMp(U<5_a> zzqG%}izqbBXRBv|!LsC)S%S{?d>(+)d6lMz8UnsoQ+L zA7zSAS|}d;x4xNNJNvG3E?EwF;%AIb$!SYg+pg+VHqUg% zXcM7uvDoAO6#8+;OlM+*&WJdzD>J$|bx@j+G5Xfej;!|<6RKN&qxg4P_-n(dB~_`0 zb!m!z#E~(2OI(2%Sh7E*Op5n2Mk{X25PPnkihAKXu0F zE@4)>W?e?n*wsgL|62=3bfz=#?{L)epiQgpWE-O!yK2e*hXc)AdRsI`|4gxAnho7( z)NBj!zqIf-4JPZG(4!xw^lJT8(HOn_VqKQD(2tTk#bq0#fB&fA=jcD1rf#V%3dMte z(p%5XrN~9cvzyu{G&V-xebJ0|tT3ab#V5r7p@kw-)3q~2<*q5zDZZ9yj83^fn;fFu zbw1}G9xz6CELBtNb$13GOerNAYT@r2Ud(hMRe*wmOz(@v=yTR7Vq(M?x|dd8G)9jf zGl#UfC3Ss-&M61~4E{xL#8uL?(JZ>v%~mue)Y4{5LK9VJdT|KkMrB zUU$J)p)*F0i|gPw?oJSyZW<*Tqa7v)H1YM4?6Iy-8W+<-@xVX_Sxlp#DXVmv-F1D9 z(JRy+va-ZE)Xm#mG)DWoccZb@gjYI~-Z7#+_;>xnte+TPVj3-WQi{gtEfGVIi!S0Qwbtp%%&D+u6Yr(MLJ01?t?53n4Ar8zKeIiky_CD?W4t~gMY>bY# zDfFA#eg>V2=5<9{C?5Qc9(3Wd?nko%ooQ$@W{fU5qm<6M&2-8i+*fCe78Yyo>CR4e zqUUW@%oyEtrJ1hWv>tT*z9s%G@BgivJzM5iFkGM}C#(7XjsEY3!1n?zTAu2=Y|Nm> z#^^0e1e*SRx}Q4ncw=Mqe9x+4m6J1RaqHiH^F4*)!Qbo5v2#i2_s~yRF+0l`ea+WW z?3Fx+Qf8m@{hwNZ=?rGjXy7fs(d`SfjM3w>L&cxtCeXgN?xHbz;?i0={|BFSCZYZ4 zuUh!Kh7Pn-v@n@K!Bt&EV{`>gL-C9AYw1^ag`=uLOq4)Jx_T^a9eT9Gk&LiBmDU~nJ$)^)dN zj6QDLS*)|WKUKK(QfHure`px~;G+0{ot;}qR6!WWH&9u$R=ZfDYz1v*l5Q@UWuEh4 zH|rr5%rYM|)Do!-vSdxmSxZ8Ft2E7r@T^KmAzcv4)j38lp%f};Wo)!|LsD!Zte~R# z&e^lsMIAe^@SFd9Gau~Z;mmKEE+>I$X`sx*$7;_&>3NtYA6}6QcW$0&L`i8utJS_IMh$%tG z;l?uy<>CDDM6u3egN^Mm^cRz-w(gm`t7UTN^9;o5SJI&>-dE z(DXNAeS9`V4M$NPzB`yF@45I;EH1QCFO#W*zu0v?5QLF5SeLD%Jp9&B1eJj`w5DWy z3l9_a{#(e2E{1*9TuM4;=X|DUF8HJq-09TG!!N5=3g#0wa68NCKbib7F_x1h?_Bgs z{90c?dDvlS6~A>Hh19Y%%EPE`#G6o2)reX~xzA#p_fb#m#7 zTO=PBe3n3~ab`GRfE{+h8q77udaOZL%`Q(PEFAUP`L{_v4q_j|YBiIa95CTgYKV*z z-hsSU|AxLTlhUspi-KbFBf^YYi~h8el!<>`(SS z_PXGnMF8IItX^o;}j!IE9fuau-4WVcPMIR{2J<$Y;K2S7- r2D5pziH0bO&rn70iNa45d!pVGg`X()M7?JS8b6mY^QUU|jL`iA2?CUy delta 1494 zcmWmE_d^T-07vmI&BJ9xgoc^fCaaR9jAV!Gk-fK!kh8Osz4u;cME2f$Z`pfizV8p8 z_b0ruPj?yDJlgz~kPw?_N-`OeOx8)JEIUm$na<__R~t)!tL^H| zmU?GwxA(T33$}abXlR-8F~-i#oDpef$%wT7+9zdTn!THCV33c$pE)(n9uAh&G{@vv z%LEtW&}fHgH^U5JQPEZwpG(bR4Cc6aBeEhJvcm)?n|IBFmQH4%v@)Ix34L0v?n9%4`*4bTvc&=^h76tQTA=4gR9v_vbkMjNz6JG4g! zbVMh_qcggoE4raOdY~tIp*Q-VFZ!WB24EltVK9bZD25>c!!ZIQF$#$ojWHODB#eU@ z<1qmfF$t3~1yhlXX_$@~n2A}KjX6lcT%=+i=3@aCVi6W&36^3RmSY80Vii_n4c1~E z)?))UViPuF3$|h#wqpl&VizpfjXl_leMrN896&k_;t&qw2#(?yj^hMQ;uKEf49?;l z&f@|u;u0?73a;WBuHy!7;udb>4({R}?&AR-;t?L>37+B^GVmNP@Di`^8gK9x@9-WU z@DZQz8DH=f-|!tj@DrK%h2QvtzxX#PF5Zv~#ahXt*eJG&ono&zD2|Fz$*N>ivMVOV zNy(w)RB|b~6=x-nl2>t2@+tWhSEYbbP${IiDej7g;;9r?iYP^uVoGtPgyN-?R7xqO zl`=|MrJUle_$a=LpW?3sD1l0OrGgTqR8%S{m6a+=RV7#nQ9_k!N|;hz30G<;5lT%Z RQi)P(DYXshti%v{<3DtnTj&4) diff --git a/locenice/tools/apple_tree.mjs b/locenice/tools/apple_tree.mjs new file mode 100644 index 0000000..a3c8bdc --- /dev/null +++ b/locenice/tools/apple_tree.mjs @@ -0,0 +1,193 @@ +/** + * Mladá jabloň ze školky (kmínek s vodičem ~1,8 m, pár postranních větví, bambusová tyčka) + * v zahradě 3 m od hranice parcely v jihozápadním rohu. Je součástí vrstvy „Celý pozemek“. + * + * node tools/apple_tree.mjs [models/dum.glb] + * + * Skript přepíše uzly Site_AppleTree_* (opakované spuštění strom nahradí, nepřidá druhý). + * Polohu určuje TREE.corner (roh parcely) a TREE.fromEdge (vzdálenost od obou hran rohu). + * Po změně modelu zvyšte ?v= u "model" v data/app-config.json. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parseGLB, encodeGLB } from '../js/model-io.js'; + +const TREE = { + corner: [47.42, 4.04], // jihozápadní roh parcely (konec zahrady u západní hranice), x / z + fromEdge: 3.0, // od jižní i západní hranice + height: 1.8, // vodič nad terénem + trunk: [0.024, 0.008], // průměr kmínku dole / nahoře + crownFrom: 0.9, // první větve + branches: 7, + seed: 7, +}; +const BARK = '#75604d', LEAF = '#6a8c38', BAMBOO = '#c8b27a'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const file = process.argv[2] || path.join(here, '..', 'models', 'dum.glb'); +const { gltf, binary } = parseGLB(new Uint8Array(fs.readFileSync(file))); +const views = gltf.bufferViews, viewBytes = views.map(v => binary.subarray(v.byteOffset || 0, (v.byteOffset || 0) + v.byteLength)); +const COMPS = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 }; +function read(i) { + const a = gltf.accessors[i], v = views[a.bufferView], n = COMPS[a.type], size = { 5126: 4, 5125: 4, 5123: 2, 5121: 1 }[a.componentType]; + const dv = new DataView(viewBytes[a.bufferView].buffer, viewBytes[a.bufferView].byteOffset + (a.byteOffset || 0)), stride = v.byteStride || n * size, out = []; + for (let k = 0; k < a.count; k++) { + const row = []; + for (let c = 0; c < n; c++) { const o = k * stride + c * size; row.push(a.componentType === 5126 ? dv.getFloat32(o, true) : a.componentType === 5125 ? dv.getUint32(o, true) : a.componentType === 5123 ? dv.getUint16(o, true) : dv.getUint8(o)); } + out.push(n === 1 ? row[0] : row); + } + return out; +} +const assert = (ok, msg) => { if (!ok) throw Error(msg); }; + +// --- position: 3 m inside both parcel edges at the corner --------------------------------------- +function outline() { // centreline of Site_Cadastral_outline, simplified to corners + const n = gltf.nodes.find(q => q.name === 'Site_Cadastral_outline'), p = gltf.meshes[n.mesh].primitives[0], pos = read(p.attributes.POSITION), idx = read(p.indices); + const mid = (a, b) => [(a[0] + b[0]) / 2, (a[2] + b[2]) / 2], key = q => q[0].toFixed(3) + ',' + q[1].toFixed(3); + const segs = []; + for (let t = 0; t < idx.length; t += 6) segs.push([mid(pos[idx[t]], pos[idx[t + 5]]), mid(pos[idx[t + 1]], pos[idx[t + 2]])]); + const next = new Map(segs.map(s => [key(s[0]), s])), chain = [segs[0][0]]; + for (let s = segs[0], g = 0; g <= segs.length; g++) { s = next.get(key(s[1])); if (!s || s === segs[0]) break; chain.push(s[0]); } + const simplify = (pts, tol) => { + let best = -1, at = 0; const [a, b] = [pts[0], pts[pts.length - 1]], L = Math.hypot(b[0] - a[0], b[1] - a[1]); + for (let i = 1; i < pts.length - 1; i++) { const d = Math.abs((b[1] - a[1]) * (pts[i][0] - a[0]) - (b[0] - a[0]) * (pts[i][1] - a[1])) / L; if (d > best) { best = d; at = i; } } + return best <= tol ? [a, b] : [...simplify(pts.slice(0, at + 1), tol).slice(0, -1), ...simplify(pts.slice(at), tol)]; + }; + const far = chain.reduce((b, p, i) => Math.hypot(p[0] - chain[0][0], p[1] - chain[0][1]) > Math.hypot(chain[b][0] - chain[0][0], chain[b][1] - chain[0][1]) ? i : b, 0); + return [...simplify(chain.slice(0, far + 1), 0.05).slice(0, -1), ...simplify([...chain.slice(far), chain[0]], 0.05).slice(0, -1)]; +} +const poly = outline(); +const ci = poly.reduce((b, p, i) => Math.hypot(p[0] - TREE.corner[0], p[1] - TREE.corner[1]) < Math.hypot(poly[b][0] - TREE.corner[0], poly[b][1] - TREE.corner[1]) ? i : b, 0); +assert(Math.hypot(poly[ci][0] - TREE.corner[0], poly[ci][1] - TREE.corner[1]) < 0.5, 'TREE.corner is not a parcel corner'); +const area = poly.reduce((s, p, i) => s + p[0] * poly[(i + 1) % poly.length][1] - poly[(i + 1) % poly.length][0] * p[1], 0); +// both edges at the corner, moved inward by fromEdge; their intersection is the tree +const edgeLine = (a, b) => { + const L = Math.hypot(b[0] - a[0], b[1] - a[1]), dir = [(b[0] - a[0]) / L, (b[1] - a[1]) / L], inward = area > 0 ? [-dir[1], dir[0]] : [dir[1], -dir[0]]; + return { p: [a[0] + inward[0] * TREE.fromEdge, a[1] + inward[1] * TREE.fromEdge], dir }; +}; +const prev = poly[(ci + poly.length - 1) % poly.length], C = poly[ci], next = poly[(ci + 1) % poly.length]; +const l1 = edgeLine(prev, C), l2 = edgeLine(C, next), det = l1.dir[0] * l2.dir[1] - l1.dir[1] * l2.dir[0]; +const t = ((l2.p[0] - l1.p[0]) * l2.dir[1] - (l2.p[1] - l1.p[1]) * l2.dir[0]) / det; +const X = l1.p[0] + l1.dir[0] * t, Z = l1.p[1] + l1.dir[1] * t; + +const ground = gltf.nodes.filter(n => n.mesh !== undefined && /^Site_Terrain_/.test(n.name || '')).flatMap(n => gltf.meshes[n.mesh].primitives.map(p => ({ pos: read(p.attributes.POSITION), idx: read(p.indices) }))); +function groundY(x, z) { + let best = -Infinity; + for (const s of ground) for (let k = 0; k < s.idx.length; k += 3) { + const a = s.pos[s.idx[k]], b = s.pos[s.idx[k + 1]], c = s.pos[s.idx[k + 2]]; + const d = (b[2] - c[2]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[2] - c[2]); + if (Math.abs(d) < 1e-12) continue; + const w1 = ((b[2] - c[2]) * (x - c[0]) + (c[0] - b[0]) * (z - c[2])) / d, w2 = ((c[2] - a[2]) * (x - c[0]) + (a[0] - c[0]) * (z - c[2])) / d, w3 = 1 - w1 - w2; + if (w1 >= -1e-6 && w2 >= -1e-6 && w3 >= -1e-6) best = Math.max(best, w1 * a[1] + w2 * b[1] + w3 * c[1]); + } + assert(best > -Infinity, 'no terrain under the tree'); + return best; +} +const G = groundY(X, Z); + +// --- geometry ----------------------------------------------------------------------------------- +let seed = TREE.seed; +const rnd = () => { seed = (seed + 0x6d2b79f5) | 0; let r = Math.imul(seed ^ (seed >>> 15), 1 | seed); r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r; return ((r ^ (r >>> 14)) >>> 0) / 4294967296; }; +const add = (a, b) => a.map((v, i) => v + b[i]), sub = (a, b) => a.map((v, i) => v - b[i]), mul = (a, s) => a.map(v => v * s); +const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; +const unit = a => mul(a, 1 / Math.hypot(...a)); +const mesh = () => ({ pos: [], nor: [], idx: [] }); +const wood = mesh(), leaves = mesh(), stake = mesh(); +// tapered tube along a polyline +function tube(m, pts, r0, r1, sides = 6) { + const k0 = m.pos.length; + pts.forEach((p, i) => { + const d = unit(sub(pts[Math.min(i + 1, pts.length - 1)], pts[Math.max(i - 1, 0)])); + const a = unit(cross(d, Math.abs(d[1]) > 0.9 ? [1, 0, 0] : [0, 1, 0])), b = cross(d, a), r = r0 + (r1 - r0) * i / (pts.length - 1); + for (let s = 0; s < sides; s++) { const ang = 2 * Math.PI * s / sides, n = add(mul(a, Math.cos(ang)), mul(b, Math.sin(ang))); m.pos.push(add(p, mul(n, r))); m.nor.push(n); } + if (i) for (let s = 0; s < sides; s++) { const q = k0 + (i - 1) * sides, s1 = (s + 1) % sides; m.idx.push(q + s, q + s1, q + sides + s1, q + s, q + sides + s1, q + sides + s); } + }); +} +// leaf: pointed oval as 4 vertices (stalk, two sides, tip), double-sided material +function leaf(m, at, dir, size) { + const side = unit(cross(dir, Math.abs(dir[1]) > 0.95 ? [1, 0, 0] : [0, 1, 0])), n = unit(cross(side, dir)); + const L = size, W = size * 0.45, k = m.pos.length; + m.pos.push(at, add(add(at, mul(dir, L * 0.45)), mul(side, W / 2)), add(at, mul(dir, L)), add(add(at, mul(dir, L * 0.45)), mul(side, -W / 2))); + m.nor.push(n, n, n, n); + m.idx.push(k, k + 1, k + 2, k, k + 2, k + 3); +} +function leavesAlong(pts, every, size) { + for (let i = 1; i < pts.length; i++) { + const a = pts[i - 1], b = pts[i], len = Math.hypot(...sub(b, a)), d = unit(sub(b, a)); + for (let s = 0; s < len; s += every) { + const p = add(a, mul(d, s)), ang0 = rnd() * 2 * Math.PI; + for (const ang of [ang0, ang0 + Math.PI * (0.8 + 0.4 * rnd())]) { // dva listy v uzlu + const out = unit(add(add(mul(unit(cross(d, [0, 1, 0.001])), Math.cos(ang)), mul(unit(cross(d, unit(cross(d, [0, 1, 0.001])))), Math.sin(ang))), [0, 0.6, 0])); + leaf(leaves, p, unit(add(out, mul(d, 0.5))), size * (0.8 + 0.4 * rnd())); + } + } + } +} +// leader with a slight natural sway +const leader = Array.from({ length: 10 }, (_, i) => { const h = TREE.height * i / 9; return [X + 0.03 * Math.sin(h * 2.1) * h / TREE.height, G - 0.05 + h, Z + 0.025 * Math.sin(h * 1.4 + 1) * h / TREE.height]; }); +tube(wood, leader, TREE.trunk[0] / 2, TREE.trunk[1] / 2); +leavesAlong(leader.filter(p => p[1] - G > TREE.crownFrom + 0.1), 0.035, 0.085); +for (let b = 0; b < TREE.branches; b++) { + const h = TREE.crownFrom + (TREE.height - 0.25 - TREE.crownFrom) * b / (TREE.branches - 1), ang = b * 2.39996 + rnd() * 0.4; + const len = 0.42 - 0.2 * b / TREE.branches + rnd() * 0.08, rise = 0.55 + rnd() * 0.35; + const start = leader[Math.round((h / TREE.height) * 9)], dir = unit([Math.cos(ang), rise, Math.sin(ang)]); + const pts = Array.from({ length: 5 }, (_, i) => add(start, add(mul(dir, len * i / 4), [0, 0.04 * (i / 4) ** 2, 0]))); + tube(wood, pts, 0.0055, 0.0025, 5); + leavesAlong(pts.slice(1), 0.032, 0.08); +} +// bamboo stake next to the trunk with two ties +const sp = [X + 0.035, 0, Z - 0.02]; +tube(stake, [[sp[0], G - 0.25, sp[2]], [sp[0], G + 1.35, sp[2]]], 0.006, 0.005, 6); +for (const h of [0.6, 1.2]) tube(stake, [[X, G + h, Z], [sp[0], G + h, sp[2]]], 0.004, 0.004, 4); + +// --- write ------------------------------------------------------------------------------------ +const srgbToLinear = h => [1, 3, 5].map(i => { const c = parseInt(h.slice(i, i + 2), 16) / 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }); +function material(name, hex, roughness) { + let i = gltf.materials.findIndex(m => m.name === name); + if (i < 0) { gltf.materials.push({ name }); i = gltf.materials.length - 1; } + gltf.materials[i] = { name, doubleSided: true, pbrMetallicRoughness: { baseColorFactor: [...srgbToLinear(hex), 1], metallicFactor: 0, roughnessFactor: roughness }, extras: { loceniceId: 'material-' + i } }; + return i; +} +function accessor(data, type, componentType, target) { + const flat = data.flat(), arr = componentType === 5126 ? new Float32Array(flat) : new Uint16Array(flat); + views.push({ buffer: 0, byteLength: arr.byteLength, target }); viewBytes.push(new Uint8Array(arr.buffer)); + const a = { bufferView: views.length - 1, componentType, count: data.length, type }; + if (type === 'VEC3') { a.min = [0, 1, 2].map(c => Math.min(...data.map(v => Math.fround(v[c])))); a.max = [0, 1, 2].map(c => Math.max(...data.map(v => Math.fround(v[c])))); } + gltf.accessors.push(a); return gltf.accessors.length - 1; +} +const parts = [ + ['wood', wood, material('Strom / kura jablone', BARK, 0.9)], + ['leaves', leaves, material('Strom / list jablone', LEAF, 0.7)], + ['stake', stake, material('Strom / bambusova tycka', BAMBOO, 0.6)], +]; +const existing = gltf.nodes.map((n, i) => n.extras?.prop === 'apple_tree' ? i : -1).filter(i => i >= 0); +const root = gltf.nodes[0].children; +parts.forEach(([part, m, mat], k) => { + assert(m.pos.length < 65536, 'too many vertices for 16-bit indices'); + const prim = { attributes: { POSITION: accessor(m.pos, 'VEC3', 5126, 34962), NORMAL: accessor(m.nor, 'VEC3', 5126, 34962) }, indices: accessor(m.idx, 'SCALAR', 5123, 34963), material: mat }; + const name = 'Site_AppleTree_' + part; + let ni = existing[k]; + if (ni === undefined) { gltf.meshes.push({ name, primitives: [prim] }); gltf.nodes.push({ mesh: gltf.meshes.length - 1 }); ni = gltf.nodes.length - 1; root.push(ni); } + else gltf.meshes[gltf.nodes[ni].mesh] = { name, primitives: [prim] }; + gltf.nodes[ni].name = name; + gltf.nodes[ni].extras = { siteLayer: 'parcel', castShadow: true, prop: 'apple_tree', loceniceId: 'node-' + ni }; +}); +// drop accessors / bufferViews nothing refers to any more, then repack the BIN chunk +const usedAcc = new Set(gltf.meshes.flatMap(m => m.primitives.flatMap(p => [...Object.values(p.attributes), p.indices].filter(v => v !== undefined)))); +const accMap = new Map(); gltf.accessors = gltf.accessors.filter((a, i) => usedAcc.has(i) && accMap.set(i, accMap.size)); +for (const m of gltf.meshes) for (const p of m.primitives) { for (const k in p.attributes) p.attributes[k] = accMap.get(p.attributes[k]); if (p.indices !== undefined) p.indices = accMap.get(p.indices); } +const usedView = new Set([...gltf.accessors.map(a => a.bufferView), ...gltf.images.map(i => i.bufferView)].filter(v => v !== undefined)); +const viewMap = new Map(), order = views.map((v, i) => i).filter(i => usedView.has(i)); +order.forEach(i => viewMap.set(i, viewMap.size)); +for (const a of gltf.accessors) a.bufferView = viewMap.get(a.bufferView); +for (const im of gltf.images) if (im.bufferView !== undefined) im.bufferView = viewMap.get(im.bufferView); +let total = 0; const place = order.map(i => { total = (total + 3) & ~3; const off = total; total += viewBytes[i].length; return [i, off]; }); +const bin = new Uint8Array((total + 3) & ~3); +gltf.bufferViews = place.map(([i, off]) => { bin.set(viewBytes[i], off); return { ...views[i], byteOffset: off, byteLength: viewBytes[i].length }; }); +gltf.buffers[0].byteLength = bin.length; +fs.writeFileSync(file, encodeGLB(gltf, bin)); + +const dEdge = (a, b) => Math.abs((b[0] - a[0]) * (Z - a[1]) - (b[1] - a[1]) * (X - a[0])) / Math.hypot(b[0] - a[0], b[1] - a[1]); +console.log(`jabloň: x ${X.toFixed(2)} z ${Z.toFixed(2)}, terén ${G.toFixed(2)} m, od hran ${dEdge(prev, C).toFixed(2)} / ${dEdge(C, next).toFixed(2)} m, výška ${TREE.height} m`); +console.log(`trojúhelníky: kmen+větve ${wood.idx.length / 3}, listy ${leaves.idx.length / 3}, tyčka ${stake.idx.length / 3}; zapsáno ${file} (${fs.statSync(file).size} B)`);