├── rust ├── .gitignore ├── rustfmt.toml ├── src │ ├── lib.rs │ ├── raycasting │ │ ├── mod.rs │ │ ├── util.rs │ │ ├── prepare.rs │ │ ├── js_api.rs │ │ ├── vision_angle.rs │ │ ├── postprocessing.rs │ │ ├── raycasting.rs │ │ └── types.rs │ ├── profiling_main.rs │ ├── ptr_indexed_hash_set.rs │ ├── tests │ │ └── mod.rs │ ├── geometry.rs │ └── serialization.rs ├── tests │ ├── limited_vision_angle.ascii85 │ ├── 32-limited_angle_wall_to_the_right_with_start_visible.ascii85 │ ├── limited_vision_angle_over_180_hidden_overflowing_wall.ascii85 │ ├── 31-limited_angle_wall_to_the_right_with_end_visible.ascii85 │ ├── 25-limited_vision_angle_overflow_both_visible.ascii85 │ ├── 6-zero_length_walls.ascii85 │ ├── limited_vision_angle_overflow_end_hidden.ascii85 │ ├── limited_vision_angle_overflow_start_hidden.ascii85 │ ├── 30-tiny_vision_angle.ascii85 │ ├── 14-overflow_wall_not_overflowing_in_fov.ascii85 │ ├── 4-directional_walls.ascii85 │ ├── 15-overflow_wall_no_point_seen_wall_far.ascii85 │ ├── 15-overflow_wall_both_points_seen.ascii85 │ ├── 15-overflow_wall_bottom_point_seen.ascii85 │ ├── 15-overflow_wall_top_point_seen.ascii85 │ ├── 19-origin_on_wall_endpoint.ascii85 │ ├── 15-overflow_wall_no_point_seen_wall_close.ascii85 │ ├── 29-minimally_intersecting_walls.ascii85 │ ├── 17-old_closest_wall_paralell_to_ray_line.ascii85 │ ├── 27-almost_vertical_line_with_negative_incline.ascii85 │ ├── 5-t_junction.ascii85 │ └── zero_width_walls.ascii85 ├── Cargo.toml └── Cargo.lock ├── .gitignore ├── install_dev_dependencies.sh ├── .editorconfig ├── js ├── polygon.js ├── fog.js ├── scene_builder.js └── raycasting.js ├── LICENSE ├── module.json ├── README.md └── CHANGELOG.md /rust/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /rust/rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifact/ 2 | /foundry*.js 3 | .idea/ 4 | .vscode/ 5 | *.txt 6 | perf.data* 7 | -------------------------------------------------------------------------------- /install_dev_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cargo install cargo-watch 3 | cargo install wasm-pack 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = tab 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod geometry; 2 | mod ptr_indexed_hash_set; 3 | mod raycasting; 4 | mod serialization; 5 | 6 | use wasm_bindgen::prelude::*; 7 | 8 | #[wasm_bindgen(start)] 9 | pub fn main() { 10 | std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 11 | } 12 | -------------------------------------------------------------------------------- /rust/src/raycasting/mod.rs: -------------------------------------------------------------------------------- 1 | mod js_api; 2 | mod postprocessing; 3 | mod prepare; 4 | mod raycasting; 5 | mod types; 6 | mod util; 7 | mod vision_angle; 8 | 9 | pub use raycasting::compute_polygon; 10 | pub use types::{ 11 | Cache, DoorState, DoorType, PolygonType, TileCache, VisionAngle, WallBase, WallDirection, 12 | WallHeight, WallSenseType, 13 | }; 14 | -------------------------------------------------------------------------------- /rust/tests/limited_vision_angle.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m47N`dc_!+>Jh^J$C&+Y2b(H;\g>#*'3d--rUt&9s[]e@?([>m8\ch,Grp+\O,s__GC6%UP-MQcJfQ%N+_O1[gXc%;MiE;"APJIf4m@ObU'q4DaGW(hAC`!R'G-u72L"oLa,F0Ht"uWSC;-$Up(s[>jp\<$6W"Pn=*4"K.*2K&>[j%p3d%Zi38s~> 2 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lichtgeschwindigkeit" 3 | version = "1.4.9" 4 | authors = ["Manuel Vögele "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [[bin]] 11 | name = "lichtgeschwindigkeit-profiling" 12 | path = "src/profiling_main.rs" 13 | 14 | [profile.release] 15 | lto = true 16 | #debug = true 17 | 18 | 19 | [dependencies] 20 | ascii85 = "*" 21 | console_error_panic_hook = "*" 22 | js-sys = "*" 23 | nom = "*" 24 | partial-min-max = "*" 25 | rustc-hash = "*" 26 | wasm-bindgen = "*" 27 | yazi = "*" 28 | -------------------------------------------------------------------------------- /rust/tests/32-limited_angle_wall_to_the_right_with_start_visible.ascii85: -------------------------------------------------------------------------------- 1 | <~"*iOZ:gHbX;&BLed[$Aj(G]:)['a&SRU]`2e2nhi3EA.@1)rZ=)T4*>Qp3B0(=rO\f;_:D-e>Y]aLapPIampuV_He5KoD(bl-_8QNud)@gsj$f'lf-JN?a]A=!Hts=QARmV[1$oKU"Ub7kGmop0fi/6X*45p#%An9C\CHM0UDWMXL$*>REfI4h+^LoH?pm1,J@!/:fa"q'X.ke]h9ac)pq?aBA%OJF-qQK-4U,arZaCKG8jqe<3@?P*q5$p9^A:&J=R[Bl!@^#>km\:6Se_0FtOoA/-B.GHRO)`2.5uFb\kBO^_Me(Qrkh4*'=J9H4-2i2?G4M/)$)'"MBT,`[:UDKKZt@I:V0a?Xdm^POj!GL5M]YV#G$hh/BZas$)M7pS' 2 | -------------------------------------------------------------------------------- /rust/tests/limited_vision_angle_over_180_hidden_overflowing_wall.ascii85: -------------------------------------------------------------------------------- 1 | <~"*iOZ;Jes'@2.R7U`39pe+,(mV;2`SG8Cj#W&ImoTkY'+m^@r*)TXmAUiHa>T4Qe`f49C(!"J';Dmgi18;<0D]8k,LN3;7?P-\:2'N[`9s+T&?!hD9b'om`h.7K,FZ<>%JZ.ZG6sZ[Y>dFO;=RDSfA()9q=qEM6Qk()\*Q&D`RK_9PpJ%cl"[#qrThi.o^oBo2UX.*!P>cXDjd;M+He'A>T"l4Z\%ImZpZPi0Y6JJ^h\SAbYP;gnnG?nb@(;KAog@Zd\gH$!bZJ3aGS>1ZTkskdV[\P/MD"Dl)=s1rb[I&E?@"gT]M4%58+W'u>@cIc4\u7;>f?*B=/6eDliH7lR`is$C#C`iIE[nP1tTSsE65PAoZ)9/)=Sa*1$\l:!TI`3g7YH6%WpaVk/4$\^mmP8:+T*E3^`YX(6/FENq@>_jR/Sn'SU,K=%L&8C<;HiEBWH.K>T.'9[.12q@8MMaSEP0d:\La5#/lk;Sam*J;3]/YPp9UZ;j9g[kBG\>hj86o8*J?*:\+AUO4:dMRn*h?ck.m>1KMEAH%fZX-DZkn:!P&(up=gqWj7IF92b83Z2YNZ\'iM 2 | -------------------------------------------------------------------------------- /rust/tests/31-limited_angle_wall_to_the_right_with_end_visible.ascii85: -------------------------------------------------------------------------------- 1 | <~"*iOb:gHUj@2M1k7_86/b\;YFMaD8mLq5F$Qc/G*1D*Ir_%LQUUnog]SZRj)FP,0)__"NBc3t_0D/I0/bWqXY>-&K.R'EX<7l`if\Tu"p>),5.Q?%+>+<*^3R7gYcY@mOoLj3q1;[#lFWTAJAq05PT_r)=\]]1K:d.3Ip-qQ'f#g_ss0`=CH0C2T4:O@.=2nn%1O(l^k"<.p%!n_2`FRPPr8h29r3:%1c]+RQGBY#2XK(G^:DQ3&WO_QZeJuZM,-m"i/*Jd2(urkV\7:O@)pDOfSO7LRkYD:EX"dFQa8oRL3[6Ae)6QYtB5b5DJQA6+OO98NkCWqblk;hWGZ9W:5/eCb+nWu4-35$,K5FeM1nnUr"]Jc_5^g4:AX7a'MT99Q7m_-GlL"4j:.;InGI/+P#g*82.J*3HI^QGa`l?5\idkQp?Lf:Hn;kr\IMq-[XqT\e",olnKc)I;Foj!QcqZCL!Ip2sdF)e2CuAdQLIE!:pFXXMekX#FXtd*F!W@"t-):J23"Nfs79flNFj$39N--!GtB^oR_+e@n&\gYDX@JA!bBa5qLHhOlt[m3Vh&58mF$5lb4q5+Eir!+t\u'bRL2?O,i!\7*BTEf57iM)j?tP>1]lhm?/t:/Ld9kug#H6>Ct$O$?*M?#4I;jZe'$4f(_Cm2(eZh(,V(p8=&L>P[X0k;4b1U@S1Chd7LpGh;~> 2 | -------------------------------------------------------------------------------- /rust/tests/25-limited_vision_angle_overflow_both_visible.ascii85: -------------------------------------------------------------------------------- 1 | <~"*iOZ;Jes'@2.R?JX)Z8U*+%NI3=\/,>C!9#U?XlO#3iWW%^pp%9sf\$(>C3\iY-)h5h%V=)*pM*GSVG(tG!C@EDd,Lc(:kbG%T0k%*T-B\M_'ON"f+0GV%cN5NDu"g2\&3EBW-]gnT\"YW*9]M#(]D%Zi$PG-O[;RC63:d3R)XNZWT2Il*M??Zt'hYE]$7D"EqdkZtq#?nrkMCInoQ%@,M1&&Kch[JQ"$po2c845a&o***U)WILDQ/,3KI?V@?4g#0D.QeJ9]eGg`^\ju"[cT]^'FQ!&BbliEcU\P(F+.,:M(pa17q.EV'3:4b7_CqS\q*t2'BYXK$r*K8nYtMlj-,'l;itgHrpO94 2 | -------------------------------------------------------------------------------- /rust/src/profiling_main.rs: -------------------------------------------------------------------------------- 1 | mod geometry; 2 | mod ptr_indexed_hash_set; 3 | mod raycasting; 4 | mod serialization; 5 | #[cfg(test)] 6 | mod tests; 7 | 8 | use raycasting::*; 9 | use serialization::*; 10 | 11 | use std::fs::read_to_string; 12 | 13 | fn main() { 14 | let data = deserialize_ascii85::(&read_to_string("data.txt").unwrap()); 15 | let cache = Cache::build(data.walls, TileCache::from_roofs(data.roofs)); 16 | let mut sum = 0; 17 | let mut los = None; 18 | for _i in 0..1 { 19 | los = Some(compute_polygon( 20 | &cache, 21 | data.origin, 22 | data.height, 23 | data.radius, 24 | data.distance, 25 | data.density, 26 | VisionAngle::from_rotation_and_angle(data.rotation, data.angle, data.origin), 27 | PolygonType::SIGHT, 28 | None, 29 | )); 30 | sum += los.as_ref().unwrap().0.len(); 31 | } 32 | 33 | println!("{:#?}, {}", los.unwrap().0.len(), sum); 34 | } 35 | -------------------------------------------------------------------------------- /rust/tests/6-zero_length_walls.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m5RQ9T%];&%%->BJ;e?)eHaTN%kGFE)92Jq!iZLnKBQ@&>><64kXZUi]HZK;,I`gDZ8T'2=Zi`(;5Dh%XiCFO8-Q&6t^Y]R.tQpO)-M''RiSgjWU[3Ucr-pRLDlhH^&"eMq"?Yu17+Acp/f36p]'[.!LMTm=0WGZI%Y,T5XA6Y._pmhe/]%6YB!ENC1HR6(G3GA9%]`gc0D1`/o0*6QY>-l$UjKTj0B7p0GVtK)YHTR=918S]).Q-q@P)AX5-Z#+*G!*_m0d4FE[TS6VmNdohi?4qRC'0Cs4b[((2?<8(g)dH-mXQ)=X_NhZ]Tt;RI07*uu#0,_VpcS1YC,j`/E*Z;HZ!N1oe75T!CgAkam0hr>h85Tq`dt^T2+t-2hf%9,kq;k]_ttN2OE[(5+Tj`Yoo\XJaT\^QD:q^EB6CI*)6.>FbEJ)D*e*;:rjB95e;`k1e6MQZiq)#^C]<)V"p[$P8d2b`lX4lOd^-gMtX6QUlop'a%Wj^EeE27l#X7+G##,cT0~> 2 | -------------------------------------------------------------------------------- /js/polygon.js: -------------------------------------------------------------------------------- 1 | let originalContains = undefined; 2 | 3 | Hooks.on("init", () => { 4 | originalContains = PIXI.Polygon.prototype.contains; 5 | PIXI.Polygon.prototype.contains = contains; 6 | }); 7 | 8 | function contains(x, y) { 9 | if (this.top === undefined) { 10 | // Construct the bounding box of the polygon 11 | const length = this.points.length / 2; 12 | this.left = Infinity; 13 | this.right = -Infinity; 14 | this.top = Infinity; 15 | this.bottom = -Infinity; 16 | for (let i = 0;i < length;i++) { 17 | const px = this.points[i * 2]; 18 | const py = this.points[i * 2 + 1]; 19 | this.left = Math.min(px, this.left); 20 | this.right = Math.max(px, this.right); 21 | this.top = Math.min(py, this.top); 22 | this.bottom = Math.max(py, this.bottom); 23 | } 24 | } 25 | 26 | // Check if the polygon is within the bounding box 27 | if (x < this.left || x > this.right || y < this.top || y > this.bottom) { 28 | return false; 29 | } 30 | 31 | return originalContains.call(this, x, y); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Manuel Vögele 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lichtgeschwindigkeit", 3 | "title": "Lichtgeschwindigkeit (Lightspeed)", 4 | "description": "Improves the speed of the vision calculation in Foundry.", 5 | "version": "1.4.9", 6 | "minimumCoreVersion": "0.7.9", 7 | "compatibleCoreVersion": "0.8.9", 8 | "authors": [ 9 | { 10 | "name": "Manuel Vögele", 11 | "email": "develop@manuel-voegele.de", 12 | "discord": "Stäbchenfisch#5107" 13 | } 14 | ], 15 | "esmodules": [ 16 | "js/fog.js", 17 | "js/polygon.js", 18 | "js/raycasting.js" 19 | ], 20 | "url": "https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit", 21 | "download": "https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/releases/download/v1.4.9/lichtgeschwindigkeit-1.4.9.zip", 22 | "manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-lichtgeschwindigkeit/master/module.json", 23 | "readme": "https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/blob/master/README.md", 24 | "changelog": "https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/blob/master/CHANGELOG.md", 25 | "bugs": "https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues", 26 | "allowBugReporter": true 27 | } 28 | -------------------------------------------------------------------------------- /rust/tests/limited_vision_angle_overflow_end_hidden.ascii85: -------------------------------------------------------------------------------- 1 | <~"*iOR;LMBjFVs!M3*o"gG(bG%c9*cdPd"+2H]f0aQ4XLoLraR$]F!BOJ=-I"c2hg(0_S7XdXVcpfNiMCJR9Oa9Zb34V*bMF1eL=L-piho^ZTe&Fk?WFpR^jFHTod=qtYVQ1`@C[l"K3$kcC_hcL"srSPEPj07*Ol6!M0hqF#e-HsjUR$gQZhJBbnSbC'SY3Xn)&BX?T`"#mm=`&"sVK&K@u_P0d!j4ApGA!OpbI@80*Lh.n]:-aGQ3.#U['N'8JBUJC%)"\V.^RaUn:K4bt4%NfG4)h;/`P]?IP:$Uu[iUN1'S5]"eH=rsC"j=H`63I`/u'BANs<]+rbNqb)I+PkT6=XW-hPL"MiRm``S27+*k,rgrm-8[R;*o62W0B,N#[r8?sqpP>)uCqi71,J:lPQaW'?^m=h.<[GlV]?3%UDC=QBWIAJ%[$=k9ii$-pO&<\cKr?\mgBFY7R*g5:HK3!\.C@(9W(r7-'1^7do:]1r5O"NNY?c"[muT`ka?Yj!aVB5kh6ik@Mb,S8X/9L!89LZh'!&)sTC8Rp1?hiVld&T`&c+f:VaN^Ba17ljHZZ9s,W=/njNAp!;i^T\^Is%a>__1L68^lM^$hTuCr,CLpj_im*S75G)l@V.lcpDf8hEkXcNPPQiu2%@:B^Duc]=YH%jj[X`[\iD>m&"Od#PIae;T!,@LqYkP+'V!gLZ6Yu"f-DlNT:S_4Wu2eEpbI1P^FW1aPWh99p+j_KNjkN@+p3PDf,pVeg/&QZ7CXN/nphu!`[`e~> 2 | -------------------------------------------------------------------------------- /rust/tests/limited_vision_angle_overflow_start_hidden.ascii85: -------------------------------------------------------------------------------- 1 | <~"*iOR;gU@NBc*H+U,m#q/R9/i*6p%"j0W*`<&1RcQ`.>[+si#%.#c]T&Lu@FN,d2!C6BLcbd!H(D5)4*SXadf/pfLP[RDc'gZ6Ol:Oi$9nU&UZ^#/_"n+XK@O253XO]oQI(^d.!H^W:]@O@<7m`TnO5%(=g+29!k$XV8"=b?"HNiCtA-55Pc`oI&';8QS/B5K>skPM79'/tJN7:ObtM)KkK67f2(arfRS:?]@HXsM(9Yb*YZ;`TiUmB'&r:8pTU<3VWhe0,I&o?s?]cO:]]L\VO_/s06X/K]P8$ds'T.Q/=c,>l+C%Aq9p30A'c4S"^S$+h!p4&'%?3*'NmKhil^s#O:eGBP_7*OckA?>fiL\Z&r$=WB`cL*Yrc#_H.8)t[fOOK]+Hg,;HD&[U5.o0q!iK_)jY/(Zlba#EOsZhO3V"L#s.#7DUtjk*?&`"UGW<."8WqWk$:S$:6+(lBNMF_/bM]Cd>[p6=C-\4'.Gl3cfb)^767W57gsHc#e3ZG>>%*!M3ET&j\n8`(lN>.RK7&e"F=UTY2ioZPIXSdG+PM-\AtV`0e2GX4]"8iPg<`lsr8?q'jMD-f*TB./]Z'sm0daQ\:kgVj_9!GFQ8_rapP=-HfbhjP"U;op:3R&!o+;#*f.Jf%3:n,(Z:%e_Y`i.X/W`IMN.G0fm#/bF5+s$E81e>`/06*PIBRqOl!o^6+J0,-,GC5`XnHai2_a)>b?OL#(aqMl;ulf4*`jh"iOV@=*eIb`Co,+n73DHFSU4].j%4'27;WJKR6JLoZ*m?gKE[/V.W3Eh+$?.oFd[S#jr*E[HaE2"#qB!b/38f.mJ/p;)3Fg[.;*YT/8NiP&OU!#1)93O0CZ\P]QsJfS['KnpLV!sr%K-K>kVP_ddqVF\2#-egFPF$%@-\/Vh4naS]Eef141>HL1dU-i(VPnVJT0k:&pr1=b-;RG;p%+WBI]KK8b:OWkoW[0\NNLL)^&DtrE#&^JE!mXFU<95pD20U.j-uGn_VT9"k'Kdaki(Jkks;!hF^U7%`H`gh1`6'rdgT7^@_B'OM99'B2F&+L 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/staebchenfisch) 2 | 3 | # Lichtgeschwindigkeit 4 | 5 | **This module supports only FoundryVTT v0.7 and v0.8. The lighting system in FoundryVTT has drastically improved since FoundryVTT v9. For this reason a port of this module to newer FoundryVTT versions is unlikely.** 6 | 7 | Lichtgeschwindigkeit improves the speed and accuracy of Foundrys lighting calculation. On the maps used for benchmarking, Lichtgeschwindigkeit is around 2x-3x faster than Foundrys native lighting calculation. 8 | 9 | In addition the lighting calculation by Lichtgeschwindigkeit is incredibly precise, providing better lighting quality than Foundry does natively, especially on scenes with many walls. For easy comparison, the videos below compare a forest scene with some trees, with vision calculation performed by Lichtgeschwindigkeit and by different supported foundry versions. The lighting rendered by native foundry is flickery during animations, because the shadows vary in width between the frames. With Lichtgeschwindigkeit the shadows have exactly the width they should have and are animated very smoothly as a result. 10 | 11 | [Forest scene with Lichtgeschwindigkeit](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/blob/media/media/lichtgeschwindigkeit.mp4?raw=true) 12 | 13 | [Forest scene with Foundry 0.8.6](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/blob/media/media/foundry_0.8.6.mp4?raw=true) 14 | 15 | [Forest scene with Foundry 0.7.10](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/blob/media/media/foundry_0.7.10.mp4?raw=true) 16 | -------------------------------------------------------------------------------- /rust/tests/30-tiny_vision_angle.ascii85: -------------------------------------------------------------------------------- 1 | <~"*iOrR6PATFVr0f6A?s675Cf`LsVh__;\_m)*0`g$DiBm%ALg-WZVS)*eJ(RQsQ6)jmG6lpM=koM$_s6+ZP?U&I/Q'i_XN3;MQkkVpdQur1;sEhS%8,KP&;d-[\+mkP+uT^MaI5_BEJl$]fb8IX57rq!0`VA[,?bq"p9Pa$JZK/gja_)jd[B.e^66X]&8?Sd(S#:*UF<)L6*0C*[1"F5oD84l\KoO9V)jgiuUfc^4pPd/Z8m\8;[tf5R7X!J[r"do,4p'[cuFQj:X[deIYh65j5E2bU$V;-'(&:,]n$4UYtS8_e_*dMS0#E\tQWhPZITR&k/L,T;"\>])f".8:J6\$dX*l'\ac8WSh[?f7fuFok?%j\F@QF,@cr+@9X6_u@(>`P>2&c",]bfX,[Se`>0^:/?"ZuT)@78J%RV.B&j&TI?nt4m%5m4J4])a&hmIkNWp>,9>,s-kGmo\6d=<.!L%/=1r(A[:)q(1=WIr*M4GS,@(>`PNCLOG^:5,tS7_SQ+I,C7f=^ZmKJ0*'\.Q)Pc,d\VkpD23$#/?&h.XpC`HShh:O6\Cln&Js3?B+S>LOG>\C'uCW<#KC(HJpj.:BO-4h:NmX_EaZ]M$j_1GutHcEh+ug"%uLoac:T0%uc6,l?`#:a6jAB>&RSe0U&Ff5*"jR`;:LG$J)7D[M"dfA&?k-^ViO=rs$rp>c:62V0ps-Pp04ZQ@NpUQ$h#!Ci=VN\aT99Y0A@]pA^Wagu/#JC,8ggWH^s"rLXLX(T`,!2J)MlJ^M/`#>D9#GHhR(p.$P/Q9G1hRX9H(o*@I=rFl>`oLWSc/-YFU[)X/r5TiYNR8ZjQ?p^qDN3&1S@r@W8\elI2XG3-?rZ+TCYIAX_!o*0m`WcJSL42JeDYaGlb&84j#s8!Q]pelCY*[`rlE[IB!&8sp8QQ_RBK*9:\e.YWg6M-IEs"Bt&#fieptA(b$AZb+nF\kCc?@bHJlZfc)7G3KVm>7%[5:mZ"i>WdCgA"M,(B1(ChrL)3sC:\Sgr>Eeug;1Ijg%.&!SSD:U>LFddcCV(JOR5Z=99PUH62"PXf.O>[,oX\fhOMfMoSU`h#gGrgF>_p$j4YmN.(%i!/"J6G8B7:?=LQW$ls;TQ?+QhQ.?mQ6V-')Jg8.HTCaIGo,$f5Jo`*4nrVhL>WXdm5n[Wukq0RFOr9g\:4,[IZ9]J'k_Q,6:jg*AjO-Reqhs-aoG<`b=q'9k%Is1J&q_OC!FrgSbhT%RL~> 2 | -------------------------------------------------------------------------------- /rust/src/ptr_indexed_hash_set.rs: -------------------------------------------------------------------------------- 1 | use rustc_hash::FxHashSet; 2 | use std::collections::hash_set; 3 | use std::hash::{Hash, Hasher}; 4 | use std::rc::Rc; 5 | 6 | pub struct PtrIndexedHashSet(FxHashSet>); 7 | 8 | impl PtrIndexedHashSet { 9 | pub fn new() -> Self { 10 | PtrIndexedHashSet(FxHashSet::default()) 11 | } 12 | 13 | pub fn insert(&mut self, value: Rc) -> bool { 14 | self.0.insert(PtrIndexedRc(value)) 15 | } 16 | 17 | pub fn remove(&mut self, value: &Rc) -> bool { 18 | self.0.remove(&PtrIndexedRc(Rc::clone(value))) 19 | } 20 | } 21 | 22 | impl std::fmt::Debug for PtrIndexedHashSet { 23 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 24 | f.debug_set().entries(self.0.iter().map(|e| &e.0)).finish() 25 | } 26 | } 27 | 28 | struct PtrIndexedRc(Rc); 29 | 30 | impl Hash for PtrIndexedRc { 31 | fn hash(&self, state: &mut H) { 32 | Rc::as_ptr(&self.0).hash(state) 33 | } 34 | } 35 | 36 | impl PartialEq for PtrIndexedRc { 37 | fn eq(&self, other: &Self) -> bool { 38 | Rc::ptr_eq(&self.0, &other.0) 39 | } 40 | } 41 | 42 | impl Eq for PtrIndexedRc {} 43 | 44 | pub struct PtrIndexedHashSetIterator<'a, T>(hash_set::Iter<'a, PtrIndexedRc>); 45 | 46 | impl<'a, T> Iterator for PtrIndexedHashSetIterator<'a, T> { 47 | type Item = &'a Rc; 48 | 49 | fn next(&mut self) -> Option { 50 | match self.0.next() { 51 | Some(item) => Some(&item.0), 52 | None => None, 53 | } 54 | } 55 | } 56 | 57 | impl<'a, T> IntoIterator for &'a PtrIndexedHashSet { 58 | type Item = &'a Rc; 59 | type IntoIter = PtrIndexedHashSetIterator<'a, T>; 60 | fn into_iter(self) -> Self::IntoIter { 61 | PtrIndexedHashSetIterator::((&self.0).into_iter()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rust/tests/14-overflow_wall_not_overflowing_in_fov.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m4/P^>5$=<,@o;!CD8[T*nhi`'Pk;bHsMnd$P"eg#@?=6Lg(fZg1N`frEEkKK=&))I#8@Q^fYnRt%*;P^_S.-4;X>>henVELKVqu,]!n+V4U^#u!En,>!XbS$R=@ho5JZZ=LCj:kUD,'ItNRZi)75<4E2E\]%M1,WOOa1,+5DRrlXro@JC0Sha]'8\HDH,[XW`6(>-[j(PV'NiY+ln&R,(:,OYF6&bk*c=1]t96#t-'lp8]Q,d:2?52?/XmQC9dDOM5qKI0`C=0TrrXGIbm4@DNFK[Dik4iPdTe%7Hn;$#5>&Ja)HYFr]p.WNt!JAfROA'J6MTluu#>uO#qeMUD2=C3N]T`?16l)os0;j*dFL=AI7m]9cg#`%^B7PC_qQNPF)KX/U326(<5?s%>!JRi;iSR1T4l7q0UY0WVklNr#CPGtu]4<-.r0_PPTc`SgF>3LiYSYcN190uHMp;d"\U)U[\O,8e(a[MFrj%5.<2G!ViCSRtP3F_$EVoUn-<2J7#Y$bXlpcRrC%"],O1%QYF^1!sT`^mRVdojU?91gVc@Xpk]#HW5]mu#2e1^t=.'rWJO`\YSVgJR)`IG^CIkE)(HDJT)!(*,+bh/1@><,]e7m0_l=`m()f*o7+"+hHEif=W-Fc=c^OND6'8EsC*VCCBK*;:WJtJ/V@Pg;H_J5inPZ7=RGrJ/Whm>.+'JJB4F[hg+o?C8c/S9n%=!C5!.E10`d8Imb,^aGm[+IK`0#$S$W^TkNVb_DbjOoC;Wk^GjGr:2mR_Al@^.Wl8([g<7Dd@dRD[#MR;Ocig(o!Vp'OLKJLCQM[7c_,rjbrBNV5"352oWjKP/orZ>.D\A/mXQVX*_\%:V\UeKf=Qj!)b#ake-F:'MA>2!lXgu>&es<2@]u!T[EQYfc1:<(YeGskJ)]Ke>'CU?JiTr<8aeiS.sRF!DV;hG@BQ6i*BMTg!i:fWrFJ-^P*+3P/pX3)j?u^G)j=+mT&2f//2,s9;$+UDBYXI#@)[H?c4j1E6QpU7pP0Y`VHSJE:=0b8uNEEZ;I=pF$?8G.LBkHgsAhFc,88=pGJ/-Q1Uj!r:Y[Shb!ekQ`SR$ap!;b$Kkd-J3.-JVpp)7gg_J6em#-9RqF;bs6UP2^hm6d[rY+'$C--n9dP[P\&N+*mk?!b2nR'XCE;J9c^8'`"jI'f@#PiWe("e`*n%S"A&!\)@cQNJQAoGX^b9BOPJW;"'8_-H7ClQ`=?H-9cQ7;Z.9#sH/#."h?#)sZ_XAJmsuAAf#Qir5dBt6lDTmr^^Aj?.Q:4W@Z/,i\5#h-#AZ6'UV!14[!K,R,65m(9s^`UW^BX)_DT;3Fl8dhkV5q7Jtd_eG3Z>n^l;g)WAX:#@XV/a?fQi$7,#n^j)ImhDV(cO6sh06m[G'1L]e]L0L7Hh4L.R1r1tbI78k1SE0oTB)7%%HK%(\htHS<:U[(0%(qoimK27N=UT_fj-Nf$Zm3M*'UJk)$e"GYL2.)E1j3\]N;A!"$oc/F-l<&TIHg"FDWo<6&Lu^nTXCdP>)tgOPYS^oDlcg-k#81pMo$L64bh3Y.eQY#,]i@1A/jO&C 2 | -------------------------------------------------------------------------------- /rust/tests/4-directional_walls.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m47;[H,s4#?aqj)H:BZ<"h1qtIs&hN-^3?`qp)hBgiMlu#Qt?&ga$9WL`b`Qb`TX0-L1O+kN@r\_aUOkMrEcZ2U'.ME@lp3K2P7t&hSj)6e2bDG19uG3TS]=GEa.mg(g[^:-KN9X/V]UEP4V@HBJI.>ZA;k&N>BFT+d]i5NR2-Z3TPr.5E-Grb<\Si;ED0%?4KC5U_C*P?LFB08X"J`C&^C7Sc?nrcPoWOMI<1his[2dG-3C)2aok%R3/ZVWF'<@mSeXTcA+tU4&el[[(;Me89[9gq:M&*hd8!jgoS2ro(b,,s4#IZi%Y[T6bXjQc8(WBg+7iJGscl"B8QI^"NkUB>`^P"+PS9Y?gTC2R:TsFj,_b9G:4I/R?ec;O$fCtRAgd\!=0P7,-3RhLs4PMY2`D15u!6>%lN+%PS-'mo2X!f]H*Up\oM3;keXqQ]HfQT75GS0gj*)sp/O53Vc@!RTj-Xq?j6QO)8ejXb0@&MW"$)l1TI79?#8Pic(qu.%==*Fn]XI2H=8!#M(^cM@d);UpBKUp$#j(^6Imu=iV+a"G/Q1TM^U3_.Sa1A\j^\'Rj,5+n$=97r1`m,4I0?G.YeDD9K\b+,FXB.aeRk`cGX_q2$!n,,X$'K`K2Y+_SoW)B:_B51\r$"[=2s.^\U$PsX*U1>*M+Ds#.5@Thr/"i+[l>;geG4(_9fuu2+-5!S]Vd$)p$(6>Z"L[0Z_Ye/r^fKr?444*oUu%-fV_daI4rR%hY2Tug0IG?)^WEuaDP_"V1E*ZQ=$?8BQCFQQ%Mp6*-I9O`HqqP_AYdf0<2>Ua_"oJ7hD*17L>EQ,N?]B#S%,M+`>hR<@sq4#RK[uPeS4`THYIl352X(X]XFT3-$:F+KjK]Yoqn(6"cob"r>Ra68t^RaM2%e[;mk$(4Oc_C6M9)8\;IQ2EF[0UGqi])?lMeTr7PQ&>FrooS1'ssU\jPMn7G.>V>o)::/O_J6,U:;(4-QN&.2n_K86r:dgrXm4h,`?F3=e`-4Xafgd"3@+SH%o@#UX8g*K.>=kMV-^_-gpT!\l,@ZZ0b#LR&uhaQ0$>Woj1ophm:W`aE84)`.QZ!jpGX8R(5ml@u'tC2LYhk$oY:WhJF$@"!tkAlTsqE>:1/R,.;A2ErKJ(q%T6Fm(9<\4iH>@OTh&;&6Z]5Wj^I&7TL_V>E$c6CXigJ%4@V#K[nrd.2.>2#$$dVg)lcPk6`SG#THqH),EM92C!qq4=8q>:=LSLP<~> 2 | -------------------------------------------------------------------------------- /rust/tests/15-overflow_wall_no_point_seen_wall_far.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m4'<-^Hg=W"o><[p0_Z4&',9,L7>CEcDC0ghEORV\7"QSG>.7#Gqhf5d4=jBc_*Y++P0(oN)WO;N\+$>$#2,!/N]T(1%Rq<*.g3T'Y'p\c\!6m@IRNY,uS!9L%jrbkLarc]-Is3>q`Z+T`h"Z"t1pt8l45d\,dTA+^]FW+7H5_UIeLW>Lj).qdc6,fmTA9EJ57?b%q[#Ra,n*JbJ5d:Z@mQOQQ7:oB9e.XW/U'"bO5L7\N#c4$VK:4<+cXr1j!Q(]*KE?'na>7u/YL]6KiOR:?,QS^KA%_bT%gkR(XeYQ^/dSE7\o2cArs?Q2F7BD]&g;8g;1rfRGn?j%#\]ZR%oKljr1srh&8mT>;(Tm\m]NU_:KNWC&3HQCo:6i3&9g@mE`kV+'`I4qXSBBLN5iT:rFiHp.I=dSFhjG=3H#)?q[4b5Jd3$MapTgDeh?`9t+>d(J5M,NCYNfMYt)%-X&f>j4UB[V]Hr5lW!jsd)^`-)K.9Mo4sX6*#T3a?h(h0;j8TRu^5Ph%1(Lp3Jam.'is@e9$n>(6*07ut,O1i!pOQD@@Ie5IO%BWc=+jcD4>q`t,I"mSN*YoFKudrCTkANZP\P(tg0Jjq]-00r:@5$lfD$,gL![$,kSJ23Df2WuP[Tj/s;Q\!@iP\Lm!^17fljgAHI'p^*Z4)j0"H,V2M&ncL&dnW:0/^<%PH#1F%HY)9-5X:-HR/suV'GYB_`*FEk$jfrM(S/JT+HHB(5d9Ji:f3\Q*6A3rR-Ts&L%_M*[R@brsF56_#L*-YC/<383'VZ5;WrgY"O;:;H7'dSO3#$mLohl,dYH6((B(@lIa&AI^"F'Q<"t3OM&^a\`_6fB#`((cXkd;i3^LHNqFo-.,50f6ie6q06cZU0f7us$t+`HkJT2*r0/fOMt7!]5l4!LfSG&BNGCK+#R!QPi9L;>UR'R7uEV5X5+FG+0"',FY1ATuVj_K%HEtW'k-8>;(U(^7YJ4UF51[O;^O&R7?r'CYCI"4so4-7^'J_6nS?hNYDcorM:]9f;nK[U/m>l6>'/<3\R1fJqog#m9UB+NZMG4u[U4+RZA.IHfZ'>I%J+O82uC8juperhK6UqtjpqjC3-^GIQ8`ol_3KA&!8CF?`E:E:Q8ate;4rS9_mH+Yt=T/WS/Wb*aD7,n&1+ak)dc*mU%&lmsbJi(fjT?DY.)^:T>pe+Jc%pHCU=OJSK0N(2U-IX<72I9UtA 2 | -------------------------------------------------------------------------------- /rust/tests/15-overflow_wall_both_points_seen.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m47PBf.HEn8ksgjNV%\0haaps[n2W2oML2:Zi@LcT$MNHP`(G+4coL9FjIB:#)0p.]U^]#uq)\(bRc0l%I#es[n;q8l.%EUbSFlS+-msh'!=mbP4I!bdl%tEf!oO?gLLh5G]"iDY>Y\CC[g>=eib8fIN'Y[Mqb@*X!6RUVLTG3?BY^aC&nY`PnG6#OYKclr/g#[u^Z(IdIY]N(eZLhFK3&Z:p;2[nYu,u-^!QiLFsaNE,EYKXRmN[%e&G[F76dE/ajCljLpoMsnI3Dn_n>C]o@KO@,8a4`D-R:0U\"6*RLIYs#,o\K@PkQ*\0^#CuVBIm6ld'?*3InTckHE/O4lEt4'CF'b#4H)hZ`a6-E#e'$D<75E]m*NGo2\kqY=9CiC8?A%C,\8PC(L$``+N-V]-1#McMQi"'4>s[g8m:2&VB!Q.2;FhXFU#6+cT-Vk;)=)&=*gW(N)N"5q='Wn2bHCjN*:Im":k-L?p\?Ol5BP1]gj%P#SAnFg?JYdX:ojI5K:e\>&/XR$d+3W@&.I2*n_I%/l$cjSF:hN4_!1e<6 -------------------------------------------------------------------------------- /rust/tests/15-overflow_wall_bottom_point_seen.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m47;gU@Oq&V]aQ:VrFO^s,,9FjH-71U=l.@%AIG:;KS7,CdI2-1;`Pk3h'_tb=6j#)*H%N8\KNH1LV?#OM#8gT6S*oVG+2RdhlO4NZIp1LS:f4TLqC[FikdA0#Fd!c3:6Qc.(RTKJQQMQ$T2P.2a/SYE4TIon4i^R2"pun^2&`P:m;[aDp7ie!_qZKB\mR)WI.?-:R)9@;Q='kK7&k#)J:<0MsT]k?cf$qoYLit&)m!`pe5GX0beDB(MrK63rAr$ABacj,Z23'nm\6_b@F3:f'@dMZoZ&_nW&_0*mS;"Z<6<3qoQ]7[mB,Ihd60QgiA"0BX-epYMI/2DZR4HF8CN5]h%QcZ1HR6?]H"dc_rEULSeEe+6"GDWIfoQXM^Q=sg\G>XaZaV#**eMJ2M,].t6b#hFI8<%1iT)XM<@ogT!JcNq]Ji5DBV\nfUFl87L$8Hg^W?A@YnGC(d@RF#K6n)T8Q:uArS(iHt-KaDbJiIjJG9ofb04P;ttjI-r!q]AZc*f2cbHV-%uor\a^b#"9\Z$JeeMe&fOS_t4,UA@,A_"NG)P-Y[Z=2(7kA_#nQYPZTVWjgC,\PO.YT/g.PfC1/uou#UlrH>]!Bh+"6i%iD5p?_)>l":\pi$AoEWKVq/$P%CLT1$MoTCe?84ArCcppoj$+TsU!Wq@p17#24cF`qrdN?#(8@i\qu+>B^NpiAG*E\gs*2B?h.^+9dQ4OSfTbH(Fn=<&\/s%&%ro)C1s+3D]>!pV$K@d;VfSW!7Kf<@>t!fr8^t)f2+^Rfr4Q#:Ae(\kIS[\MQ7G<^$*+MP[bi3d$5^B`Yq'7qZ>=LDq2TD\],.h93TNYSM.H$`1#X3/MHuuq99M34,^95Q-\?iTo2MqCZg2L$(Y'>"Uf?rd_1oRkfbF%o(.[AW6)AV@NN1!+bmXrmR0%#(:<9WJ:N%pIU[8Z#k.eV(1uHSK(Q3E^.CIU\FYDkhO5c*7lmYlhNfJl`^*V@$,3od3`G@@\=VbE2&N\SF@#_,f&NSq1!,\\fWo7u.Or1"B;C$hOpJ3]%2DLT5O%3c5OFP_IOM\7@KWo1a"#7HrrYQO[WnlY&(qW\NZ60D+E1bKLU`*_8)V4->:C#4m_"^\^(L.oQ$pih@SlYJ$H[di#!^!!J698h'KJrG.37oI"+K:qkC\Y-AN"A-#IS$rAil?']aE*E8LpH6Ho$S9(9W=+'ZH<7*#Kp2@="p#(LcH^#MKpu-S"Wa]LUR0[>H7"@=%8h:J6+uA9H)QeKq"/Y4&Fs,mRaHV*jb"Of!IRfZ'`*,C0)>;=dV2jJ@4dI)MD?I=bC/\*tHLU[30L&NY"KZ6q1=HKmaD#kpUAqQ;YitIIrGaa:dI?eoFa!r24g!N+AKS5(-u=]9o`/cGb-*)u!F;+8YTH)C>?CS6Crt0fJ\hYaBo=WKn/@]pYrq[9u+D=1./,DohA,ju^7p"Co6k:/t4!3=KJ3PC6[iAp"9o#Jq!3*LQ;WKS-6EqIP%Y%/=h.e2bN)0R`2k0k"SMGIHcX*8^psk[?:A;%P.XR'a'@Bjdq/D7'fjrM)"s_oa;$Md?>d<:rV&~> 2 | -------------------------------------------------------------------------------- /rust/tests/15-overflow_wall_top_point_seen.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m4'PC#8'p_q1C-f&r)U5B0BH5t3A-(=eto)H*?1t9&T*Fe1LnXfb7$18N,];pE>m/a,"Y2jWG!`IP1p:i3h1A?c?#TEqp5$850s,=J%Y7IThm-E&/\>p(K1)oq#<+Qj:>>eULn5BC-.OZcLSM2H`LkNfDNuoY4&`Bo(s]2;a08uMj095pA]EWMn]X27QtZL*`$csj!j(H4&qLLC?t&:;c.9%6E!:6NoEiT)lr(9G0fXI3=I#bOs=b_Pr`I>Eo!JO:EA746'@9K%5GpdJR$1Wr;FZV9][6b:=;pga9(!BpeoL285,*uUW_*'Jr(nhp4jgs-\rhSXaPLFRH,)E5?B7YNpG4h$"17V66BG>dNr.hg"S=C#DhlQZ1:p-?%Z*CMYRD`GLj$H]QJnR8+*P2Gcmlb)9Hp[6&c?@MDK&R1*Wf#Qpd^WSN6e*0sYE]I*nI.%Qf%XtgnOFN_[r5C6Egj9(`P2`qpsB=;:^OQc=@)@Vhet^b)NuH9iN\MhL,0@%Do9dD_;TgIE^`D(s8LhDDG8*O0"](mr\:-+X>$9;U<<$=fJgC,@4=,XY-[lUb!Y3Q=F60Hk*dhK:%GkQ'(l!-CT'W"h_\Vm8R/6AEjE4*3;4-h>k)uDl<\ckS*BJOXBRa`;J2*f+bm'dGJR?18Z4(V7E0/@nX3ZDi31YpF#1q*r4NHq7Soi^4u`WZ/D$ED5.Suu$>Bj)NW%bEJNZbLMX^[Es6B,beWDPiX#pU@U-l`XmC)gSUCcckc5G>t)`YTu^NHM1!';WKfOM?&*8OeBBuXbhR8\iG-2q9W79gj7\HI)Lk-Q8@*@W9:o&0L\Y^7\j\fI,fop_,`kuSYYXB1g=d5)5trSm3-04r![.;5l^Gp#-=qEg,&&TCpkmUt!=^g-5R4`oXuE,>[=&'N,3%.^@_3E?9#KD!sb%KeD6D-Z@I5m+)-oSr9X"li#=ZU!P$8=bmlg=Fr/Ts.%^+EeT9l"fj5rb)L^?G.$JVA&2(KWT'o%_A:(A9KIFkmP1Z7-QGi7`O$QmS1ca*][RljotTo+WYcFQ%A\ih_t6YqghLn9lnK]n;W,he20V@Qeb:snGX7"@hof59e/K\S>3(N2*n*YeZN`J%(an6?)LC\aO3lL)Y7_IrRr[eE%)6!(NrjDe:.u)5SVaHY6%kO>1RC-'/47hoecef+].oQcP!qn6aka_NM-Ei@GOO/2%))if7duS>_.DUF6NF[!bPBHfd=*G&3L5m>,XVQEZhF8DB^$OGqmce7h8_&`tV2?1EkO-kKg(j%@3M?IJKoL#e);ee8MHZMB?tFMFZ1:%/guc5X:b~> 2 | -------------------------------------------------------------------------------- /js/fog.js: -------------------------------------------------------------------------------- 1 | Hooks.on("init", () => { 2 | const [ major, minor, bugfix ] = game.data.version.split("."); 3 | if (major == 0 && minor == 7) 4 | return; 5 | if (major == 0 && minor == 8 && bugfix < 7) { 6 | return; 7 | } 8 | SightLayer.prototype.commitFog = commitFog; 9 | }); 10 | 11 | let recycledRenderTexture = undefined; 12 | 13 | function getRecycledRenderTexture(d) { 14 | if (recycledRenderTexture) { 15 | if (recycledRenderTexture.baseTexture && recycledRenderTexture.width === d.width && recycledRenderTexture.height === d.height && recycledRenderTexture.resolution === d.resolution) { 16 | const tex = recycledRenderTexture; 17 | recycledRenderTexture = undefined; 18 | return tex; 19 | } 20 | recycledRenderTexture.destroy(true); 21 | } 22 | const tex = PIXI.RenderTexture.create(d); 23 | return tex; 24 | } 25 | 26 | function recycleRenderTexture(tex) { 27 | if (!(tex instanceof PIXI.RenderTexture)) { 28 | tex.destroy(true); 29 | return; 30 | } 31 | if (recycledRenderTexture) { 32 | recycledRenderTexture.destroy(true); 33 | } 34 | recycledRenderTexture = tex; 35 | } 36 | 37 | function commitFog() { 38 | if (CONFIG.debug.fog) console.debug("SightLayer | Committing fog exploration to render texture."); 39 | this._fogUpdates = 0; 40 | 41 | // Protect against an invalid render texture 42 | if (!this.saved.texture.valid) { 43 | this.saved.texture = PIXI.Texture.EMPTY; 44 | } 45 | 46 | // Create a staging texture and render the entire fog container to it 47 | const d = canvas.dimensions; 48 | const tex = getRecycledRenderTexture(this._fogResolution); 49 | const transform = new PIXI.Matrix(1, 0, 0, 1, -d.paddingX, -d.paddingY); 50 | 51 | // Render the texture (temporarily disabling the masking rectangle) 52 | canvas.app.renderer.render(this.revealed, tex, undefined, transform); 53 | 54 | // Swap the staging texture to the rendered Sprite 55 | recycleRenderTexture(this.saved.texture); 56 | this.saved.texture = tex; 57 | this.pending.removeChildren().forEach(c => this._recycleVisionContainer(c)); 58 | 59 | // Record that fog was updated and schedule a save 60 | this._fogUpdated = true; 61 | this.debounceSaveFog(); 62 | } 63 | -------------------------------------------------------------------------------- /rust/tests/19-origin_on_wall_endpoint.ascii85: -------------------------------------------------------------------------------- 1 | <~!dNFYPm:-MB,\ff'TiZFMOkAE\u5alhN<:Q'3#$$'G*if%HF!W";)']!/?N.Ne0f[B1.W-m-Z5hoKsDYboe[DP_DOlFr@-ulG\"[i8'@9gs%ukJ#UD#DT?D>4cZ0;/b7M(`Dani@6#J9Tp2pRtOtuF>8%2[B;GoMXdLVZ7kd#&3T:@V?`AZd:I=pbpW2AJS1GMP*Y1(Vb#R_]p].Da]GMFj8Od^`MiHUI3ofV3VKbDSc[M)AP7-E,_fAn/,47A@&Ui[k)F[;L-/SaK'e?0o4qq[k&1Iu<2<,_Yn//?gEt?HatV1@)P_e1%*tIi&#F-%6/R_E`imUHA+YA5n9(BNHH'E:4SModXBbY#43DUeIlT#\SlLF[ARb*#k`e#/jt6)CG_2sb98R0kGCD]R+>odIjaAB7Mh@Sak)gI#![1#S$!`4*,UcF#@CNU*DG'a(pCYQF:7,HoA"H*/U>n`Ao,*cSEHd(oOUt1?!E#fn%1g!l5WJ^;&/8<4%=&o1PAA4FQX6UIOFXOQ+_R-h>lASn7p4,o4Jns_hrlEh/rDV]ZdK-pV:!^D5hu?(#YXe7Jc7S_jOB6,q)!+P^2(kK)R?sY-(2rLtm3X#IFp':bV->JpZ6,>:co5P7o,l0*dg\mndf4F`UJ'&$9h2#=(XI`Qo0QtG@-"?$%SdB5EBi;0+*S?V]CJQ,l00+GpEcm^M!T9<)fH#H&G-+joTL]5h9s,j-&ZI4!C%[5tF+Ye8Q>me]KbaJTqCMb+A^U0AJ4eI=g/ccajYF29>FW[f$s!)\rgc-lFmXEM"7%/+6DXk:\d`f4A#&+8k0m/krl>h`rL"_sTos'X%k&;/d@7Tr!KN%Zs,:X4QZa1-SAe,beTBB-MmV]^/\E8/bWP]+!1LP#nhiu29rVF[=.EgD0B%.^fS`h:=U:l17dC%@iQl2j0)E'N@Pb7U='.WZMTOJ+,-MaKgr-ZU_&=NqU:b_J>kk?2gd^ma,[6])=(3Rm_K.uFKWBI!Sp5CtP^F52D*D1BX!@[?7LX$.&6.dF$B<'0$]K3<=?&B%]Fa2#HkuD+ReMUXQ'@g/Qi^7pi"cTG`[JKWe5YJ[t:CY=cm#I6iQgRq%kPhZO6~> 2 | -------------------------------------------------------------------------------- /rust/tests/15-overflow_wall_no_point_seen_wall_close.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m4';gULRq&5m2-sO6X23Ecd/];Y?U3Jk$gJQ[gDR;B!G*c$f,khj=PKR@RbV78eN8(i][kVR2ABSJblm>rF`[->i/`_*#3ldh`Z[_WCkIj"Ho=j\bGs3;gNOAoAB!ac&G2J)Ms#RLaIrWFLH%ts?/VEd-]_OJ5E7@@1D>c.eV-Va!UNM)^qbZ1+Uj[p0+7Fh=<3TNEF$%O*$XShd+%<;)V5AmF$7+r$4$qW0;57<$Y`8GuQ1[`mP2[jfi`c$'VQZ`JlDm'T7/0$Q)#1>nQ[7/W3I'<5>t9V1S)9r''N!P#OQ\E"J^T;)LGW8U+`1bS-HBkrYUf1&\Df"'DDrB>)il,F,Y?rRb!Q]@7a5V+Wb5QQiE7q"*4[u+(0$/$W8nN+U#(=4JKe(\5S*gN,nZ`7J)77$2'e-B-K9!"j:oT.C7][$o!XZ>l+N-LT($9Y=(EaP?MnDDF#&aillonaRm"\G`F7)5Q&4/8cXKSE)nEk%2:`_L,_]u&_FgL&ZC=!E%14:NU#QF5X_JM)2Ns0(;fkoN3nV\*\V6:5T^@Np'Sr-IFH("#KpX2mKqn6sU`b4%13L;5DY<9^_a^4CIq/LMeLTh(N<[c`bnoA1Y'!,@A"O-;"Eu/?Y;ed8[!Y*?(^%WqWL2*s4[^\/+h&V\OS$L/iBi5.8Y68k1X:>=W2PiESF@<2$?@Xh2#`_qG!@c5=77DnR=jI^Xli7C]phB*I81ZRY&QVSBA?'h[LLtEBhs;#$@6W=Hi]Y(]B[ONH^=+'mjT[@;4mqER%H-Li#F_#_[i2,.JG]4%;B2^fD!?h6;5+*/Qr.@,MXLNl?"_q\\6>I*Xno0*&VCXO@q9+O,'/2to3=,E5H1eW9?R9ciEi+$PIejL@Z\UL^UR9K%#^8AV"/:q,8QJQ;W[.lVDOSDo6$.L4561,!U\XgnS;IB:0M\&XTaFo?o\dp$eKQR3\ahF20Y8>)p1!PCP'^(u+uW=ZV3nHREIO&`er.3hh;B7*?d.5J7kIensV#:]7$USju0A9<7^0\*`:.?jerYjJatT;euCFuCEk^Xn3P=H4WTY$BqVN>GJO/&YarW^5dqgN-_jgEP2,Y-Ym'$F38Fr1KPCNDtop@&n/K"D8_:L)'BC3q:*s:ok]@P>0TLh,RiC[1E&$."akNQ@GD9KComN>d(lCX'8)[`Z4DtmF3$Rl3%Pq_psQ'"XUCn'WjO-/aISM&ROFnL:$a*Fk#qsaJLKb%HY0QZW@nn%$^\SY7LQ?IdFQD!l'%t)?~> 2 | -------------------------------------------------------------------------------- /rust/src/raycasting/util.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::{Line, Point}; 2 | use crate::raycasting::types::Wall; 3 | use crate::raycasting::WallBase; 4 | use std::f64::consts::PI; 5 | use std::rc::Rc; 6 | 7 | pub fn between(num: T, a: T, b: T) -> bool { 8 | let (min, max) = if a < b { (a, b) } else { (b, a) }; 9 | num >= min && num <= max 10 | } 11 | 12 | pub fn between_exclusive(num: T, a: T, b: T) -> bool { 13 | let (min, max) = if a < b { (a, b) } else { (b, a) }; 14 | num > min && num < max 15 | } 16 | 17 | // Check if angle1 is smaller than (i.e. is located on the circle counter clockwise from) angle2 18 | // This check is normalized to be able to deal with the overflow between 360° and 0° 19 | pub fn is_smaller_relative(angle1: f64, angle2: f64) -> bool { 20 | let mut angle_distance = angle2 - angle1; 21 | if angle_distance.abs() > PI { 22 | angle_distance *= -1.0; 23 | } 24 | return angle_distance > 0.0; 25 | } 26 | 27 | // TODO Use a segment class instead of this weird trait 28 | pub trait LineSegment { 29 | fn line(&self) -> Line; 30 | fn p1(&self) -> Point; 31 | fn p2(&self) -> Point; 32 | } 33 | 34 | impl LineSegment for Wall { 35 | fn line(&self) -> Line { 36 | self.line 37 | } 38 | 39 | fn p1(&self) -> Point { 40 | self.p1 41 | } 42 | 43 | fn p2(&self) -> Point { 44 | self.p2 45 | } 46 | } 47 | 48 | impl LineSegment for WallBase { 49 | fn line(&self) -> Line { 50 | self.line 51 | } 52 | 53 | fn p1(&self) -> Point { 54 | self.p1 55 | } 56 | 57 | fn p2(&self) -> Point { 58 | self.p2 59 | } 60 | } 61 | 62 | impl LineSegment for Rc { 63 | fn line(&self) -> Line { 64 | self.as_ref().line() 65 | } 66 | 67 | fn p1(&self) -> Point { 68 | self.as_ref().p1() 69 | } 70 | 71 | fn p2(&self) -> Point { 72 | self.as_ref().p2() 73 | } 74 | } 75 | 76 | pub fn is_intersection_on_wall(intersection: Point, wall: &S) -> bool { 77 | is_intersection_on_segment(intersection, wall.line(), wall.p1(), wall.p2()) 78 | } 79 | 80 | pub fn is_intersection_on_segment(intersection: Point, line: Line, p1: Point, p2: Point) -> bool { 81 | if intersection.is_same_as(&p1) || intersection.is_same_as(&p2) { 82 | return false; 83 | } 84 | if line.is_vertical() || line.m.abs() > 1.0 { 85 | return between(intersection.y, p1.y, p2.y); 86 | } 87 | between(intersection.x, p1.x, p2.x) 88 | } 89 | -------------------------------------------------------------------------------- /js/scene_builder.js: -------------------------------------------------------------------------------- 1 | export async function build_scene(data) { 2 | const points = data.walls.map(wall => [wall.p1, wall.p2]).flat(); 3 | let width = points.map(p => p.x).reduce((max, current) => max > current ? max : current, data.origin.x); 4 | let height = points.map(p => p.y).reduce((max, current) => max > current ? max : current, data.origin.y); 5 | const now = new Date(); 6 | const name = `${now.getFullYear()}-${now.getMonth()}-${now.getDay()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; 7 | const folder = await getOrCreateFolder(); 8 | const sceneData = [{ 9 | name: name, 10 | active: false, 11 | navigation: true, 12 | width: width, 13 | height: height, 14 | globalLight: false, 15 | grid: 100, 16 | gridDistance: 10, 17 | gridType: CONST.GRID_TYPES.GRIDLESS, 18 | initial: { x: data.origin.x, y: data.origin.y, scale: 0.5 }, 19 | tokenVision: true, 20 | folder: folder.id, 21 | fogExploration: false, 22 | }]; 23 | const wallData = data.walls.map(wall => { 24 | return { 25 | c: [wall.p1.x, wall.p1.y, wall.p2.x, wall.p2.y], 26 | door: wall.door, 27 | ds: wall.ds, 28 | sense: wall.sense, 29 | sound: wall.sound, 30 | dir: wall.dir, 31 | flags: { 32 | wallHeight: { 33 | wallHeightTop: wall.top, 34 | wallHeightBottom: wall.bottom, 35 | }, 36 | }, 37 | }; 38 | }); 39 | window.lsdata = data; 40 | const tokenData = [{ 41 | actorId: "", 42 | name: "Mr. Bug", 43 | actorLink: false, 44 | brightSight: (data.radius - 50) / 10, 45 | brightLight: 0, 46 | dimSight: 0, 47 | dimLight: 0, 48 | vision: true, 49 | height: 1, 50 | width: 1, 51 | scale: 1, 52 | hidden: false, 53 | sightAngle: data.angle, 54 | rotation: data.rotation, 55 | x: data.origin.x - 50, 56 | y: data.origin.y - 50, 57 | elevation: data.height, 58 | }]; 59 | let scene = await Scene.create(sceneData, { renderSheet: false }); 60 | if (["0.7.9", "0.7.10"].includes(game.data.version)) { 61 | await scene.createEmbeddedEntity("Wall", wallData); 62 | await scene.createEmbeddedEntity("Token", tokenData); 63 | } 64 | else { 65 | scene = scene[0]; 66 | await scene.createEmbeddedDocuments("Wall", wallData); 67 | await scene.createEmbeddedDocuments("Token", tokenData); 68 | } 69 | await scene.activate(); 70 | 71 | } 72 | 73 | function getOrCreateFolder() { 74 | const folderName = "Lichtgeschwindigkeit Gen."; 75 | let folder = Array.from(game.folders.values()).find(folder => folder.data.type === "Scene" && folder.data.name === folderName); 76 | if (folder) 77 | return folder; 78 | const folderData = [{ 79 | name: folderName, 80 | color: "#081d72", 81 | parent: null, 82 | sort: null, 83 | sorting: "a", 84 | type: "Scene", 85 | }]; 86 | return Folder.create(folderData).then(folders => folders[0]); 87 | } 88 | -------------------------------------------------------------------------------- /rust/tests/29-minimally_intersecting_walls.ascii85: -------------------------------------------------------------------------------- 1 | <~"*iP%Rt$6!eJ6,KLXh.-6:c`^2Le8[,,LN)>*0B=6#7In:o'@#V:;U]]BC`@Le*Z>Q-_-@Q*_F[RG$q;G36K+\$Oc@pGT9I.?_,RYsfq@$6hbOnIPZhO.\dj7f8DEhD:\.o?KLVq'1sho(gLXn%Klbms0+(-75pi^W]E"oT/-Y%_#4j7%Tp=r:'`A03eesmpis=j1I;=dXZ?'&PZ\[VouIFe`!t8,NO'2Veo.;PeVk+[5EeE]m=ImuV?CD*E%aDdUh[P`n=Z0B^qQ%kjTcFLMsAPV?bIG3/kO8?4RQ%if9cagW_+*R4/iHFlT>E]oeP<3"f0Iir]5AP$IU\s"_rGMKBO;R)hb?&=n9k'?:_6oI6UVS0-9joloM-0_NX\"K&GMtYJ?tp%Dp"d,mG.Y,H)i!)MpYC^TG:,'&2Z8WZft9r[`d1$O)"m=;F0fUs&u0%1CqF:oXc!7Uer3Co)6Y@`>oGD3iK?sM@VZUOp4Ftl@FY;RE80&&[145^.5%b]St#!=mFEOPmF0-8fUBaWr>9_SZE6]7FK@.m\n$Uoo1>:G0[Api9*7XV3;r*4VPZ&maZ5[lN/g-`dM0\]rOGQ'7Z049\jTts,A+&'Ln'LY+8N/eYc[89jq#Af44BQX\"HenD$^/0f=[4_A4c8LMT[o<"IUGS@t+!UDiR4j0kg=HMhoPVD*>o%ID'sCe\BG3%]Hdo\u#NkK$CJN3A]1LKt]40YU#Go<(_((uGqM_lap`(hYr^@Tn#RKY)YHf(Di>`]8'Z.?*qUVD-:g^N=2*W-QhA,mrZPfh$nf&0R\rN:X>N$_s=J]SU;&&)0hmq0/^50[>Tc]/3nXi.u+!NRsKY>oGEbP),thcB4Ys(nEXDCsT^.`jsfDBu%7OC?!r=@+7M[iN[=RR`)0%[S'O9FYBG2HR>gJlD!WC[H@PtUMQ8i;]+]o:6u0D>E=//.?+,eR^tY3;]+]O?C(kT,eVD-<'CL%i4l>O6jKlLuMFD8N=]*A'hq^R^FAN50t&PZ_^7mnCa$DVK`P1`c;Ki*HujH=rJ'LiDk4GuG122P1N^o&2Lg*,&aQ,:&N&CD1]4j8_O7r1n/["c=c7X,98[40^&.ZDrj/o<`jY=6GW$DV(UV(%@\hrC`Aa'8tHNUJ#dS!q2R2g2bh@!([*Q_lI^O^h<,Sc0k>4iCA[lf/3knF)/dlKqWZRj3eX]3ANAIVVQqFks8OcW3j%,h/@(Qi4IoARG2SF#)q+eO`&pk:k`mYiAd]SIT_-mN1Yf-B34"&LfksMmP^dDWmFtVuKi*jH1VH$*R`)`/mUb!F!i,F:5&Hq?SjM`N[O*_45D*H/CMOR3-`"+BnC"%d8q#P0\Q%mSfAfRV*h2C73MSe,?1La4:dTXA>T_/e&Yg$nn0#D/'EQPGn6tZX8hCN7KVlN\.mf=i/rN'`(=`Q&7H9$aj()_!pnBdWfpPZsROL@Ld]=r%&m1DF\Ue.[>!0i156DkG+U6K8.>lroNEMl8'"/`8I@l6p[HX'[M;jO-!632TMR%QRG:8[25>PZ^>5e-];/mR^V](gj$MD-Ja_P3AX-(4%asaE^_:W.pnr-fA##:^FUScG$e67d[lA9jsQ%YI;:lc^N(K""k$"[hd@*`]:=W8A@\o4^3(2QB/%U];SpQYPhL:+cffrXB;Nb?;c'`fijbnSNu-`_mOb6+)4mV+#AJ6(hMYi4D8LCUXbirD_\XX=p`B/3[jDAp.1m3"U?W[Z-h.*:F_;9e19";D,"JeN2aLB=CHO'Vu)er0-]/S8/+Ph/,5jE=/%1'nh$KBV6hpV3c/YY7MYKKXiW=Fjd9k-mHK8?-r76/I^YUn/Q<;K-Pue$!=shB%'ogq5[dW"f"^cS>E]m=G]KqAA8;esjkW*0[IXGQT/-;!2%$,uIO8l*@Pkm<]P=YF:`'Y_VifF$qK?n_g.;t`NnG.nL!eUcdBg*'G?@TS14E\53hDQQV->QH@@Y6(V\2]5EbQ7ORr"O^rdi)[I3P"QaEBFS_U%Bj!mbk$a]`W>*pL&G#Db=Z&NI+]6Lq\j->QZnlS8U2ps!UMmT?85*$:M@@Y6"2PPoomSK.+BFk1pV$be$7.&PZ^1dOrqM+Sg4M\+&:)JK6s4Fk+>&meAO(_;??%'AQ*=bc,Gp$.aGb54dn9/W?u=gQUN;p%,;4pP&NZI)ZCI3Re'~> 2 | -------------------------------------------------------------------------------- /rust/tests/17-old_closest_wall_paralell_to_ray_line.ascii85: -------------------------------------------------------------------------------- 1 | <~!dNG4Rt#*WB,SbY),\tIC)DR3p[&];6d1(*M_lTT?eE@u#*P.56[)O7$1Y7ppZdl3m(Ya%+gAJHPmZ6Gc7,(m\_(h3\B+b0BmK]6RO.fRn#hVAK1+BQ9me/ijF78q]o01J,K*C?2h*.HXc?[b5^?7N>4j.B1YL/b1VYWeYoCbkO[f,$bleCWos8DGZfDfdO)K1b3_`N7D\A#DBq0[$@EZpaL%^"$prfGN4]HBr`FI8cD9tg]<:%cMsk_<[S(n)?6*FQ6KXCD>:gG.)9HAUD9Ccp&:CKj$Df;5c6PK;,'%c7:VoBZtE@>6+`I0UUXW;+C;$tU6'#8k^>bJ(lQ*inB.;s]=L$"q`'rE'R-Y3*S>hdNC2IVVig!B@Xiq$o2;QLu8+1s`ho43Z'V52QB1o@-4d@>6,Cl8udpcF*EW1\Gp-8^Z,I9`EaiR%ZjcGC\(W[/hs\]];/iBX[bNJ`kAWn)J\*ep!LTr*OgV(sQ!WI>8]/Ia*(ed;7'Gn&CJM;=>#Hj:M5j4f!;)/Vme!=%;-8c>c9;u1qp\[]VNGN:q8o:'l@]EP_kqdMRj^'7&J6QI7jV"PVVEe(u99leaSl&Cb!)%,g;T.s+/M>]o<,NCOY+%/&@CLn@k39HBoVJur#7?W<%>-cq9/I0=WbZ.4EhG7_NZAqZ"_Q*ANaZ7bbg)oY;,j,`"L#G,R3IaK$Br-HQ:M(4WXdL^F(sNM"46SAnqm@DO1s^%2*G4NV3SsjlNf;JrV%&.mB6:nS",uA_YO=/4jUU/K7W\!?hdK"OD!l]'!CelsBVm"sSCFp3",pjXfRT*pcuaf3RFW'ICU2DcpBDY;H_p*]#8er^DbP"'S2`lW=*t5i.c&>?BLg\U7ZZ)?JC2S^Qg)Th!&UF)Ee(3(*Rd`i`C;",pi`_bggqI0SC,RHlD>eAn$6G(?9k(B8c4TnSL*q#I2#@>uSsaEi]u].=1(bmj+nK8r'p05UA'@.[P?moc@UBtDW5KH4GW>K6pVc(=SK?tbjudAHaL-dLq)/I*+Bnh\(WdBA,#5^AKA3`OkW1^V;LuD/VGMBV%HS43UNM)s;%i!J^0XAh[li*FQ5p/I*-;T`onXMsY'1;Cs.7EY^dZ<[90odDM_AK'?tf0VBB+FJN98.SrZOe_1%8I6PeoH+n&L0,K_GS-B'XhpU%'X^])b84C\bX*_[\AHhm4;11g>A5(i='3!@Cf2_#<3)5"b@XNUK6/,s;4Ko4aHKGttZpQBic'AYI'Ie(k]E:i+5=jOT$Y0U+`d42>OZ\>GC[M>9m!g`H[RCXc])ErjN,u&I+WXnd4B&:Ol=Ab_*N%'p2_qG1sS0+8*1h;0"0%1(qlR4GF6KIS=r9Amf5_/6iQl]P;r<2':O9M[K_J8ZIX5MdG26$)02OT:N#p1%ksFoC%iW[E,:nO6.VI5J=ia%#+c>\O&VIL`4lgZZ`4p&5cFqZ01XhKIdb7`VXm!2JTJ#H,K&",uA4gO/23[Wq3X0V#q`=kep`.sr'\MJR*iRekh!U`Q?GoBD";Jg)Q1BtoM0o6!2DE*jT)(5^H=Lu4uQQiOu3clQpmDtb8aBRZ68!2I*.bBNfoP/h_m79`E6U>'mZU!dM%/-)IFm7@7GUQ*UtJC/1=+(1Q&OA(Nm;4bD$HO2D&4h9YSUY0Aa@%(gbCTcYS]ujUuD?HKB.Z_sQS(IXuEirTFJ8pT$70Cg/\fjgP0V9CKWXEc9RTV6j@d"tBSOce\TKIHRR$ln@*s(/Im.Hcr`;>$[rPrp$pWq,6mXT:s]q.ec'e?`F!QWNC=Rq5`''>t/lg<;1Hu$7fohSU7C1qh;Xsa#3VEl>`Dn_T(;LcX)%k^Yc\R*u266c>3+i[GNJs^hrf4Rp*g\Z`F)o='n(X.G^\Hn[6UPI:2/HqYS_'Ko`=jH?J/rVJDWM$hE%*s)llU4ADHZA_FZNs/*XgHN*J>YL<'ch`_Tgq?@[Ph=0MJhn&8>]B85D`:Ep/heW#3~> -------------------------------------------------------------------------------- /rust/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fs::read_to_string; 2 | 3 | use crate::{ 4 | raycasting::{compute_polygon, Cache, TileCache, VisionAngle}, 5 | serialization::{deserialize_ascii85, TestCase}, 6 | }; 7 | 8 | fn run_test(filename: &str) { 9 | let test_root_dir = "tests/".to_owned(); 10 | let test = deserialize_ascii85::( 11 | &read_to_string(test_root_dir + filename + ".ascii85").unwrap(), 12 | ); 13 | let cache = Cache::build(test.call.walls, TileCache::from_roofs(test.call.roofs)); 14 | let (los, fov) = compute_polygon( 15 | &cache, 16 | test.call.origin, 17 | test.call.height, 18 | test.call.radius, 19 | test.call.distance, 20 | test.call.density, 21 | VisionAngle::from_rotation_and_angle(test.call.rotation, test.call.angle, test.call.origin), 22 | test.call.polygon_type, 23 | None, 24 | ); 25 | let e = 0.1; 26 | assert_eq!(test.los.len(), los.len()); 27 | for (expected, actual) in test.los.iter().zip(los) { 28 | assert!(expected.distance_to(&actual) < e); 29 | } 30 | 31 | assert_eq!(test.fov.len(), fov.len()); 32 | for (expected, actual) in test.fov.iter().zip(fov) { 33 | assert!(expected.distance_to(&actual) < e); 34 | } 35 | } 36 | 37 | macro_rules! raytracing_test ( 38 | ($name:ident,$path:expr) => { 39 | #[test] 40 | fn $name() { 41 | run_test($path); 42 | } 43 | }; 44 | ); 45 | 46 | raytracing_test!( 47 | limited_vision_angle_over_180_hidden_overflowing_wall, 48 | "limited_vision_angle_over_180_hidden_overflowing_wall" 49 | ); 50 | 51 | raytracing_test!( 52 | limited_vision_angle_overflow_end_hidden, 53 | "limited_vision_angle_overflow_end_hidden" 54 | ); 55 | raytracing_test!( 56 | limited_vision_angle_overflow_start_hidden, 57 | "limited_vision_angle_overflow_start_hidden" 58 | ); 59 | raytracing_test!(limited_vision_angle, "limited_vision_angle"); 60 | raytracing_test!(zero_width_walls, "zero_width_walls"); 61 | raytracing_test!(directional_walls_issue_4, "4-directional_walls"); 62 | raytracing_test!(t_junction_issue_5, "5-t_junction"); 63 | raytracing_test!(zero_length_walls_issue_6, "6-zero_length_walls"); 64 | raytracing_test!( 65 | overflow_wall_not_overflowing_in_fov_issue_14, 66 | "14-overflow_wall_not_overflowing_in_fov" 67 | ); 68 | raytracing_test!( 69 | overflow_wall_both_points_seen_issue_15, 70 | "15-overflow_wall_both_points_seen" 71 | ); 72 | raytracing_test!( 73 | overflow_wall_top_point_seen_issue_15, 74 | "15-overflow_wall_top_point_seen" 75 | ); 76 | raytracing_test!( 77 | overflow_wall_bottom_point_seen_issue_15, 78 | "15-overflow_wall_bottom_point_seen" 79 | ); 80 | raytracing_test!( 81 | overflow_wall_no_point_seen_wall_close_issue_15, 82 | "15-overflow_wall_no_point_seen_wall_close" 83 | ); 84 | raytracing_test!( 85 | overflow_wall_no_point_seen_wall_far_issue_15, 86 | "15-overflow_wall_no_point_seen_wall_far" 87 | ); 88 | raytracing_test!( 89 | old_closest_wall_pralell_to_ray_line_issue_17, 90 | "17-old_closest_wall_paralell_to_ray_line" 91 | ); 92 | raytracing_test!( 93 | origin_on_wall_endpoint_issue_19, 94 | "19-origin_on_wall_endpoint" 95 | ); 96 | raytracing_test!( 97 | limited_vision_angle_overflow_both_visible_issue_25, 98 | "25-limited_vision_angle_overflow_both_visible" 99 | ); 100 | raytracing_test!( 101 | almost_vertical_line_with_negative_incline_issue_27, 102 | "27-almost_vertical_line_with_negative_incline" 103 | ); 104 | raytracing_test!( 105 | minimally_intersecting_walls_issue_29, 106 | "29-minimally_intersecting_walls" 107 | ); 108 | 109 | raytracing_test!(tiny_vision_angle_issue_30, "30-tiny_vision_angle"); 110 | 111 | raytracing_test!( 112 | limited_angle_wall_to_the_right_with_end_visible_issue_31, 113 | "31-limited_angle_wall_to_the_right_with_end_visible" 114 | ); 115 | 116 | raytracing_test!( 117 | limited_angle_wall_to_the_right_with_start_visible_issue_32, 118 | "32-limited_angle_wall_to_the_right_with_start_visible" 119 | ); 120 | -------------------------------------------------------------------------------- /rust/tests/27-almost_vertical_line_with_negative_incline.ascii85: -------------------------------------------------------------------------------- 1 | <~"*iObRqZtEeJ6-^%5/kJ_5.iM'FI\d`W8N8$;+F4<+r%7,#Y+lg0,5E63/ej6\UaB$7'.)OEY%EnnqbV5Up7nKrb>Y:pMLu!Yumb(auab7LCS@ie)IK\Xrb?#4`p]m:?7bRJ-HKIEoOXj095iU;h9?#Nd=eY@puW^4!C?/+i`19=G@+'>$`D;-\k';UB#W*@AU6=Q/c=2:*t'/P$Rl)7F(LLuisl0^H/?:7[2QV\u74+R-s_oc6G^nBG^:ITX'-6nJ[6"V@2hHi&d\j:5\!5%p5.-etBnjHuPG"m7VOdMB*@:HN0YiR1>TW[)$[^--[jiV4dqC4n?W6lI$5XQ4fd/YJq4T&9!d\EP;CP+@QZ9gr\p,>M=ZQ;;?N"T30)h\?6jl:*+qE\'a['arAWl">I`)MMV(Wkaf\6iOpr5`=fH_7CqAiU.\od>PQjQn_G]4Ob*&#^p+UT`U+00/"1tORW7^629c1%E'[9A"/bAmdLtYZL6F8P.3j8!H[l55mpgI:?td)Pr:m]OrgI[XQj5g]5D5\g,;>mCO59@b"?YDKid'1CH#g2Sc^4K,/G,8p*!W5ShWiB\ZOX.\SddLU$VuB0[,g//m56SBJaknC1r.'8oYP<2aE3M\)#T:flj)//rF=D%+g^\6XBN'.+&;ZDJRQ@]\F5<6k[Ba.(5>p8j52I1q[N2X&'`&BWla:(gS^C!Uj:tMGneC$0gC]/P6,,`i_fRp2n=47.]VPQNlFC1:&B#&5k*;PVGfY-tUXRKtPp$;H19+C!#+a4c;J+4o&d5&\MSP"t5d(Y=^XWOsm%snI1I-ETQm78/WarfM:fg9e8dh*3]HIKo"\&]]5h;QA5K24U0W`K$Y`s52'3=p!Qb%X]1uiFGPg6jcODq]X11C\#JL>T@Wtni$8T:I+H54=)Me236(?_IU\"rV+.s`Zr9gGSiL0pi=s':M+8UY`0HsB(nee^JnkfG*6WO>fZ%-/KB^*?r)mZ8'L,\JcBZ4LMaOBW_Y6>e-tbDX=)DBd1^FR&"Y_0K2++X*@[Lmu,oYa^D.O[(iD.CR%[((Tib=q2J5i5<2eeW#d8mV5BpYeq1+2>);k^!WWN=4h3%A-3RoCeaE/&.f!QkZ-[+JE'qekY1%%^"Q)'i!XYQ))k\$m-na.QV'^oN8eq]$Ft(fWJhfD`FmS6CrqPa]5"K-9d5Cs>+i4\GNeah)iNEq/h4TS;1R[i-2(#o^9$=HonY/*/9YjGZ"=%g^R7q&&)ACpLQb@?D3(7+Ed1(,'A9NqN'A[>+A_7W]f.QA/^1O]LQaK]oR/=`hnY;i'(4.'V3Pcp5J-aUl$'p2s+UhpaB>KmB`/U+TT5@P!XB]oRiHDfa7I_C"I%;Ra8r4-=DR35PR*ATWd5fV*PXaHDcglUf,UGq8T\'k1.XgWpZe..Sj/L+H<%j.GpL2YXG#]&3ET<>6KJH;$Pb?!%@7!QK>>Uimj>JcPg`>rfca)/(Uk8>.;1_V^$bnAnA`a1c9J>'\#uXH=j-l\S4Q8g\/]5XhnR,RuSo5H>G]Hi4fLSYk)\4k(7AH5\DDXhZ&gRf(=#*Jr;$?C0Ml="#Rc:utfj9'8&gNi@`EbNE`6b>:68mUe:ANW`)D*+M<,SPM/LhJO$&t>4nE)YfX(46t_RAE?(2YlXNOdHbFUJpgcfi4l_f/+t?%NBtZ6sSh/V1Onf3EQDU\U@:\8aU.S[LrGb.!H:.%UnlMFVnT?8&]Iuh.UcuJaBQ(KZIC?6!)[.\nI#OI;X-_IIZ"]!r\@t/Ub3I$/S4b[/l:.[UXnselj/`^Jj@lc%Dn$&=PLia5g@PJ[l2,lP,8KQ.dp#nmT*2'2d%[*V561S!M+_Kb^B^9l^(&CpRgjP/GOXON%tdgW`+rZ8RgsW8nDKSDD/dXlkiAZkk\j(]OF6oef,P]P"hDK-3.WFMqjk_k4#Pa%"lHlr9<\>f!M"_e%%8$&$GJBOHXic[KCb#'X*/AU!T2V\rT!lT$;U/BN8;0[J8[g,D=:GXrnmZ`;KAlA^.mGT+XPXpV!VrdDGC2hf!O<&Qd5`2j*@P1M%.1fELS=&h@IoF)$M\Ns(h*ERPJCiQ)B014[;SsE/j4TorXnQtCf49U3cpI++JGUmmse9upuo#Rc^i\_:G+2/k8i`WMCh;8.KMI$iRRJGS0g"4%=`;-(g`=rTJfJEG$egRccASp<#oA-,?5CBn[a8rN';Eq1j(.-sFYi+4#MMJpPoeZeHhq,WIF,8^6n<5JGD$ZX@G3XO\0A+=.k3eb+"G$qDhaq/,)XR&3[h&F`1BbFo8qnr;_7l>oc[#/"#[H]LA)EPnn@/Dg]#L>&K4Bo/s1g#fr9`OR4C.se3,sH3QsQ6cr\"5<1Y(dnVp0Y3WJ=d]J%)^9issXjdsfZ5WCJ:d^W5.+;?%*Eo1ri$7n16(rn^#5r]=W+H*Zi6Bs580c:9d!A:At4e*1nP~> 2 | -------------------------------------------------------------------------------- /rust/src/raycasting/prepare.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::Point; 2 | use crate::ptr_indexed_hash_set::PtrIndexedHashSet; 3 | use crate::raycasting::types::{Cache, Endpoint, VisionAngle, Wall}; 4 | use crate::raycasting::vision_angle::restrict_vision_angle; 5 | use crate::raycasting::{DoorState, DoorType, PolygonType, WallSenseType}; 6 | use rustc_hash::FxHashMap; 7 | use std::cell::RefCell; 8 | use std::f64::consts::PI; 9 | use std::mem::swap; 10 | use std::rc::Rc; 11 | 12 | pub fn prepare_data( 13 | cache: &Cache, 14 | origin: Point, 15 | height: f64, 16 | vision_angle: &Option, 17 | polygon_type: PolygonType, 18 | ) -> (Vec>>, PtrIndexedHashSet) { 19 | let wall_bases = &cache.walls; 20 | // TODO Cell/RefCell introduces runtime overhead 21 | let mut endpoints = FxHashMap::default(); 22 | let mut start_walls = PtrIndexedHashSet::new(); 23 | let mut walls = Vec::with_capacity(wall_bases.len()); 24 | let mut restricted_walls = Vec::new(); 25 | 26 | for wall in wall_bases { 27 | if wall.p1 == wall.p2 { 28 | continue; 29 | } 30 | if wall.p1 == origin || wall.p2 == origin { 31 | continue; 32 | } 33 | if (wall.line.is_vertical() && wall.p1.x == origin.x) 34 | || (wall.line.is_horizontal() && wall.line.p1.y == origin.y) 35 | { 36 | continue; 37 | } 38 | 39 | if wall.door != DoorType::NONE && wall.ds == DoorState::OPEN { 40 | continue; 41 | } 42 | 43 | let sense = wall.current_sense(&cache, polygon_type); 44 | if sense == WallSenseType::NONE { 45 | continue; 46 | } 47 | 48 | if wall.height.bottom > height || wall.height.top < height { 49 | continue; 50 | } 51 | 52 | let e1 = endpoints 53 | .remove(&wall.p1) 54 | .unwrap_or_else(|| Rc::new(RefCell::new(Endpoint::new(origin, wall.p1)))); 55 | let e2 = endpoints 56 | .remove(&wall.p2) 57 | .unwrap_or_else(|| Rc::new(RefCell::new(Endpoint::new(origin, wall.p2)))); 58 | 59 | // Check if the wall's line goes through the light sources center. 60 | // If so, the wall doesn't have any width and doesn't influence light calculation 61 | if e1.borrow().angle == e2.borrow().angle 62 | || (e1.borrow().angle - e2.borrow().angle).abs() == PI 63 | { 64 | endpoints.insert(wall.p1, e1); 65 | endpoints.insert(wall.p2, e2); 66 | continue; 67 | } 68 | 69 | let mut start; 70 | let mut end; 71 | if e1.borrow().angle < e2.borrow().angle { 72 | start = e1; 73 | end = e2; 74 | } else { 75 | start = e2; 76 | end = e1; 77 | } 78 | let is_start_wall; 79 | if end.borrow().angle - start.borrow().angle > PI { 80 | swap(&mut start, &mut end); 81 | is_start_wall = true; 82 | } else { 83 | is_start_wall = false; 84 | } 85 | 86 | let wall = Rc::new(Wall::from_base( 87 | *wall, 88 | Rc::clone(&end), 89 | &cache, 90 | polygon_type, 91 | )); 92 | if let Some(split_walls) = restrict_vision_angle(&wall, &start, &end, &vision_angle) { 93 | for wall in &split_walls { 94 | if let Some(wall) = wall { 95 | restricted_walls.push(*wall); 96 | } 97 | } 98 | } else { 99 | walls.push(Rc::clone(&wall)); 100 | start.borrow_mut().starting_walls.push(Rc::clone(&wall)); 101 | end.borrow_mut().ending_walls.push(Rc::clone(&wall)); 102 | 103 | if is_start_wall && !wall.is_see_through_from(-PI) { 104 | start_walls.insert(Rc::clone(&wall)); 105 | } 106 | } 107 | 108 | let start_point = start.borrow().point; 109 | let end_point = end.borrow().point; 110 | endpoints.insert(start_point, start); 111 | endpoints.insert(end_point, end); 112 | } 113 | 114 | for wall in restricted_walls { 115 | let e1 = endpoints.remove(&wall.p1).unwrap_or_else(|| { 116 | Rc::new(RefCell::new(Endpoint::new_with_precomputed_angle( 117 | wall.p1, 118 | wall.angle_p1, 119 | ))) 120 | }); 121 | let e2 = endpoints.remove(&wall.p2).unwrap_or_else(|| { 122 | Rc::new(RefCell::new(Endpoint::new_with_precomputed_angle( 123 | wall.p2, 124 | wall.angle_p2, 125 | ))) 126 | }); 127 | 128 | let mut start; 129 | let mut end; 130 | if e1.borrow().angle < e2.borrow().angle { 131 | start = e1; 132 | end = e2; 133 | } else { 134 | start = e2; 135 | end = e1; 136 | } 137 | let is_start_wall; 138 | if end.borrow().angle - start.borrow().angle > PI { 139 | swap(&mut start, &mut end); 140 | is_start_wall = true; 141 | } else { 142 | is_start_wall = false; 143 | } 144 | 145 | let wall = Rc::new(wall.to_wall(Rc::clone(&end))); 146 | walls.push(Rc::clone(&wall)); 147 | start.borrow_mut().starting_walls.push(Rc::clone(&wall)); 148 | end.borrow_mut().ending_walls.push(Rc::clone(&wall)); 149 | 150 | if is_start_wall && !wall.is_see_through_from(-PI) { 151 | start_walls.insert(Rc::clone(&wall)); 152 | } 153 | 154 | let start_point = start.borrow().point; 155 | let end_point = end.borrow().point; 156 | endpoints.insert(start_point, start); 157 | endpoints.insert(end_point, end); 158 | } 159 | 160 | for intersection in &cache.intersections { 161 | if height >= intersection.height.bottom && height <= intersection.height.top { 162 | endpoints 163 | .entry(intersection.point) 164 | .or_insert_with(|| Rc::new(RefCell::new(Endpoint::new(origin, intersection.point)))) 165 | .borrow_mut() 166 | .is_intersection = true; 167 | } 168 | } 169 | 170 | let mut sorted_endpoints = endpoints 171 | .values() 172 | .filter(|val| { 173 | val.borrow().is_intersection 174 | || val.borrow().starting_walls.len() + val.borrow().ending_walls.len() > 0 175 | }) 176 | .map(|val| Rc::clone(&val)) 177 | .collect::>(); 178 | sorted_endpoints 179 | .sort_unstable_by(|e1, e2| e1.borrow().angle.partial_cmp(&e2.borrow().angle).unwrap()); 180 | 181 | (sorted_endpoints, start_walls) 182 | } 183 | -------------------------------------------------------------------------------- /rust/tests/5-t_junction.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m5BS8F4Tf2]lc=\PKbEg,L;'W&g&OB19T'.RK8&flR6\Kt2ZdSNB+Z9jA;-n8(#-E'a-M5Vkc1#TE6]&%_eRG"!VSt0M+B0El+"]c='=sYL\e:JY'dZtG2<)leOgaGc6n+/d,Soa>UD*QTY-Kg`qh"Q0`\M(9f;q&5(?gjGpn;eX'XSNo$C[CE)IsVXZo6XpUR&$K7Nbt4dDoug&B*fpYl79.%e6Z?kfNEh%][o$[@`;(^WBQ)UX?KED(Gq0"E*#B4b@tjr5%cNM)Tugh;:tUlNFasE7.j^;Y;DQeGNqC0VZS8kf7,q9&"(k?IOM?8(mV;AM4ilLWJIV6Q!M;mF=sb3t#^N7(ft3QD&T]ArIT*`W%cm+[cE1Y9>cNT;Q\aU4M02_L>0m/I.ru:[68n/_JZ+`-"#_'ZGh`')SE\GRf0j@0Hg8YT&UY;DDiu$;V_LJL=jqoti&o/I#]0-gC^o2fBD8/0^h(bn,0!hAF%Eq,^YP(V(^?=&XG"UiKoYL^q;ReBac+''<'W-ot.'o+jtp=uo,`Z1a3k#qT#31W(22fn^34%h8amoYdmN*;%Xk>XS4R+Uf0D`l&%%iBXV0?:$#eS6r,]\dJdDVKF(bWcVB`TiJ?XiEQ1%f/$>Dn4PkV:,M](V<@%Z>d:D[>jXnI($M64gQA!C=ACE0)ho;(t,V7G5mG^S+C]dsQbm;Y0pe3>%9qq-pXWUh`rH9P'?$2:?JG!5`0(S"A[HZ]q2T`#6[UlN0l)$"?t.G'N&>!PT=e;%%El\]\KO]QN-Z;Z)TSec!rG5biuFinDQ?>_Yoe9qq+S9lCngsq.NWYiRV;HlI7e";N.^tLTm[oU#3)!'X/\rIUW+k=[(_BOA"5!EZ&IsTYaR+T<4ak[a"ahpWjT/+&i;`%i*UK$Z(@<6e/pPk3ZXaT2O**d^"4s<5A&WbUr.%GK25!r;O-DKgKub(YOt+XD2@ktnWMEZe65\dq-sMLk&]*CIH,FZs*2;B`Jb]g>>9]H"$TBH;d+tb&J%bQD5h#Ba/C^NA&#%q%Ap:;2\2oH[#A`>6gD&cI^t4nMYAC,p3!Qhn\,W_f_e*&1]1B0S1!eI1;@11/WO%043ErYX)UFF8rHc5O1mV"!(o=.u*OtK_mst[Y;brApa!c_OVtNj(Vbdoj0+7QBrhFIY^a*jNdg8K(cB%jH#0k"'(h0WH?LqFMQu(]2WukNlfsuq:hpnQH@g(WFA*4_EF:?a#,J,9[:@FeNT!j';VW67;:r]&W^b\5p!JqQ%CM`FlIalMRlG\[uh3Q=iM>\J0iYqDd@c*^TZc*SMmUgA28MYA!ZQNVIXtFi,f8D[@=g6O(2P_-[`&]r,@\#$S`HBe5IUW]Uir?XRmY>,qJKiL@Z4&"b7sWR$ROFsVN9#p1sJ/;u;lLp;52*2n2q2o!6Jo*8_6(tC%c3%">im<9KKBiqUK`4grSn`@;8^Ub9&_dC=Vl'PSG]">eG.!IpD`ids#gKtuf&'FVr%=`<"*NQCpL->1eEbD"AF2p$r0@Li[`%F7dCV]!^Y5A[8%WYoaN:jWHp8Z8fo8""I:L>o$[:eR0*UkXis/ON*N+hhcQrgnZSL'Yn#E=1-Be5J>T[>DANFd,O1qHld%Ei;.5>l9S@hK:]s%d>[UOqrIMVpE5UB5Wu_u!E$Yq?QL*hS8teX9FOc*h]m(>/(MBQ0Ogp<-mg0ArG,`4YAD*@m`g5/Gj!LsPXpWK#O(9B5inF--@ur7KY)Jr.1fN&=uU+)6dF5)-/uKJU^8($df9QX_,V)R2LBg5u',"nm2R-SA@[fK^s6M?mFjthiV#>d,'/B41ZhXk@i[KCK'g?Qe2+o]j@:US0JFsVruIQV)m5f1)=*]H@a_lS4hq!\a$(eRoY!0j%A[cpq9(u@f@]`1u@9#0in01LGJaHaS)@^%h:CShZrN%4#e.+*T+,f*&$e%ktcsen+YYURuL^m>$*Qc]WNLJ`A`N,9E*`HltcShN??)T*'\MV_li!3^Fj_h?1f#Z%u\JEIhaG>mD/0An66Lu6UsM3)t:m.qP8[^Qlcb(_f'N+HW"oX4>6K2'E1JH7i0^h\RT+?IOC%VYWnl1_>]hrQ&U!:B+=;?#C!5]/31qZZmZ`6-R;13(DZZ?5nIjkh9p!MMj*h3t)e6,(Mf#A=CpdL4<1?B&*gSfaJ)Z?@%LVNRapf7*DOa;B4=Kp\Jd(BaT-P@>hkNMC14,odW(sR-6/,piq#L2iHc1@g861_jrB)'$foKjKhh":Vi*?1t:D\D4Wf'6]43@og&Z!dTjl?BVgdOr-a0AR>Gh-^l9//?=R>=L3GY=gMONFUJTk?.+7*=MR#Tc)UtQ7n0f!i%+1*LHG'a#,R3eO6LGI&59DmtlO!r^YL&QHB!"23-'UoZ=C"K6<,bG/SNI/D/\jRdE#:V'Btd?kRtQIWs"s'bcc^I2VF)20f$DWdQFn-J$f=FU?<8/tJbG?HDl$g9kYmGuYp/jR-m_AYl[19[1b_-;1/P^0'R[R&i%&cc,2[R,!sde_\+sWZgdNIn"-W3]$Zno$jn8NuLWFm5mX[C7*3)gKf*]ROA^Eo*kYBAD>*BBo?iuqDI-$(2SEuj6BsgfH5TFX\FVRQMqU_F3<-2u!r]@('t1B)]8?Ad2\s5*T>^7SV1G6*_DCrF(Eaq?8XgCp%C2D=G.*MYCKlPu[?G=#dW/Nsj2jVTT;Mp(HhYb+X'T!`k.2KVX8Vq399I,?4HMs^%uYm*nq@7G=ZBm*]T(:-h5Q-NcR2$<3%Qf"GmQI167f5!]ZNmRji>K\m(B)&K>pf4T&8SG93)BSKQb_#flEB7K:ZTlZC(@:>A5+C-fggS5cY6?8VY1%K&NTBb#m%5:3gZ2q6m%97!@Ac+"Y@!;e\L!B`Q45[;,W(jt0r?d[3NHSXI9dVp@,I3ilUb0LDF/ui(jmLX3#6>\5)prDa8L0[IY4VLbNW."\"&u6j?_jX3-q<.)olUu1"V:Dqf47?HP.78+sY$V/*b?p0cIlhXSeC4=@T'pa5q<"L?ITC3CZBS^Ctb9kSObKDK8@OK9"%n4EW8_S%rQ0SpE/`Vl-'RSQls-?8*69PFmfS+W-i"cpZ^#3(="%Xk!m#A0'+clknW$RsoR?MDFUDa(XGtZ,X%Vs&_Td0o:Z4h[CFDjPF:=c;u$M4.(l9s)KZMq/3%r=N%6l\s^\+gik/O[G0q(08[uY$>*]-aW/WN]ob.-=LPPqIuB?+Q?X#5pbm,#5]*M]%1GWa;7B?.@2B+l2Rl1/!'Wk*h+9]"'trimr6AQ"<6a5_Js8R#kfA(8FX]rKq23CYUu/u744Te@s\rmQ"6/D,:^)RNo\k?F--V6MQi)q,+QN4HpG3*^Q8rkZO?pN,N(M7N4\B[4ZaWFNd$PL/[$H0M`b0l!o2rn0#OS$f!I&j$lqZO+lZQe0nY?>"*olLF4CY]!0Ji2?DM;NF2c`38Fqm$O?JB-@7QiP;jod-p?]5POI]nFA0t8o[n+Vp:Wq*RCpYjIHjqes/9&k*U%V?t0AIW,Jn+KB5,*6qlg<`hMXVS-nd;5`mgjDf?N.=[X=dau7S[r9SAa)=fNqp`9o*fo1eLdq>$OQ/BNA&Pg)fAtd1KsrT_YNUC-8n6bk"T(,W^%a/rhE-P,270'i.H<>mYjqGmWcSgDo@pL'Z*>egI+488)"Co/T8(#@dAM4f7_RQ9D;N'I^%h>FGb[7#2=aLshe)522.5D1jn3V"GZu3FAuOUc7di;$YZ:S12qgIOCAm-Ng\gdBE'/UEn+Q8'E,TcsQjl6Lhd\Um2u3su^I=F^89;dG^00H>"^4k(1YN^XG/%`_ra&(NjD7g7s$gZ/[e^H1J%$tIRt!A1h"r=QKCsR:B&eqV%IU0XYr\e`IAUKe:U;C+RG>49FSNS.2/MT,Y%(Gg*GZW&2D/@gbCd.:8IHI3hT;BoRI[%N"%lh`Y#T4G.GktK,K\Z^t[04:9]'$J_t.BN>Y3h>N[1qPY;a5%CT=Kp>fDKg:Mn"f_R@6/#ZcLm2c3aJ(I2j$keV5*HE^)N&]Qf?mR9i"2r%>'!RTfM*hWdXfWDZ5V2lma%QhDs6_-T(QZNI#C-s8E5W*VDL$hP9VO0AHG?m\H6:+gofAKs*o_Lf<";P(X)GfmhWt^~> 2 | -------------------------------------------------------------------------------- /rust/src/raycasting/js_api.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::Point; 2 | use crate::raycasting::types::{ 3 | Cache, PolygonType, TileCache, TileId, VisionAngle, WallBase, WallHeight, 4 | }; 5 | use crate::raycasting::{compute_polygon, DoorState, DoorType, WallDirection, WallSenseType}; 6 | use js_sys::{Array, Object}; 7 | use rustc_hash::FxHashMap; 8 | use wasm_bindgen::prelude::*; 9 | 10 | #[allow(unused)] 11 | macro_rules! log { 12 | ( $( $t:tt )* ) => { 13 | log(&format!( $( $t )* )); 14 | }; 15 | } 16 | 17 | #[wasm_bindgen(js_name=computePolygon)] 18 | #[allow(dead_code)] 19 | pub fn js_compute_polygon( 20 | cache: &Cache, 21 | origin: JsValue, 22 | height: f64, 23 | radius: f64, 24 | distance: f64, 25 | density: f64, 26 | angle: f64, 27 | rotation: f64, 28 | polygon_type: &str, 29 | internals_transfer: Option, 30 | ) -> Object { 31 | let origin = Point::from(&origin.into()); 32 | let polygon_type = PolygonType::from(polygon_type); 33 | let (los, fov) = compute_polygon( 34 | &cache, 35 | origin, 36 | height, 37 | radius, 38 | distance, 39 | density, 40 | VisionAngle::from_rotation_and_angle(rotation, angle, origin), 41 | polygon_type, 42 | internals_transfer, 43 | ); 44 | let result = Object::new(); 45 | js_sys::Reflect::set( 46 | &result, 47 | &JsValue::from_str("los"), 48 | &los.into_iter().map(JsValue::from).collect::(), 49 | ) 50 | .unwrap(); 51 | js_sys::Reflect::set( 52 | &result, 53 | &JsValue::from_str("fov"), 54 | &fov.into_iter().map(JsValue::from).collect::(), 55 | ) 56 | .unwrap(); 57 | result 58 | } 59 | 60 | #[allow(dead_code)] 61 | #[wasm_bindgen(js_name=updateOcclusion)] 62 | pub fn update_occlusion(cache: &mut Cache, js_tile_id: &str, occluded: bool) { 63 | if let Some(id) = cache.tiles.id_map.get(js_tile_id) { 64 | cache.tiles.occluded[*id] = occluded; 65 | } 66 | } 67 | 68 | #[allow(dead_code)] 69 | #[wasm_bindgen(js_name=buildCache)] 70 | pub fn build_cache(js_walls: Vec, enable_height: bool) -> Cache { 71 | let mut occluded = vec![]; 72 | let mut id_map = FxHashMap::default(); 73 | let mut walls = Vec::with_capacity(js_walls.len()); 74 | for wall in js_walls { 75 | let wall = JsWall::from(wall); 76 | let roof = if let Some(roof) = wall.roof() { 77 | let next_id = occluded.len(); 78 | let id = id_map.entry(roof.id()).or_insert_with(|| { 79 | occluded.push(roof.occluded()); 80 | next_id 81 | }); 82 | Some(*id) 83 | } else { 84 | None 85 | }; 86 | walls.push(WallBase::from_js(&wall, roof, enable_height)); 87 | } 88 | Cache::build(walls, TileCache { occluded, id_map }) 89 | } 90 | 91 | #[allow(dead_code)] 92 | #[wasm_bindgen(js_name=wipeCache)] 93 | pub fn wipe_cache(cache: Cache) { 94 | drop(cache); 95 | } 96 | 97 | #[wasm_bindgen] 98 | extern "C" { 99 | #[wasm_bindgen(js_namespace = console, js_name=warn)] 100 | pub fn log(s: &str); 101 | } 102 | 103 | #[wasm_bindgen] 104 | extern "C" { 105 | 106 | pub type JsWall; 107 | pub type JsWallData; 108 | pub type JsWallFlags; 109 | pub type JsWallHeight; 110 | pub type JsTile; 111 | 112 | #[wasm_bindgen(method, getter)] 113 | fn data(this: &JsWall) -> JsWallData; 114 | 115 | #[wasm_bindgen(method, getter)] 116 | fn roof(this: &JsWall) -> Option; 117 | 118 | #[wasm_bindgen(method, getter)] 119 | fn id(this: &JsTile) -> String; 120 | 121 | #[wasm_bindgen(method, getter)] 122 | fn occluded(this: &JsTile) -> bool; 123 | 124 | #[wasm_bindgen(method, getter)] 125 | fn c(this: &JsWallData) -> Vec; 126 | 127 | #[wasm_bindgen(method, getter)] 128 | fn door(this: &JsWallData) -> DoorType; 129 | 130 | #[wasm_bindgen(method, getter)] 131 | fn ds(this: &JsWallData) -> DoorState; 132 | 133 | #[wasm_bindgen(method, getter, js_name = "move")] 134 | fn movement(this: &JsWallData) -> WallSenseType; 135 | 136 | #[wasm_bindgen(method, getter)] 137 | fn sense(this: &JsWallData) -> WallSenseType; 138 | 139 | #[wasm_bindgen(method, getter)] 140 | fn sound(this: &JsWallData) -> WallSenseType; 141 | 142 | #[wasm_bindgen(method, getter)] 143 | fn dir(this: &JsWallData) -> Option; 144 | 145 | #[wasm_bindgen(method, getter)] 146 | fn flags(this: &JsWallData) -> JsWallFlags; 147 | 148 | #[wasm_bindgen(method, getter, js_name = "wallHeight")] 149 | fn wall_height(this: &JsWallFlags) -> Option; 150 | 151 | #[wasm_bindgen(method, getter, js_name = "wallHeightTop")] 152 | fn top(this: &JsWallHeight) -> Option; 153 | 154 | #[wasm_bindgen(method, getter, js_name = "wallHeightBottom")] 155 | fn bottom(this: &JsWallHeight) -> Option; 156 | } 157 | 158 | impl WallBase { 159 | pub fn from_js(wall: &JsWall, roof: Option, enable_height: bool) -> Self { 160 | let data = wall.data(); 161 | let c = data.c(); 162 | let height = if enable_height { 163 | data.flags().wall_height().into() 164 | } else { 165 | WallHeight::default() 166 | }; 167 | Self::new( 168 | Point::new(c[0].round(), c[1].round()), 169 | Point::new(c[2].round(), c[3].round()), 170 | data.movement(), 171 | data.sense(), 172 | data.sound(), 173 | data.door(), 174 | data.ds(), 175 | data.dir().unwrap_or(WallDirection::BOTH), 176 | height, 177 | roof, 178 | ) 179 | } 180 | } 181 | 182 | impl From> for WallHeight { 183 | fn from(height: Option) -> Self { 184 | let height = height 185 | .map(|height| (height.top(), height.bottom())) 186 | .unwrap_or((None, None)); 187 | let top = height.0.unwrap_or(WallHeight::default().top); 188 | let bottom = height.1.unwrap_or(WallHeight::default().bottom); 189 | Self { top, bottom } 190 | } 191 | } 192 | 193 | impl From<&str> for PolygonType { 194 | fn from(value: &str) -> Self { 195 | match value { 196 | "sight" => Self::SIGHT, 197 | "light" => Self::LIGHT, 198 | "sound" => Self::SOUND, 199 | "move" => Self::MOVEMENT, 200 | _ => { 201 | log!( 202 | "Lichtgeschwindigkeit | Unknown polygon type '{}', assuming 'sight'", 203 | value 204 | ); 205 | Self::SIGHT 206 | } 207 | } 208 | } 209 | } 210 | 211 | #[wasm_bindgen] 212 | extern "C" { 213 | pub type InternalsTransfer; 214 | 215 | #[wasm_bindgen(method, setter)] 216 | pub fn set_endpoints(this: &InternalsTransfer, endpoints: Vec); 217 | } 218 | -------------------------------------------------------------------------------- /rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ascii85" 7 | version = "0.2.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1f7b2cc50ccfca05cc3e99a014901ae232948108082a2eecebc3ab6544ebd938" 10 | 11 | [[package]] 12 | name = "bumpalo" 13 | version = "3.7.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "0.1.10" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 22 | 23 | [[package]] 24 | name = "cfg-if" 25 | version = "1.0.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 28 | 29 | [[package]] 30 | name = "console_error_panic_hook" 31 | version = "0.1.6" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" 34 | dependencies = [ 35 | "cfg-if 0.1.10", 36 | "wasm-bindgen", 37 | ] 38 | 39 | [[package]] 40 | name = "js-sys" 41 | version = "0.3.55" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" 44 | dependencies = [ 45 | "wasm-bindgen", 46 | ] 47 | 48 | [[package]] 49 | name = "lazy_static" 50 | version = "1.4.0" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 53 | 54 | [[package]] 55 | name = "lichtgeschwindigkeit" 56 | version = "1.4.9" 57 | dependencies = [ 58 | "ascii85", 59 | "console_error_panic_hook", 60 | "js-sys", 61 | "nom", 62 | "partial-min-max", 63 | "rustc-hash", 64 | "wasm-bindgen", 65 | "yazi", 66 | ] 67 | 68 | [[package]] 69 | name = "log" 70 | version = "0.4.14" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 73 | dependencies = [ 74 | "cfg-if 1.0.0", 75 | ] 76 | 77 | [[package]] 78 | name = "memchr" 79 | version = "2.4.1" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 82 | 83 | [[package]] 84 | name = "minimal-lexical" 85 | version = "0.1.3" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "0c835948974f68e0bd58636fc6c5b1fbff7b297e3046f11b3b3c18bbac012c6d" 88 | 89 | [[package]] 90 | name = "nom" 91 | version = "7.0.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1" 94 | dependencies = [ 95 | "memchr", 96 | "minimal-lexical", 97 | "version_check", 98 | ] 99 | 100 | [[package]] 101 | name = "partial-min-max" 102 | version = "0.4.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "6448add382c60bbbc64f9dab41309a12ec530c05191601042f911356ac09758c" 105 | 106 | [[package]] 107 | name = "proc-macro2" 108 | version = "1.0.29" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" 111 | dependencies = [ 112 | "unicode-xid", 113 | ] 114 | 115 | [[package]] 116 | name = "quote" 117 | version = "1.0.9" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 120 | dependencies = [ 121 | "proc-macro2", 122 | ] 123 | 124 | [[package]] 125 | name = "rustc-hash" 126 | version = "1.1.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 129 | 130 | [[package]] 131 | name = "syn" 132 | version = "1.0.76" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84" 135 | dependencies = [ 136 | "proc-macro2", 137 | "quote", 138 | "unicode-xid", 139 | ] 140 | 141 | [[package]] 142 | name = "unicode-xid" 143 | version = "0.2.2" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 146 | 147 | [[package]] 148 | name = "version_check" 149 | version = "0.9.3" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 152 | 153 | [[package]] 154 | name = "wasm-bindgen" 155 | version = "0.2.78" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" 158 | dependencies = [ 159 | "cfg-if 1.0.0", 160 | "wasm-bindgen-macro", 161 | ] 162 | 163 | [[package]] 164 | name = "wasm-bindgen-backend" 165 | version = "0.2.78" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" 168 | dependencies = [ 169 | "bumpalo", 170 | "lazy_static", 171 | "log", 172 | "proc-macro2", 173 | "quote", 174 | "syn", 175 | "wasm-bindgen-shared", 176 | ] 177 | 178 | [[package]] 179 | name = "wasm-bindgen-macro" 180 | version = "0.2.78" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" 183 | dependencies = [ 184 | "quote", 185 | "wasm-bindgen-macro-support", 186 | ] 187 | 188 | [[package]] 189 | name = "wasm-bindgen-macro-support" 190 | version = "0.2.78" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" 193 | dependencies = [ 194 | "proc-macro2", 195 | "quote", 196 | "syn", 197 | "wasm-bindgen-backend", 198 | "wasm-bindgen-shared", 199 | ] 200 | 201 | [[package]] 202 | name = "wasm-bindgen-shared" 203 | version = "0.2.78" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" 206 | 207 | [[package]] 208 | name = "yazi" 209 | version = "0.1.4" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "c03b3e19c937b5b9bd8e52b1c88f30cce5c0d33d676cf174866175bb794ff658" 212 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.4.9 2 | ### Bugfixes 3 | - Fixed another bug where walls to the right of a token could become see-through if one of it's end points was hidden due to the token's vision angle 4 | 5 | ## 1.4.8 6 | ### Bugfixes 7 | - Fixed a bug where walls to the right of a token could become see-through if one of it's end points was hidden due to the token's vision angle 8 | 9 | 10 | ## 1.4.7 11 | ### Bugfixes 12 | - Fixed a bug where a small vision angle could allow the vision to penetrate through walls 13 | 14 | 15 | ## 1.4.6 16 | ### Bugfixes 17 | - Fixed a bug that could cause walls to become see-through if their walls aren't snapped to the grid (or the subgrid) 18 | 19 | 20 | ## 1.4.5 21 | ### New features 22 | - Small speed improvement for sight calculation when using a restricted vision angle 23 | 24 | ### Bugfixes 25 | - Fixed a bug that could cause the vision calculation to crash in specific wall constellations 26 | - Fixed multiple bugs that could cause incorrectly calculated sight area when a restricted vision angle was being used (especially if that angle is bigger than 180°) 27 | 28 | 29 | ## 1.4.4 30 | ### Bugfixes 31 | - The vision calculation algorithm no longer crashes when a token's vision radius is set to 0. 32 | 33 | 34 | ## 1.4.3 35 | ### Bugfixes 36 | - Fixed a bug where the fog of war calclation could fail after switching between maps of vastly different sizes 37 | 38 | 39 | ## 1.4.2 40 | ### Bugfixes 41 | - Fixed a bug where the background image of a map could occasionally flicker on small maps with For of War enabled 42 | 43 | 44 | ## 1.4.1 45 | ### Bugfixes 46 | - The vision calculation algorithm no longer crashes when stepping under a roof that has no walls below it 47 | 48 | 49 | ## 1.4.0 50 | ### New features 51 | - The cache that was introduced in 1.3.0, but disabled in 1.3.2 due to severe bugs is now fixed. It's now enabled again. This change makes the vision calculation about 20% faster than it was in 1.3.4. 52 | - Lichtgeschwindigkeit now properly separates walls at different elevations that were placed with the Wall Height or Levels module, leading to a potentially huge boost in performance for those maps. The vision calculation within a level will now have nearly the same speed as if the level was placed on a entirely separate scene. 53 | - Lichtgeschwindigkeit now calculates polygons of type `movement`, which increases compatibility with other modules. 54 | 55 | ### Bugfixes 56 | - Light sources withing buildings that have a wall now shine through the windows, as they are supposed to. 57 | - Fixed a bug that wrongly rendered walls as if they had a height while the "Wall Height" module is disabled. 58 | 59 | 60 | ## 1.3.4 61 | ### Bugfixes 62 | - Fixed a bug that could cause the vision calculation to crash when a token or light was placed exactly on the endpoint of a wall [#19](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues/19) 63 | 64 | 65 | ## 1.3.3 66 | ### Bugfixes 67 | - Fixed a bug that could cause the vision calculation to crash when walls were paralell to a sight ray [#17](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues/17) 68 | 69 | 70 | ## 1.3.2 71 | ### Bugfixes 72 | - Fixed a bug that would cause invisible walls to block light if they were set to block sound [#16](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues/16) 73 | - Fixed a bug that caused walls below roofs to have the wrong visibility [#16](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues/16) 74 | 75 | ### Performance 76 | The bugs listed above were caused by the cache that was introduced in Lichtgeschwindigkeit 1.3.0. Unfortunately fixing the cache isn't simple, so it's being disabled for now to get rid of those bugs. This drops Lichtgeschwindigkeits speed back to the speed it had in Version 1.2.2. 77 | 78 | 79 | ## 1.3.1 80 | ### Bugfixes 81 | - Fixed a bug that caused an error to be printed to the console in certain situations (initial load, scene switching, editing walls) 82 | 83 | 84 | ## 1.3.0 85 | ### New features 86 | - Lichtgeschwindigket now caches the scenes walls in it's wasm memory. This makes the light calculation algorithm about 20% faster than it has been before. 87 | 88 | ### Compatibility 89 | - Lichtgeschwindigkeit is now compatible with the [Wall Height module](https://foundryvtt.com/packages/wall-height). Wall Height version 3.5.3.9 or newer is required for compatibility. 90 | 91 | 92 | ## 1.2.2 93 | ### Bugfixes 94 | - Fixed a bug that would cause the vision calculation to crash when walls were to the right of the token and touching the range of the fov ([#14](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues/14), [#15](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues/15)) 95 | 96 | 97 | ## 1.2.1 98 | ### Bugfixes 99 | - Vision with restricted angle is now calculated at the correct angle (0° now means down, as in vanilla foundry. Before 0° meant left in Lichtgeschwindigkeit). 100 | - Fixed a bug that would stop a tokens movement mid-animation on scenes with Fog of War enabled when using Foundry version 0.8.6 or older. 101 | 102 | ## 1.2.0 103 | ### New features 104 | - Lichtgeschwindigkeit now ships an improved, faster version of `PIXI.Polygon`. This improves the speed of lighting calculation and potentially improves speed in other areas in Foundry that make use of Polygons as well. 105 | 106 | 107 | ## 1.1.3 108 | ### Bugfixes 109 | - Fixed a bug that caused the vision calculation to crash if a token was sitting precisely on top of a wall ([#10](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues/10)) 110 | 111 | ### Compatibility 112 | - Lichtgeschwindigkeit is confirmed to work with Foundry 0.8.8 113 | 114 | 115 | ## 1.1.2 116 | ### Bugfixes 117 | Fixed bugs that could cause the vision to be calculated incorrectly in scenes with 118 | - Directional Walls ([#4](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues/4)) 119 | - Walls arranged as t-junctions ([#5](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues/5)) 120 | - Walls that have no length at all - meaning their start point is identical with their end point ([#6](https://github.com/manuelVo/foundryvtt-lichtgeschwindigkeit/issues/6)) 121 | 122 | 123 | ## 1.1.1 124 | ### Bugfixes 125 | - Fixed a bug that caused the vision calculation to crash if a wall was positioned at a very specific angle to a token/light source. 126 | 127 | 128 | ## 1.1.0 129 | ### New features 130 | - Lichtgeschwindigkeit now speeds up a fog of war related calculation, reducing stutter during token animations on large maps that have fog of war enabled (this feature is only availabe if you use Foundry 0.8.7 or newer) 131 | 132 | ### Bugfixes 133 | - Fixed a bug that caused the vision calclation to crash if tokens/lights with limited vision angle were placed into a scene with no walls 134 | 135 | ### Compatibility 136 | - Lichtgeschwindigkeit is confirmed to work with Foundry 0.8.7 137 | 138 | 139 | ## 1.0.0 140 | Initial release 141 | -------------------------------------------------------------------------------- /js/raycasting.js: -------------------------------------------------------------------------------- 1 | import init, * as Lichtgeschwindigkeit from "../wasm/lichtgeschwindigkeit.js"; 2 | 3 | let wallHeightEnabled; 4 | 5 | init().then(() => { 6 | SightLayer.computeSight = wasmComputePolygon; 7 | WallsLayer.prototype.computePolygon = wasmComputePolygon; 8 | Hooks.on("canvasInit", wipeCache); 9 | Hooks.on("canvasReady", wipeCache); 10 | Hooks.on("createWall", wipeCache); 11 | Hooks.on("updateWall", wipeCache); 12 | Hooks.on("deleteWall", wipeCache); 13 | Hooks.on("createTile", wipeCache); 14 | Hooks.on("updateTile", wipeCache); 15 | Hooks.on("deleteTile", wipeCache); 16 | hookUpdateOcclusion(); 17 | window.lichtgeschwindigkeit = { 18 | build_scene, 19 | generate_test, 20 | } 21 | }); 22 | 23 | Hooks.once("init", () => { 24 | // This can affect the outcome of vision calculations, so we wipe the cache just to be sure 25 | wallHeightEnabled = game.modules.get("wall-height")?.active; 26 | wipeCache(); 27 | }); 28 | 29 | let cache = undefined; 30 | let emptyCache = undefined; 31 | 32 | function wipeCache() { 33 | if (cache) 34 | Lichtgeschwindigkeit.wipeCache(cache); 35 | cache = undefined; 36 | } 37 | 38 | function hookUpdateOcclusion() { 39 | let original = Tile.prototype.updateOcclusion; 40 | Tile.prototype.updateOcclusion = function(tokens) { 41 | const oldOcclusion = this.occluded; 42 | original.call(this, tokens); 43 | if (cache && oldOcclusion != this.occluded && this.data.occlusion.mode === CONST.TILE_OCCLUSION_MODES.ROOF) { 44 | Lichtgeschwindigkeit.updateOcclusion(cache, this.id, this.occluded); 45 | } 46 | } 47 | } 48 | 49 | function wasmComputePolygon(origin, radius, { type = "sight", angle = 360, density = 6, rotation = 0, unrestricted = false } = {}) { 50 | // TODO This hotfix may no longer be necessary in foundry 9 51 | if (type === "sight") 52 | radius = Math.max(radius, canvas.dimensions.size >> 1); // canvas.dimensions.size >> 1 is a fast method of calculating canvas.dimensions.size / 2 53 | 54 | let debugEnabled = CONFIG.debug.sightRays; 55 | // The maximum ray distance needs to reach all areas of the canvas 56 | let d = canvas.dimensions; 57 | const dx = Math.max(origin.x, d.width - origin.x); 58 | const dy = Math.max(origin.y, d.height - origin.y); 59 | const distance = Math.max(radius, Math.hypot(dx, dy)); 60 | const height = game.currentTokenElevation ?? 0; 61 | 62 | let internals = null; 63 | if (debugEnabled) 64 | internals = {}; 65 | 66 | let cacheRef; 67 | if (unrestricted) { 68 | if (!emptyCache) 69 | emptyCache = Lichtgeschwindigkeit.buildCache([], wallHeightEnabled); 70 | cacheRef = emptyCache; 71 | } 72 | else { 73 | if (!cache) 74 | cache = Lichtgeschwindigkeit.buildCache(canvas.walls.placeables, wallHeightEnabled); 75 | cacheRef = cache 76 | } 77 | 78 | function logParams(force, error_fn) { 79 | rustifyParams(cacheRef, origin, height, radius, distance, density, angle, rotation, type, force, error_fn); 80 | } 81 | 82 | if (debugEnabled) 83 | logParams(); 84 | 85 | let sight; 86 | try { 87 | sight = Lichtgeschwindigkeit.computePolygon(cacheRef, origin, height, radius, distance, density, angle, rotation, type, internals); 88 | } 89 | catch (e) { 90 | console.error(e); 91 | console.error("Data to reproduce the error (please always include this in bug reports!):"); 92 | logParams(true, console.error); 93 | throw e; 94 | } 95 | 96 | // Lichtgeschwindigkeit improves the speed of PIXI.Polygon.contains. 97 | // Those improvements outperform the improvements done by SourcePolygon. 98 | // As a result we don't construct SourcePolygon here. 99 | const los = new PIXI.Polygon(...sight.los); 100 | const fov = new PIXI.Polygon(...sight.fov); 101 | 102 | if (debugEnabled) { 103 | _visualizeSight(internals.endpoints, origin, radius, distance, los, fov, sight.los, true); 104 | } 105 | 106 | return { rays: null, los, fov }; 107 | } 108 | 109 | function rustifyParams(cache, origin, height, radius, distance, density, angle, rotation, type, force = false, error_fn = console.warn) { 110 | /*if (!force) { 111 | if (canvas.tokens.controlled.length === 0) 112 | return; 113 | if (Math.abs(origin.x - canvas.tokens.controlled[0].data.x) > 50 || Math.abs(origin.y - canvas.tokens.controlled[0].data.y) > 50) 114 | return; 115 | }*/ 116 | error_fn(Lichtgeschwindigkeit.serializeData(cache, origin, height, radius, distance, density, angle, rotation, type)); 117 | } 118 | 119 | function _visualizeSight(endpoints, origin, radius, distance, los, fov, tangentPoints, clear = true) { 120 | /*if (canvas.tokens.controlled.length === 0) 121 | return; 122 | if (Math.abs(origin.x - canvas.tokens.controlled[0].data.x) > 50 || Math.abs(origin.y - canvas.tokens.controlled[0].data.y) > 50) 123 | return;*/ 124 | const debug = canvas.controls.debug; 125 | if (!debug) 126 | return; 127 | if (clear) 128 | debug.clear(); 129 | 130 | // Relevant polygons 131 | debug.lineStyle(0).beginFill(0x66FFFF, 0.2).drawShape(los); 132 | debug.beginFill(0xFF66FF, 0.2).drawShape(fov).endFill(); 133 | 134 | // Tested endpoints 135 | if (!endpoints) 136 | endpoints = []; 137 | 138 | for (const endpoint of endpoints) { 139 | const color = endpoint.isIntersection ? 0xFF0000 : 0x00FFFF; 140 | debug.lineStyle(0).beginFill(color, 1.0).drawCircle(endpoint.x, endpoint.y, 9).endFill(); 141 | } 142 | 143 | for (const point of tangentPoints) { 144 | debug.lineStyle(2, 0xDDFF00).drawCircle(point.x, point.y, 5); 145 | } 146 | 147 | // Walls 148 | for (const wall of canvas.walls.placeables) { 149 | debug.lineStyle(3, 0x000000).moveTo(wall.data.c[0], wall.data.c[1]).lineTo(wall.data.c[2], wall.data.c[3]); 150 | } 151 | 152 | // Sight range 153 | debug.lineStyle(1, 0xFF0000).drawCircle(origin.x, origin.y, radius); 154 | 155 | // Cast rays 156 | for (const endpoint of endpoints) { 157 | debug.lineStyle(1, 0x00FF00).moveTo(origin.x, origin.y).lineTo(origin.x - Math.cos(endpoint.angle) * distance, origin.y - Math.sin(endpoint.angle) * distance); 158 | } 159 | } 160 | 161 | function build_scene() { 162 | new Dialog({ 163 | content: "", 164 | buttons: { 165 | ok: { 166 | icon: '', 167 | callback: html => { 168 | let data = document.getElementById("lichtgeschwindigkeit-debug-input").value; 169 | data = Lichtgeschwindigkeit.deserializeData(data); 170 | import("./scene_builder.js").then((module) => module.build_scene(data)); 171 | } 172 | } 173 | } 174 | }).render(true); 175 | } 176 | 177 | function generate_test() { 178 | new Dialog({ 179 | content: "", 180 | buttons: { 181 | ok: { 182 | icon: '', 183 | callback: html => { 184 | let data = document.getElementById("lichtgeschwindigkeit-debug-input").value; 185 | console.warn(Lichtgeschwindigkeit.generateTest(data)); 186 | } 187 | } 188 | } 189 | }).render(true); 190 | } 191 | -------------------------------------------------------------------------------- /rust/src/geometry.rs: -------------------------------------------------------------------------------- 1 | use std::f64::consts::PI; 2 | use std::hash::{Hash, Hasher}; 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | extern "C" { 7 | pub type JsPoint; 8 | 9 | #[wasm_bindgen(method, getter)] 10 | fn x(this: &JsPoint) -> f64; 11 | 12 | #[wasm_bindgen(method, getter)] 13 | fn y(this: &JsPoint) -> f64; 14 | } 15 | 16 | #[wasm_bindgen] 17 | #[derive(Debug, Copy, Clone, PartialEq)] 18 | pub struct Point { 19 | pub x: f64, 20 | pub y: f64, 21 | } 22 | 23 | impl Hash for Point { 24 | fn hash(&self, hasher: &mut H) { 25 | self.x.to_bits().hash(hasher); 26 | self.y.to_bits().hash(hasher); 27 | } 28 | } 29 | 30 | impl Eq for Point {} 31 | 32 | impl From<&JsPoint> for Point { 33 | fn from(point: &JsPoint) -> Self { 34 | Self::new(point.x(), point.y()) 35 | } 36 | } 37 | 38 | impl Point { 39 | pub fn new(x: f64, y: f64) -> Self { 40 | Self { x, y } 41 | } 42 | 43 | pub fn from_line_x(line: &Line, x: f64) -> Self { 44 | let y = line.calc_y(x); 45 | Self { x, y } 46 | } 47 | 48 | pub fn distance_to(&self, other: &Self) -> f64 { 49 | (self.x - other.x).hypot(self.y - other.y) 50 | } 51 | 52 | pub fn is_same_as(&self, other: &Self) -> bool { 53 | let e = 0.000001; 54 | (self.x - other.x).abs() < e && (self.y - other.y).abs() < e 55 | } 56 | } 57 | 58 | #[derive(Debug, Copy, Clone)] 59 | pub struct Line { 60 | pub m: f64, 61 | pub b: f64, 62 | pub p1: Point, 63 | } 64 | 65 | impl Line { 66 | pub fn new(m: f64, b: f64, p1: Point) -> Self { 67 | Self { m, b, p1 } 68 | } 69 | 70 | pub fn from_points(p1: Point, p2: Point) -> Self { 71 | let m = (p1.y - p2.y) / (p1.x - p2.x); 72 | let b = p1.y - m * p1.x; 73 | Self { m, b, p1 } 74 | } 75 | 76 | pub fn from_point_and_angle(p1: Point, angle: f64) -> Self { 77 | let p2 = Point { 78 | x: p1.x - angle.cos(), 79 | y: p1.y - angle.sin(), 80 | }; 81 | Line::from_points(p1, p2) 82 | } 83 | 84 | pub fn empty() -> Self { 85 | Self { 86 | m: 0.0, 87 | b: 0.0, 88 | p1: Point::new(0.0, 0.0), 89 | } 90 | } 91 | 92 | pub fn is_vertical(&self) -> bool { 93 | self.m.is_infinite() 94 | } 95 | 96 | pub fn is_horizontal(&self) -> bool { 97 | self.m == 0.0 98 | } 99 | 100 | pub fn calc_x(&self, y: f64) -> f64 { 101 | (y - self.b) / self.m 102 | } 103 | 104 | pub fn calc_y(&self, x: f64) -> f64 { 105 | self.m * x + self.b 106 | } 107 | 108 | pub fn intersection(&self, other: &Line) -> Option { 109 | // Are both lines vertical? 110 | if self.is_vertical() && other.is_vertical() { 111 | return None; 112 | } 113 | 114 | // Are the lines paralell? 115 | if (self.m - other.m).abs() < 0.00000005 { 116 | return None; 117 | } 118 | 119 | // Is one of the lines vertical? 120 | if self.is_vertical() || other.is_vertical() { 121 | let vertical; 122 | let regular; 123 | if self.is_vertical() { 124 | vertical = self; 125 | regular = other; 126 | } else { 127 | vertical = other; 128 | regular = self; 129 | } 130 | return Some(Point::from_line_x(®ular, vertical.p1.x)); 131 | } 132 | 133 | // Calculate x coordinate of intersection point between both lines 134 | // Find intersection point: x * m1 + b1 = x * m2 + b2 135 | // Solve for x: x = (b1 - b2) / (m2 - m1) 136 | let x = (self.b - other.b) / (other.m - self.m); 137 | if self.m.abs() < other.m.abs() { 138 | Some(Point::from_line_x(&self, x)) 139 | } else { 140 | Some(Point::from_line_x(&other, x)) 141 | } 142 | } 143 | 144 | pub fn get_perpendicular_through_point(&self, p: Point) -> Self { 145 | let m = -1.0 / self.m; 146 | let b = p.y - m * p.x; 147 | Self { m, b, p1: p } 148 | } 149 | } 150 | 151 | #[derive(Copy, Clone)] 152 | pub struct CircleIntersection { 153 | pub point: Point, 154 | pub angle: f64, 155 | } 156 | 157 | #[derive(Copy, Clone)] 158 | pub struct Circle { 159 | pub center: Point, 160 | pub radius: f64, 161 | } 162 | 163 | impl Circle { 164 | // We don't care about tangent points. That's why we either return two or no points 165 | pub fn intersections(&self, line: &Line) -> Option<(CircleIntersection, CircleIntersection)> { 166 | // We first seach for the closest point of the line to the center. 167 | // If intersections exist, that is the halfway point between both intersections 168 | let perpendicular = line.get_perpendicular_through_point(self.center); 169 | let closest_point = perpendicular.intersection(line).unwrap(); 170 | 171 | // Calculate how far the closest point on the line is away from the circles center 172 | let closest_distance = self.center.distance_to(&closest_point); 173 | 174 | // closestDistance > radius means 0 intersections 175 | // closestDistance == radius the line is a tangent 176 | // closestDistance < radius means 2 intersections 177 | // We only care about the intersections, so we filter the other cases out 178 | if closest_distance >= self.radius { 179 | return None; 180 | } 181 | 182 | let intersection1_angle; 183 | let intersection2_angle; 184 | if closest_distance > 0.0 { 185 | // This is the usual case where the line is *not* going through the circle's center 186 | 187 | // Calculate the angle of the perpendicular relative to the global coordiante system 188 | let perpendicular_angle = 189 | (self.center.y - closest_point.y).atan2(self.center.x - closest_point.x); 190 | 191 | // Calculate the angle between the perpendicular and the first intersection 192 | let intersection_to_perpendicular_angle = (closest_distance / self.radius).acos(); 193 | 194 | // Calculate the angle between the intersection an the global coordinate system 195 | intersection1_angle = perpendicular_angle + intersection_to_perpendicular_angle; 196 | intersection2_angle = perpendicular_angle - intersection_to_perpendicular_angle; 197 | } else { 198 | // This happens if the line goes through the circles center. 199 | 200 | // Calculate the angle from the circle center + a point on the line 201 | // Taking p1 is risky, because the point could be the circle center itself. That should never happen for Lichtgeschwindigkeit though 202 | assert_ne!(self.center, line.p1); 203 | intersection1_angle = (self.center.y - line.p1.y).atan2(self.center.x - line.p1.x); 204 | intersection2_angle = intersection1_angle + PI; 205 | } 206 | 207 | // The first intersection point an now be determined 208 | let mut intersection1 = CircleIntersection { 209 | point: Point::new( 210 | self.center.x - intersection1_angle.cos() * self.radius, 211 | self.center.y - intersection1_angle.sin() * self.radius, 212 | ), 213 | angle: intersection1_angle, 214 | }; 215 | 216 | // Mirror intersection 1 along the perpendicular to find intersection 2 217 | let mut intersection2 = CircleIntersection { 218 | point: Point::new( 219 | closest_point.x - (intersection1.point.x - closest_point.x), 220 | closest_point.y - (intersection1.point.y - closest_point.y), 221 | ), 222 | angle: intersection2_angle, 223 | }; 224 | 225 | // Normalize the intersection angles 226 | if intersection1.angle > PI { 227 | intersection1.angle -= 2.0 * PI; 228 | } 229 | if intersection2.angle < -PI { 230 | intersection2.angle += 2.0 * PI; 231 | } 232 | 233 | Some((intersection1, intersection2)) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /rust/src/raycasting/vision_angle.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::Point; 2 | use crate::raycasting::types::{Endpoint, FovPoint, VisionAngle, Wall, WallWithAngles}; 3 | use std::cell::RefCell; 4 | use std::rc::Rc; 5 | 6 | use super::util::between_exclusive; 7 | 8 | pub fn restrict_vision_angle( 9 | wall: &Wall, 10 | start: &Rc>, 11 | end: &Rc>, 12 | vision_angle: &Option, 13 | ) -> Option<[Option; 2]> { 14 | if let Some(vision_angle) = vision_angle { 15 | if vision_angle.start < vision_angle.end { 16 | let wall_inverted; 17 | if start.borrow().angle < end.borrow().angle { 18 | if start.borrow().angle >= vision_angle.end 19 | || end.borrow().angle <= vision_angle.start 20 | { 21 | return Some([None, None]); 22 | } 23 | wall_inverted = false; 24 | } else { 25 | if end.borrow().angle <= vision_angle.start 26 | && start.borrow().angle >= vision_angle.end 27 | { 28 | return Some([None, None]); 29 | } 30 | wall_inverted = true; 31 | } 32 | 33 | let mut wall_shortened = false; 34 | let mut start_point = start.borrow().point; 35 | let mut start_angle = start.borrow().angle; 36 | let mut end_point = end.borrow().point; 37 | let mut end_angle = end.borrow().angle; 38 | if wall_inverted { 39 | if start.borrow().angle < vision_angle.start 40 | || end.borrow().angle > vision_angle.end 41 | { 42 | if let Some(intersection) = vision_angle.start_ray.intersection(&wall.line) { 43 | wall_shortened = true; 44 | start_angle = vision_angle.start; 45 | start_point = intersection; 46 | } 47 | if let Some(intersection) = vision_angle.end_ray.intersection(&wall.line) { 48 | wall_shortened = true; 49 | end_angle = vision_angle.end; 50 | end_point = intersection; 51 | } 52 | } 53 | } else { 54 | if end.borrow().angle > vision_angle.start 55 | && start.borrow().angle < vision_angle.start 56 | { 57 | if let Some(intersection) = vision_angle.start_ray.intersection(&wall.line) { 58 | wall_shortened = true; 59 | start_angle = vision_angle.start; 60 | start_point = intersection; 61 | } 62 | } 63 | if start.borrow().angle < vision_angle.end && end.borrow().angle > vision_angle.end 64 | { 65 | if let Some(intersection) = vision_angle.end_ray.intersection(&wall.line) { 66 | wall_shortened = true; 67 | end_angle = vision_angle.end; 68 | end_point = intersection; 69 | } 70 | } 71 | } 72 | if wall_shortened { 73 | let new_wall = WallWithAngles::new_copy_props( 74 | &wall, 75 | start_point, 76 | end_point, 77 | start_angle, 78 | end_angle, 79 | ); 80 | return Some([Some(new_wall), None]); 81 | } 82 | 83 | if end.borrow().angle < start.borrow().angle { 84 | // Only remaining option is that end.angle < end.start (which means the wall is to the right, where the circle overflows) 85 | let mut split_walls = [None, None]; 86 | if end.borrow().angle > vision_angle.start { 87 | let start_point = vision_angle.start_ray.intersection(&wall.line).unwrap(); 88 | let start_angle = vision_angle.start; 89 | let end_point = end.borrow().point; 90 | let end_angle = end.borrow().angle; 91 | split_walls[0] = Some(WallWithAngles::new_copy_props( 92 | &wall, 93 | start_point, 94 | end_point, 95 | start_angle, 96 | end_angle, 97 | )); 98 | } 99 | if start.borrow().angle < vision_angle.end { 100 | let start_point = start.borrow().point; 101 | let start_angle = start.borrow().angle; 102 | let end_point = vision_angle.end_ray.intersection(&wall.line).unwrap(); 103 | let end_angle = vision_angle.end; 104 | split_walls[1] = Some(WallWithAngles::new_copy_props( 105 | &wall, 106 | start_point, 107 | end_point, 108 | start_angle, 109 | end_angle, 110 | )); 111 | } 112 | 113 | if split_walls.iter().all(|wall| wall.is_none()) 114 | && !between_exclusive( 115 | start.borrow().angle, 116 | vision_angle.start, 117 | vision_angle.end, 118 | ) && !between_exclusive(end.borrow().angle, vision_angle.start, vision_angle.end) 119 | { 120 | return None; 121 | } 122 | return Some(split_walls); 123 | } 124 | } else { 125 | if start.borrow().angle > end.borrow().angle { 126 | let mut wall_shortened = false; 127 | let mut start_point = start.borrow().point; 128 | let mut start_angle = start.borrow().angle; 129 | let mut end_point = end.borrow().point; 130 | let mut end_angle = end.borrow().angle; 131 | if start.borrow().angle < vision_angle.start { 132 | if let Some(intersection) = vision_angle.start_ray.intersection(&wall.line) { 133 | wall_shortened = true; 134 | start_angle = vision_angle.start; 135 | start_point = intersection; 136 | } 137 | } 138 | if end.borrow().angle > vision_angle.end { 139 | if let Some(intersection) = vision_angle.end_ray.intersection(&wall.line) { 140 | wall_shortened = true; 141 | end_angle = vision_angle.end; 142 | end_point = intersection; 143 | } 144 | } 145 | if wall_shortened { 146 | let new_wall = WallWithAngles::new_copy_props( 147 | &wall, 148 | start_point, 149 | end_point, 150 | start_angle, 151 | end_angle, 152 | ); 153 | return Some([Some(new_wall), None]); 154 | } 155 | } else { 156 | let mut split_walls = [None, None]; 157 | if between_exclusive(vision_angle.end, start.borrow().angle, end.borrow().angle) { 158 | let start_point = start.borrow().point; 159 | let start_angle = start.borrow().angle; 160 | let end_point = vision_angle.end_ray.intersection(&wall.line).unwrap(); 161 | let end_angle = vision_angle.end; 162 | split_walls[0] = Some(WallWithAngles::new_copy_props( 163 | &wall, 164 | start_point, 165 | end_point, 166 | start_angle, 167 | end_angle, 168 | )); 169 | } 170 | if between_exclusive(vision_angle.start, start.borrow().angle, end.borrow().angle) { 171 | let start_point = vision_angle.start_ray.intersection(&wall.line).unwrap(); 172 | let start_angle = vision_angle.start; 173 | let end_point = end.borrow().point; 174 | let end_angle = end.borrow().angle; 175 | split_walls[1] = Some(WallWithAngles::new_copy_props( 176 | &wall, 177 | start_point, 178 | end_point, 179 | start_angle, 180 | end_angle, 181 | )); 182 | } 183 | if split_walls.iter().all(|wall| wall.is_none()) 184 | && !between_exclusive( 185 | start.borrow().angle, 186 | vision_angle.start, 187 | vision_angle.end, 188 | ) && !between_exclusive(end.borrow().angle, vision_angle.start, vision_angle.end) 189 | { 190 | return None; 191 | } 192 | return Some(split_walls); 193 | } 194 | } 195 | } 196 | None 197 | } 198 | 199 | pub fn add_vision_wedge( 200 | mut los_points: Vec, 201 | origin: Point, 202 | vision_angle: VisionAngle, 203 | start_gap_fov: &mut bool, 204 | ) -> Vec { 205 | let mut visible_points_from_start: &[FovPoint]; 206 | let mut visible_points_to_end: &[FovPoint]; 207 | let start_end_swapped; 208 | if vision_angle.start < vision_angle.end { 209 | if los_points.len() > 0 && los_points.last().unwrap().angle == vision_angle.end { 210 | los_points.last_mut().unwrap().gap = false; 211 | } 212 | visible_points_from_start = &los_points; 213 | visible_points_to_end = &[]; 214 | start_end_swapped = false; 215 | *start_gap_fov = false; 216 | } else { 217 | let mut start_index = los_points.len(); 218 | let mut end_index = los_points.len(); 219 | for i in 0..los_points.len() { 220 | // TODO Check if > or >= 221 | if los_points[i].angle > vision_angle.end { 222 | end_index = i; 223 | break; 224 | } 225 | } 226 | for i in end_index..los_points.len() { 227 | if los_points[i].angle >= vision_angle.start { 228 | start_index = i; 229 | break; 230 | } 231 | } 232 | if end_index < los_points.len() { 233 | let last_point = &mut los_points[end_index]; 234 | if last_point.angle == vision_angle.end { 235 | last_point.gap = false; 236 | } 237 | } 238 | visible_points_to_end = &los_points[..end_index]; 239 | if start_index < los_points.len() { 240 | visible_points_from_start = &los_points[start_index..]; 241 | } else { 242 | visible_points_from_start = &[]; 243 | } 244 | start_end_swapped = true; 245 | } 246 | 247 | let entry; 248 | /* The angles being exactly equal isn't as unlikely as it seems because we have 249 | introduced endpoints with perfectly matching angle during endpoint generation */ 250 | if visible_points_from_start.len() > 0 251 | && visible_points_from_start.first().unwrap().angle == vision_angle.start 252 | { 253 | entry = FovPoint { 254 | point: origin, 255 | angle: vision_angle.start, 256 | gap: false, 257 | }; 258 | } else { 259 | entry = FovPoint { 260 | point: origin, 261 | angle: vision_angle.start, 262 | gap: true, 263 | }; 264 | } 265 | 266 | let exit; 267 | if start_end_swapped 268 | && visible_points_to_end.len() > 0 269 | && visible_points_to_end.last().unwrap().angle == vision_angle.end 270 | { 271 | let (point, remaining) = visible_points_to_end.split_last().unwrap(); 272 | visible_points_to_end = remaining; 273 | let mut point = *point; 274 | point.gap = false; 275 | exit = vec![ 276 | point, 277 | FovPoint { 278 | point: origin, 279 | angle: vision_angle.end, 280 | gap: false, 281 | }, 282 | ]; 283 | } else if !start_end_swapped 284 | && visible_points_from_start.len() > 0 285 | && visible_points_from_start.last().unwrap().angle == vision_angle.end 286 | { 287 | let (point, remaining) = visible_points_from_start.split_last().unwrap(); 288 | visible_points_from_start = remaining; 289 | let mut point = *point; 290 | point.gap = false; 291 | exit = vec![ 292 | point, 293 | FovPoint { 294 | point: origin, 295 | angle: vision_angle.end, 296 | gap: false, 297 | }, 298 | ]; 299 | } else { 300 | exit = vec![FovPoint { 301 | point: origin, 302 | angle: vision_angle.end, 303 | gap: false, 304 | }]; 305 | } 306 | 307 | if start_end_swapped { 308 | [ 309 | visible_points_to_end, 310 | &exit, 311 | &[entry], 312 | visible_points_from_start, 313 | ] 314 | .concat() 315 | } else { 316 | [&[entry], visible_points_from_start, &exit].concat() 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /rust/src/raycasting/postprocessing.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::{Circle, Line, Point}; 2 | use crate::raycasting::types::FovPoint; 3 | use std::f64::consts::PI; 4 | use std::mem::swap; 5 | 6 | use super::util::is_intersection_on_segment; 7 | 8 | pub fn calculate_fov( 9 | origin: Point, 10 | radius: f64, 11 | los_points: &Vec, 12 | start_gap_fov: bool, 13 | ) -> Vec { 14 | let fov = Circle { 15 | center: origin, 16 | radius, 17 | }; 18 | let mut fov_points = Vec::new(); 19 | for i in 0..los_points.len() { 20 | let los_point = los_points[i]; 21 | let distance = origin.distance_to(&los_point.point); 22 | if distance < radius { 23 | if i == 0 { 24 | fov_points.push(los_point); 25 | } else { 26 | let previous_los = los_points[i - 1]; 27 | let previous_fov_gap = fov_points 28 | .last() 29 | .map(|previous| previous.gap) 30 | .unwrap_or(start_gap_fov); 31 | if previous_fov_gap && !previous_los.gap { 32 | if previous_los.angle == los_point.angle { 33 | let point = Point { 34 | x: fov.center.x - los_point.angle.cos() * radius, 35 | y: fov.center.y - los_point.angle.sin() * radius, 36 | }; 37 | fov_points.push(FovPoint { 38 | point, 39 | angle: los_point.angle, 40 | gap: false, 41 | }); 42 | } else { 43 | let line = Line::from_points(previous_los.point, los_point.point); 44 | if let Some(fov_intersections) = fov.intersections(&line) { 45 | let relevant_intersection; 46 | // TODO is_smaller_relative 47 | if fov_intersections.0.angle > previous_los.angle 48 | && fov_intersections.0.angle < los_point.angle 49 | { 50 | relevant_intersection = fov_intersections.0; 51 | } else { 52 | relevant_intersection = fov_intersections.1; 53 | } 54 | fov_points.push(FovPoint { 55 | point: relevant_intersection.point, 56 | angle: relevant_intersection.angle, 57 | gap: false, 58 | }); 59 | } 60 | } 61 | } 62 | fov_points.push(los_point); 63 | 64 | if i == los_points.len() - 1 { 65 | if start_gap_fov && !los_point.gap { 66 | let line = 67 | Line::from_points(los_point.point, los_points.first().unwrap().point); 68 | let intersections = fov.intersections(&line).unwrap(); 69 | let exit_intersection; 70 | if intersections.0.angle > intersections.1.angle { 71 | exit_intersection = intersections.0; 72 | } else { 73 | exit_intersection = intersections.1; 74 | } 75 | fov_points.push(FovPoint { 76 | point: exit_intersection.point, 77 | angle: exit_intersection.angle, 78 | gap: false, 79 | }); 80 | } 81 | } 82 | } 83 | } else { 84 | let previous_fov_gap = fov_points 85 | .last() 86 | .map(|previous| previous.gap) 87 | .unwrap_or(start_gap_fov); 88 | if !previous_fov_gap { 89 | if i > 0 { 90 | let hidden_point = los_point; 91 | let point_before_hidden = los_points[i - 1]; 92 | if point_before_hidden.angle == hidden_point.angle { 93 | let point = Point { 94 | x: fov.center.x - hidden_point.angle.cos() * radius, 95 | y: fov.center.y - hidden_point.angle.sin() * radius, 96 | }; 97 | fov_points.push(FovPoint { 98 | point, 99 | angle: los_point.angle, 100 | gap: true, 101 | }); 102 | } else { 103 | let line = Line::from_points(point_before_hidden.point, hidden_point.point); 104 | if let Some(fov_intersections) = fov.intersections(&line) { 105 | let relevant_intersection; 106 | // TODO is_smaller_relative 107 | if fov_intersections.0.angle > point_before_hidden.angle 108 | && fov_intersections.0.angle < hidden_point.angle 109 | { 110 | relevant_intersection = fov_intersections.0; 111 | } else { 112 | relevant_intersection = fov_intersections.1; 113 | } 114 | fov_points.push(FovPoint { 115 | point: relevant_intersection.point, 116 | angle: relevant_intersection.angle, 117 | gap: true, 118 | }); 119 | } 120 | if !start_gap_fov && i == los_points.len() - 1 { 121 | let next_los = los_points.first().unwrap(); 122 | let line = Line::from_points(los_point.point, next_los.point); 123 | let intersections = fov.intersections(&line).unwrap(); 124 | let entry; 125 | // The wall is to the right of the token, so the angles are inverted 126 | if intersections.0.angle > intersections.1.angle { 127 | entry = intersections.0; 128 | } else { 129 | entry = intersections.1; 130 | } 131 | fov_points.push(FovPoint { 132 | point: entry.point, 133 | angle: entry.angle, 134 | gap: false, 135 | }); 136 | } 137 | } 138 | } else { 139 | let previous_los = los_points.last().unwrap(); 140 | let line = Line::from_points(previous_los.point, los_point.point); 141 | let intersections = fov.intersections(&line).unwrap(); 142 | let exit; 143 | // The wall is to the right of the token, so the angles are inverted 144 | if intersections.0.angle > intersections.1.angle { 145 | exit = intersections.1; 146 | } else { 147 | exit = intersections.0; 148 | } 149 | fov_points.push(FovPoint { 150 | point: exit.point, 151 | angle: exit.angle, 152 | gap: true, 153 | }); 154 | } 155 | } else { 156 | // TODO Handle i == 0 157 | if i > 0 { 158 | let previous_los = los_points[i - 1]; 159 | if !previous_los.gap { 160 | let line = Line::from_points(previous_los.point, los_point.point); 161 | if let Some(intersections) = fov.intersections(&line) { 162 | // TODO Is smaller relative? 163 | if intersections.0.angle > previous_los.angle 164 | && intersections.1.angle > previous_los.angle 165 | && intersections.0.angle < los_point.angle 166 | && intersections.1.angle < los_point.angle 167 | { 168 | let (entry, exit); 169 | // TODO Is smaller relative? 170 | if intersections.0.angle < intersections.1.angle { 171 | entry = intersections.0; 172 | exit = intersections.1; 173 | } else { 174 | entry = intersections.1; 175 | exit = intersections.0; 176 | } 177 | fov_points.push(FovPoint { 178 | point: entry.point, 179 | angle: entry.angle, 180 | gap: false, 181 | }); 182 | fov_points.push(FovPoint { 183 | point: exit.point, 184 | angle: exit.angle, 185 | gap: true, 186 | }); 187 | } 188 | } 189 | } 190 | if i == los_points.len() - 1 && !los_point.gap { 191 | let next_los = los_points.first().unwrap(); 192 | let line = Line::from_points(los_point.point, next_los.point); 193 | if let Some(intersections) = fov.intersections(&line) { 194 | let mut entry; 195 | let mut exit; 196 | if intersections.0.angle < intersections.1.angle { 197 | entry = intersections.0; 198 | exit = intersections.1; 199 | } else { 200 | entry = intersections.1; 201 | exit = intersections.0; 202 | } 203 | let mut overflow = false; 204 | // If "exit" is above the origin this means that the circle is overflowing and "exit" is actually the entry 205 | // TODO I think this condition will always be true and this can be optimized away 206 | if exit.point.y < origin.y { 207 | swap(&mut entry, &mut exit); 208 | overflow = true; 209 | } 210 | if is_intersection_on_segment( 211 | entry.point, 212 | line, 213 | los_point.point, 214 | next_los.point, 215 | ) { 216 | let fov_point = FovPoint { 217 | point: entry.point, 218 | angle: entry.angle, 219 | gap: false, 220 | }; 221 | if overflow { 222 | fov_points.push(fov_point); 223 | } else { 224 | // TODO This isn't optimal from a performance standpoint, but fov_point's shouldn't be huge either, so it might be ok 225 | fov_points.insert(0, fov_point); 226 | } 227 | } 228 | if is_intersection_on_segment( 229 | exit.point, 230 | line, 231 | los_point.point, 232 | next_los.point, 233 | ) { 234 | fov_points.push(FovPoint { 235 | point: exit.point, 236 | angle: exit.angle, 237 | gap: true, 238 | }); 239 | } 240 | } 241 | } 242 | } 243 | } 244 | } 245 | } 246 | fov_points 247 | } 248 | 249 | pub fn fill_gaps( 250 | points: &mut Vec, 251 | start_gap: bool, 252 | origin: Point, 253 | radius: f64, 254 | radial_density: f64, 255 | ) -> Vec { 256 | let mut output = Vec::new(); 257 | 258 | if points.len() == 0 { 259 | let mut a = -PI; 260 | while a < PI { 261 | output.push(Point::new( 262 | origin.x - (a.cos() * radius), 263 | origin.y - (a.sin() * radius), 264 | )); 265 | a += radial_density; 266 | } 267 | } else { 268 | if points.last().unwrap().point != origin { 269 | points.last_mut().unwrap().gap = start_gap; 270 | } 271 | for i in 0..points.len() { 272 | // TODO This produces a quite big assembly. Think of something faster 273 | let (lower, upper) = points.split_at(i); 274 | let (current, upper) = upper.split_at(1); 275 | let current = current.first().unwrap(); 276 | let previous; 277 | if i == 0 { 278 | previous = upper.last().unwrap(); 279 | } else { 280 | previous = lower.last().unwrap(); 281 | } 282 | if previous.gap { 283 | let mut previous_angle = previous.angle; 284 | if previous_angle > current.angle { 285 | previous_angle -= 2.0 * PI; 286 | } 287 | let mut a = previous_angle; 288 | let first_filler = 289 | Point::new(origin.x - (a.cos() * radius), origin.y - (a.sin() * radius)); 290 | if !first_filler.is_same_as(&previous.point) { 291 | output.push(first_filler); 292 | } 293 | a += radial_density; 294 | while a < current.angle { 295 | output.push(Point::new( 296 | origin.x - (a.cos() * radius), 297 | origin.y - (a.sin() * radius), 298 | )); 299 | a += radial_density; 300 | } 301 | let last_filler = Point::new( 302 | origin.x - (current.angle.cos() * radius), 303 | origin.y - (current.angle.sin() * radius), 304 | ); 305 | if !last_filler.is_same_as(¤t.point) { 306 | output.push(last_filler); 307 | } 308 | } 309 | output.push(current.point); 310 | } 311 | } 312 | 313 | output 314 | } 315 | -------------------------------------------------------------------------------- /rust/src/raycasting/raycasting.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::*; 2 | 3 | use std::cell::RefCell; 4 | use std::mem::swap; 5 | use std::rc::Rc; 6 | // TODO Try out if this is acutally the optimal hasher to use 7 | use crate::ptr_indexed_hash_set::PtrIndexedHashSet; 8 | use crate::raycasting::js_api::InternalsTransfer; 9 | use crate::raycasting::postprocessing::{calculate_fov, fill_gaps}; 10 | use crate::raycasting::prepare::prepare_data; 11 | use crate::raycasting::types::*; 12 | use crate::raycasting::util::{is_intersection_on_wall, is_smaller_relative}; 13 | use crate::raycasting::vision_angle::add_vision_wedge; 14 | 15 | pub fn compute_polygon( 16 | cache: &Cache, 17 | origin: Point, 18 | height: f64, 19 | radius: f64, 20 | distance: f64, 21 | density: f64, 22 | vision_angle: Option, 23 | polygon_type: PolygonType, 24 | internals_transfer: Option, 25 | ) -> (Vec, Vec) { 26 | let (endpoints, mut start_walls) = 27 | prepare_data(cache, origin, height, &vision_angle, polygon_type); 28 | 29 | let (mut los_points, start_gap_los, mut start_gap_fov) = 30 | calculate_los(origin, radius, &endpoints, &mut start_walls); 31 | 32 | if let Some(vision_angle) = vision_angle { 33 | los_points = add_vision_wedge(los_points, origin, vision_angle, &mut start_gap_fov); 34 | } 35 | 36 | let mut fov_points = calculate_fov(origin, radius, &los_points, start_gap_fov); 37 | 38 | // Report endpoints if debugging is enabled 39 | if let Some(internals_transfer) = internals_transfer { 40 | internals_transfer.set_endpoints( 41 | endpoints 42 | .iter() 43 | .map(|endpoint| ExposedEndpoint::from(&*endpoint.borrow()).into()) 44 | .collect(), 45 | ); 46 | } 47 | 48 | // Clean up references to the walls in the endpoints to avoid a memory leak (walls and endpoints have Rc's to each other in a cyclic way 49 | for endpoint in endpoints { 50 | endpoint.borrow_mut().starting_walls.clear(); 51 | endpoint.borrow_mut().ending_walls.clear(); 52 | } 53 | 54 | let radial_density = density.to_radians(); 55 | let los = fill_gaps( 56 | &mut los_points, 57 | start_gap_los, 58 | origin, 59 | distance, 60 | radial_density, 61 | ); 62 | let fov = fill_gaps( 63 | &mut fov_points, 64 | start_gap_fov, 65 | origin, 66 | radius, 67 | radial_density, 68 | ); 69 | 70 | (los, fov) 71 | } 72 | 73 | fn calculate_los( 74 | origin: Point, 75 | radius: f64, 76 | endpoints: &Vec>>, 77 | start_walls: &mut PtrIndexedHashSet, 78 | ) -> (Vec, bool, bool) { 79 | let mut los_points = Vec::new(); 80 | let current_walls = start_walls; 81 | let mut current_ray_line = Line::new(0.0, origin.y as f64, origin); 82 | let mut closest_los_wall = 83 | find_closest_wall::<_, false>(origin, ¤t_ray_line, &*current_walls); 84 | let start_gap_los = closest_los_wall.is_none(); 85 | let start_gap_fov = closest_los_wall 86 | .as_ref() 87 | .filter(|closest_wall| closest_wall.distance < radius) 88 | .is_none(); 89 | 90 | for i in 0..endpoints.len() { 91 | let endpoint = endpoints[i].borrow(); 92 | let old_los_wall = closest_los_wall.clone(); 93 | current_ray_line = Line::from_points(origin, endpoint.point); 94 | let mut closest_wall_could_change = endpoint.is_intersection; 95 | for wall in &endpoint.ending_walls { 96 | let element_removed = current_walls.remove(wall); 97 | if element_removed { 98 | closest_wall_could_change = true; 99 | } 100 | } 101 | 102 | for wall in &endpoint.starting_walls { 103 | if wall.is_see_through_from(endpoint.angle) { 104 | continue; 105 | } 106 | if let Some(closest_wall) = &closest_los_wall { 107 | // This optimization doesn't work yet for terrain walls 108 | if closest_wall.wall.sense != WallSenseType::LIMITED { 109 | // Let's see if the wall is completely behind the currently closest wall. If so, we can skip it. 110 | if is_smaller_relative( 111 | wall.end.borrow().angle, 112 | closest_wall.wall.end.borrow().angle, 113 | ) { 114 | // Probe if the walls have any chance of intersecting. If not, the new wall is either completely in front or behind of the currently closest wall. 115 | // TODO Check if the heuristic if faster. If not, adjust the above comment 116 | // TODO Only do segment intersection 117 | let mut intersection = wall.line.intersection(&closest_wall.wall.line); 118 | // The above only gets the intersection point between the lines. We also need to check if the point is on both wall segments 119 | if let Some(i) = intersection { 120 | if !is_intersection_on_wall(i, wall) 121 | || !is_intersection_on_wall(i, &closest_wall.wall) 122 | { 123 | intersection = None; 124 | } 125 | } 126 | if intersection.is_none() { 127 | // Check if the endpoint is before or behind the currently closest wall - if it is behind the wall is completely covered, skip it. 128 | let intersection = current_ray_line 129 | .intersection(&closest_wall.wall.line) 130 | .unwrap(); 131 | 132 | // For optimization purposes we use Math.pow instead of Math.hypot, because that way we save ourselfs of doing an expensive Math.sqrt, whcih wouldn't change the result of the comparison anyway 133 | let endpoint_distance = (origin.x - endpoint.point.x).powi(2) 134 | + (origin.y - endpoint.point.y).powi(2); 135 | let intersection_distance = (origin.x - intersection.x).powi(2) 136 | + (origin.y - intersection.y).powi(2); 137 | 138 | // TODO Cull T-Intersections that are oriented away from the current wall 139 | // endpoint_distance > intersection_distance while respecting float inprecisions 140 | if endpoint_distance - intersection_distance > 0.00005 { 141 | continue; 142 | } 143 | } 144 | } 145 | } 146 | } 147 | closest_wall_could_change = true; 148 | current_walls.insert(Rc::clone(wall)); 149 | } 150 | 151 | if i + 1 < endpoints.len() && endpoints[i + 1].borrow().angle == endpoint.angle { 152 | continue; 153 | } 154 | 155 | if i > 0 && endpoints[i - 1].borrow().angle == endpoint.angle { 156 | closest_wall_could_change = true; 157 | } 158 | 159 | if closest_wall_could_change { 160 | closest_los_wall = 161 | find_closest_wall::<_, false>(origin, ¤t_ray_line, &*current_walls); 162 | } 163 | 164 | if old_los_wall != closest_los_wall { 165 | if let Some(old_closest_wall) = old_los_wall { 166 | if let Some(intersection) = 167 | current_ray_line.intersection(&old_closest_wall.wall.line) 168 | { 169 | if closest_los_wall.is_none() 170 | || !closest_los_wall 171 | .as_ref() 172 | .unwrap() 173 | .intersection 174 | .is_same_as(&intersection) 175 | { 176 | los_points.push(FovPoint { 177 | point: intersection, 178 | angle: endpoint.angle, 179 | gap: false, 180 | }); 181 | } 182 | } 183 | } 184 | 185 | if let Some(closest_wall) = &mut closest_los_wall { 186 | los_points.push(FovPoint { 187 | point: closest_wall.intersection, 188 | angle: endpoint.angle, 189 | gap: false, 190 | }); 191 | } else { 192 | los_points.last_mut().unwrap().gap = true; 193 | } 194 | } 195 | } 196 | 197 | (los_points, start_gap_los, start_gap_fov) 198 | } 199 | 200 | fn find_closest_wall<'a, I, const IS_TIEBREAKER: bool>( 201 | origin: Point, 202 | current_ray_line: &Line, 203 | current_walls: I, 204 | ) -> Option 205 | where 206 | I: IntoIterator>, 207 | { 208 | let mut closest_wall = None; 209 | let mut second_closest_wall = None; 210 | let use_y_distance = current_ray_line.is_vertical() || current_ray_line.m.abs() > 1.0; 211 | let mut ties = Vec::new(); 212 | let mut second_closest_ties = Vec::new(); 213 | for wall in current_walls { 214 | let intersection = current_ray_line.intersection(&wall.line); 215 | if let Some(intersection) = intersection { 216 | let distance; 217 | if use_y_distance { 218 | distance = (intersection.y - origin.y).abs(); 219 | } else { 220 | distance = (intersection.x - origin.x).abs(); 221 | } 222 | if let Some(closest_distance) = closest_wall 223 | .as_ref() 224 | .map(|wall: &ClosestWall| wall.distance) 225 | { 226 | let e = 0.0001; 227 | // distance == closest_distance 228 | if (distance - closest_distance).abs() < e { 229 | if !IS_TIEBREAKER { 230 | ties.push(Rc::clone(wall)); 231 | } 232 | } else if distance < closest_distance { 233 | second_closest_wall = closest_wall; 234 | closest_wall = Some(ClosestWall { 235 | wall: Rc::clone(wall), 236 | intersection, 237 | distance, 238 | }); 239 | swap(&mut ties, &mut second_closest_ties); 240 | ties.clear(); 241 | } else if let Some(second_closest_distance) = 242 | second_closest_wall.as_ref().map(|wall| wall.distance) 243 | { 244 | // distance == second_closest_distance 245 | if (distance - second_closest_distance).abs() < e { 246 | if !IS_TIEBREAKER { 247 | second_closest_ties.push(Rc::clone(wall)); 248 | } 249 | } else if distance < second_closest_distance { 250 | second_closest_wall = Some(ClosestWall { 251 | wall: Rc::clone(wall), 252 | intersection, 253 | distance, 254 | }); 255 | second_closest_ties.clear(); 256 | } 257 | } else { 258 | second_closest_wall = Some(ClosestWall { 259 | wall: Rc::clone(wall), 260 | intersection, 261 | distance, 262 | }); 263 | } 264 | } else { 265 | closest_wall = Some(ClosestWall { 266 | wall: Rc::clone(wall), 267 | intersection, 268 | distance, 269 | }); 270 | } 271 | } 272 | } 273 | if !IS_TIEBREAKER && ties.len() > 0 { 274 | let closest_wall = closest_wall.as_mut().unwrap(); 275 | ties.push(Rc::clone(&closest_wall.wall)); 276 | closest_wall.wall = find_closest_wall_tiebreaker(origin, &ties).unwrap().wall; 277 | } 278 | if let Some(closest_wall_ref) = &mut closest_wall { 279 | if !IS_TIEBREAKER && closest_wall_ref.wall.sense == WallSenseType::LIMITED { 280 | if ties.len() > 0 { 281 | let ties: Vec<_> = ties 282 | .into_iter() 283 | .filter(|wall| !Rc::ptr_eq(wall, &closest_wall_ref.wall)) 284 | .collect(); 285 | closest_wall_ref.wall = find_closest_wall_tiebreaker(origin, &ties).unwrap().wall; 286 | } else if second_closest_ties.len() > 0 { 287 | closest_wall = second_closest_wall; 288 | let closest_wall = closest_wall.as_mut().unwrap(); 289 | second_closest_ties.push(Rc::clone(&closest_wall.wall)); 290 | closest_wall.wall = find_closest_wall_tiebreaker(origin, &second_closest_ties) 291 | .unwrap() 292 | .wall; 293 | } else { 294 | closest_wall = second_closest_wall; 295 | } 296 | } 297 | } 298 | closest_wall 299 | } 300 | 301 | fn find_closest_wall_tiebreaker(origin: Point, ties: &Vec>) -> Option { 302 | let first_ending_wall = ties 303 | .iter() 304 | .reduce(|w1, w2| { 305 | if is_smaller_relative(w1.end.borrow().angle, w2.end.borrow().angle) { 306 | w1 307 | } else { 308 | w2 309 | } 310 | }) 311 | .unwrap(); 312 | let ray_to_endpoint = Line::from_points(origin, first_ending_wall.end.borrow().point); 313 | find_closest_wall::<_, true>(origin, &ray_to_endpoint, ties) 314 | } 315 | -------------------------------------------------------------------------------- /rust/src/raycasting/types.rs: -------------------------------------------------------------------------------- 1 | use rustc_hash::FxHashMap; 2 | use wasm_bindgen::prelude::*; 3 | 4 | use crate::geometry::{Line, Point}; 5 | use crate::raycasting::util::{is_intersection_on_wall, is_smaller_relative}; 6 | use std::cell::RefCell; 7 | use std::convert::TryFrom; 8 | use std::f64::consts::PI; 9 | use std::rc::Rc; 10 | 11 | #[derive(Clone)] 12 | pub struct ClosestWall { 13 | pub wall: Rc, 14 | pub intersection: Point, 15 | pub distance: f64, 16 | } 17 | 18 | impl PartialEq for ClosestWall { 19 | fn eq(&self, other: &Self) -> bool { 20 | Rc::ptr_eq(&self.wall, &other.wall) 21 | } 22 | } 23 | 24 | impl Eq for ClosestWall {} 25 | 26 | #[derive(Debug)] 27 | pub struct Endpoint { 28 | pub point: Point, 29 | pub angle: f64, 30 | pub starting_walls: Vec>, 31 | pub ending_walls: Vec>, 32 | pub is_intersection: bool, 33 | } 34 | 35 | impl Endpoint { 36 | pub fn new(origin: Point, target: Point) -> Self { 37 | let angle = (origin.y - target.y).atan2(origin.x - target.x); 38 | Self::new_with_precomputed_angle(target, angle) 39 | } 40 | 41 | pub fn new_with_precomputed_angle(target: Point, angle: f64) -> Self { 42 | Self { 43 | point: target, 44 | angle, 45 | starting_walls: Vec::new(), 46 | ending_walls: Vec::new(), 47 | is_intersection: false, 48 | } 49 | } 50 | } 51 | 52 | #[wasm_bindgen] 53 | #[derive(Debug, Copy, Clone, PartialEq)] 54 | pub enum DoorState { 55 | CLOSED = 0, 56 | OPEN = 1, 57 | LOCKED = 2, 58 | } 59 | 60 | impl TryFrom for DoorState { 61 | type Error = (); 62 | fn try_from(value: usize) -> Result { 63 | match value { 64 | x if x == Self::CLOSED as usize => Ok(Self::CLOSED), 65 | x if x == Self::OPEN as usize => Ok(Self::OPEN), 66 | x if x == Self::LOCKED as usize => Ok(Self::LOCKED), 67 | _ => Err(()), 68 | } 69 | } 70 | } 71 | 72 | #[wasm_bindgen] 73 | #[derive(Debug, Copy, Clone, PartialEq)] 74 | pub enum DoorType { 75 | NONE = 0, 76 | DOOR = 1, 77 | SECRET = 2, 78 | } 79 | 80 | impl TryFrom for DoorType { 81 | type Error = (); 82 | fn try_from(value: usize) -> Result { 83 | match value { 84 | x if x == Self::NONE as usize => Ok(Self::NONE), 85 | x if x == Self::DOOR as usize => Ok(Self::DOOR), 86 | x if x == Self::SECRET as usize => Ok(Self::SECRET), 87 | _ => Err(()), 88 | } 89 | } 90 | } 91 | 92 | #[wasm_bindgen] 93 | #[allow(dead_code)] 94 | pub struct ExposedEndpoint { 95 | pub x: f64, 96 | pub y: f64, 97 | pub angle: f64, 98 | #[wasm_bindgen(js_name=isIntersection)] 99 | pub is_intersection: bool, 100 | } 101 | 102 | impl From<&Endpoint> for ExposedEndpoint { 103 | fn from(endpoint: &Endpoint) -> Self { 104 | Self { 105 | x: endpoint.point.x, 106 | y: endpoint.point.y, 107 | angle: endpoint.angle, 108 | is_intersection: endpoint.is_intersection, 109 | } 110 | } 111 | } 112 | 113 | #[derive(Debug, Copy, Clone)] 114 | pub struct FovPoint { 115 | pub point: Point, 116 | pub angle: f64, 117 | pub gap: bool, 118 | } 119 | 120 | #[derive(Copy, Clone, PartialEq)] 121 | pub enum PolygonType { 122 | SIGHT = 0, 123 | SOUND = 1, 124 | LIGHT = 2, 125 | MOVEMENT = 3, 126 | } 127 | 128 | impl TryFrom for PolygonType { 129 | type Error = (); 130 | 131 | fn try_from(value: usize) -> Result { 132 | match value { 133 | x if x == Self::SIGHT as usize => Ok(Self::SIGHT), 134 | x if x == Self::SOUND as usize => Ok(Self::SOUND), 135 | x if x == Self::LIGHT as usize => Ok(Self::LIGHT), 136 | x if x == Self::MOVEMENT as usize => Ok(Self::MOVEMENT), 137 | _ => Err(()), 138 | } 139 | } 140 | } 141 | 142 | pub struct VisionAngle { 143 | pub start: f64, 144 | pub end: f64, 145 | pub start_ray: Line, 146 | pub end_ray: Line, 147 | } 148 | 149 | impl VisionAngle { 150 | pub fn from_rotation_and_angle(rotation: f64, angle: f64, origin: Point) -> Option { 151 | if angle >= 360.0 || angle <= 0.0 { 152 | return None; 153 | } 154 | 155 | // In Foundry, 0° means down. In Lichtgeschwindigkeit, 0° defaults to left. We need to adjust the angle accordingly. 156 | let mut rotation = (rotation - 90.0).to_radians(); 157 | let angle = angle.to_radians(); 158 | 159 | // Normalize the direction 160 | rotation -= 2.0 * PI * (rotation / (2.0 * PI)).trunc(); 161 | if rotation > PI { 162 | rotation -= 2.0 * PI; 163 | } 164 | 165 | let rotation_offset = angle / 2.0; 166 | let mut start = rotation - rotation_offset; 167 | let mut end = rotation + rotation_offset; 168 | if start < -PI { 169 | start += 2.0 * PI; 170 | } else if end > PI { 171 | end -= 2.0 * PI; 172 | } 173 | 174 | Some(Self { 175 | start, 176 | end, 177 | start_ray: Line::from_point_and_angle(origin, start), 178 | end_ray: Line::from_point_and_angle(origin, end), 179 | }) 180 | } 181 | } 182 | 183 | pub struct Wall { 184 | pub p1: Point, 185 | pub p2: Point, 186 | pub line: Line, 187 | pub sense: WallSenseType, 188 | pub see_through_angle: Option, 189 | pub end: Rc>, 190 | } 191 | 192 | impl Wall { 193 | pub fn from_base( 194 | base: WallBase, 195 | end: Rc>, 196 | cache: &Cache, 197 | polygon_type: PolygonType, 198 | ) -> Self { 199 | let see_through_angle; 200 | if base.dir == WallDirection::BOTH { 201 | see_through_angle = None; 202 | } else { 203 | let offset = match base.dir { 204 | WallDirection::LEFT => 0.0, 205 | WallDirection::RIGHT => PI, 206 | WallDirection::BOTH => unreachable!(), 207 | }; 208 | let mut angle = (base.p1.y - base.p2.y).atan2(base.p1.x - base.p2.x) + offset; 209 | if angle > PI { 210 | angle -= 2.0 * PI; 211 | } 212 | see_through_angle = Some(angle); 213 | } 214 | let sense = base.current_sense(&cache, polygon_type); 215 | Self { 216 | p1: base.p1, 217 | p2: base.p2, 218 | line: base.line, 219 | sense, 220 | see_through_angle, 221 | end, 222 | } 223 | } 224 | 225 | pub fn is_see_through_from(&self, angle: f64) -> bool { 226 | if let Some(see_through_angle) = self.see_through_angle { 227 | if is_smaller_relative(angle, see_through_angle) { 228 | return true; 229 | } 230 | } 231 | false 232 | } 233 | } 234 | 235 | impl std::fmt::Debug for Wall { 236 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 237 | f.debug_struct("Wall") 238 | .field("p1", &self.p1) 239 | .field("p2", &self.p2) 240 | .field("line", &self.line) 241 | .finish() 242 | } 243 | } 244 | 245 | #[wasm_bindgen] 246 | #[derive(Debug, Copy, Clone)] 247 | pub struct WallBase { 248 | pub p1: Point, 249 | pub p2: Point, 250 | #[wasm_bindgen(skip)] 251 | pub line: Line, 252 | pub movement: WallSenseType, 253 | pub sense: WallSenseType, 254 | pub sound: WallSenseType, 255 | pub door: DoorType, 256 | pub ds: DoorState, 257 | pub dir: WallDirection, 258 | pub height: WallHeight, 259 | pub roof: Option, 260 | } 261 | 262 | impl WallBase { 263 | pub fn new( 264 | p1: Point, 265 | p2: Point, 266 | movement: WallSenseType, 267 | sense: WallSenseType, 268 | sound: WallSenseType, 269 | door: DoorType, 270 | ds: DoorState, 271 | dir: WallDirection, 272 | height: WallHeight, 273 | roof: Option, 274 | ) -> Self { 275 | let line = Line::from_points(p1, p2); 276 | Self { 277 | p1, 278 | p2, 279 | line, 280 | movement, 281 | sense, 282 | sound, 283 | door, 284 | ds, 285 | dir, 286 | roof, 287 | height, 288 | } 289 | } 290 | 291 | pub fn current_sense(&self, cache: &Cache, polygon_type: PolygonType) -> WallSenseType { 292 | match polygon_type { 293 | PolygonType::SOUND => self.sound, 294 | PolygonType::LIGHT => self.sense, 295 | PolygonType::MOVEMENT => self.movement, 296 | PolygonType::SIGHT => { 297 | if self.roof.map(|id| cache.tiles.occluded[id]).unwrap_or(true) { 298 | self.sense 299 | } else { 300 | WallSenseType::NORMAL 301 | } 302 | } 303 | } 304 | } 305 | } 306 | 307 | #[wasm_bindgen] 308 | #[derive(Debug, Copy, Clone)] 309 | pub struct WallHeight { 310 | pub top: f64, 311 | pub bottom: f64, 312 | } 313 | 314 | impl Default for WallHeight { 315 | fn default() -> Self { 316 | Self { 317 | top: f64::INFINITY, 318 | bottom: f64::NEG_INFINITY, 319 | } 320 | } 321 | } 322 | 323 | #[wasm_bindgen] 324 | #[derive(Debug, Copy, Clone, PartialEq)] 325 | pub enum WallDirection { 326 | BOTH = 0, 327 | LEFT = 1, 328 | RIGHT = 2, 329 | } 330 | 331 | impl TryFrom for WallDirection { 332 | type Error = (); 333 | fn try_from(value: usize) -> Result { 334 | match value { 335 | x if x == Self::BOTH as usize => Ok(Self::BOTH), 336 | x if x == Self::LEFT as usize => Ok(Self::LEFT), 337 | x if x == Self::RIGHT as usize => Ok(Self::RIGHT), 338 | _ => Err(()), 339 | } 340 | } 341 | } 342 | 343 | #[wasm_bindgen] 344 | #[derive(Debug, Copy, Clone, PartialEq)] 345 | pub enum WallSenseType { 346 | NONE = 0, 347 | NORMAL = 1, 348 | LIMITED = 2, 349 | } 350 | 351 | impl TryFrom for WallSenseType { 352 | type Error = (); 353 | fn try_from(value: usize) -> Result { 354 | match value { 355 | x if x == Self::NONE as usize => Ok(Self::NONE), 356 | x if x == Self::NORMAL as usize => Ok(Self::NORMAL), 357 | x if x == Self::LIMITED as usize => Ok(Self::LIMITED), 358 | _ => Err(()), 359 | } 360 | } 361 | } 362 | 363 | #[derive(Copy, Clone)] 364 | pub struct WallWithAngles { 365 | pub p1: Point, 366 | pub p2: Point, 367 | pub angle_p1: f64, 368 | pub angle_p2: f64, 369 | pub line: Line, 370 | pub sense: WallSenseType, 371 | pub see_through_angle: Option, 372 | } 373 | 374 | impl WallWithAngles { 375 | pub fn new_copy_props( 376 | prop_src: &Wall, 377 | p1: Point, 378 | p2: Point, 379 | angle_p1: f64, 380 | angle_p2: f64, 381 | ) -> Self { 382 | Self { 383 | p1, 384 | p2, 385 | angle_p1, 386 | angle_p2, 387 | line: Line::from_points(p1, p2), 388 | sense: prop_src.sense, 389 | see_through_angle: prop_src.see_through_angle, 390 | } 391 | } 392 | 393 | pub fn to_wall(self, end: Rc>) -> Wall { 394 | Wall { 395 | p1: self.p1, 396 | p2: self.p2, 397 | line: self.line, 398 | sense: self.sense, 399 | see_through_angle: self.see_through_angle, 400 | end, 401 | } 402 | } 403 | } 404 | 405 | pub struct Intersection { 406 | pub point: Point, 407 | pub height: WallHeight, 408 | } 409 | 410 | // TODO Locate this into a different module 411 | #[wasm_bindgen] 412 | pub struct Cache { 413 | #[wasm_bindgen(skip)] 414 | pub walls: Vec, 415 | #[wasm_bindgen(skip)] 416 | pub intersections: Vec, 417 | #[wasm_bindgen(skip)] 418 | pub tiles: TileCache, 419 | } 420 | 421 | impl Cache { 422 | pub fn build(walls: Vec, tiles: TileCache) -> Self { 423 | let intersections = Self::calc_intersections(&walls); 424 | Self { 425 | walls, 426 | intersections, 427 | tiles, 428 | } 429 | } 430 | 431 | fn calc_intersections(walls: &Vec) -> Vec { 432 | use partial_min_max::{max, min}; 433 | let mut intersections = Vec::new(); 434 | if walls.len() >= 2 { 435 | for i in 0..walls.len() - 1 { 436 | for j in 0..walls.len() - i - 1 { 437 | let (i_walls, j_walls) = walls.split_at(i + 1); 438 | let wall1 = &i_walls[i]; 439 | let wall2 = &j_walls[j]; 440 | let bottom = max(wall1.height.bottom, wall2.height.bottom); 441 | let top = min(wall1.height.top, wall2.height.top); 442 | if bottom <= top { 443 | let point = wall1.line.intersection(&wall2.line); 444 | match point { 445 | Some(point) => { 446 | if is_intersection_on_wall(point, wall1) 447 | && is_intersection_on_wall(point, wall2) 448 | { 449 | intersections.push(Intersection { 450 | point, 451 | height: WallHeight { top, bottom }, 452 | }); 453 | } 454 | } 455 | None => {} 456 | }; 457 | } 458 | } 459 | } 460 | } 461 | intersections 462 | } 463 | } 464 | 465 | pub type TileId = usize; 466 | 467 | #[derive(Default)] 468 | pub struct TileCache { 469 | pub occluded: Vec, 470 | pub id_map: FxHashMap, 471 | } 472 | 473 | impl TileCache { 474 | pub fn from_roofs(occluded: Vec) -> Self { 475 | Self { 476 | occluded, 477 | ..Self::default() 478 | } 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /rust/tests/zero_width_walls.ascii85: -------------------------------------------------------------------------------- 1 | <~!-m5bSU$_?`O3a)NuLrP)c7k1O@M!Vq]:]"&Oj\+#q?S,"Z;0N0VX@ioEJ1ag^rJ*,,.'<3m>;mF(apb0Ye)>TJ)2^@,;P0,Du=c>f%HD;l@t;e[])eGlKr+REt^"]iEV=e^`JUeQo0(DV)G=CX]9VIrdMl-cug!E*c)Uf2\n:gDjTerRpUds0"LdJ$qAQK)2guJbih;]"S;ib]G`]Nb1TUg#`8BlHaWDZ_7;6F+=GeB=fr0dV1McBnQ,W?^OU8NqEeG%ks#lqm4O]r?Q"Hc]&qe(\B"DZ4Yen&1,W-lZHF@lkL8UcD$PDQLC+HSE65^3\(]a"O;nBS9E0co3,"\dEYo?SIJ>2]8u'^1E3#.\a@S\`qbscd<>?\W6ct6!`#0)CO)Fnaj2@g+#ek5:WIKVmMBeWaec9&j3jQsHirG1p5/AoU?hEnTh`V,D09@M`0>F#RF(b7VmEC9R*M_TFZQQEcK')i&(1s,.PGrn0BGmWl1d"aViOTnDnC*lPf?JC-+'9lV.:PfM_uC>khj=$rY"lc[qtdm*Ik5rF%PT&p55Eb+?BaU*9Z!]jS%KM`V5>?n]pj@VPJG$/GV5qcB@'&:3h0KcCXS+e_NKNO6>=II42`UkB59Bal+quDNO0=+oqF^7ATf0da'FGO''*D:mR;&hZU[6q]1NmD3*:444'ef/n^p(W"[e"JbHl^%0:6!_.cq-3c\YWjOT-53!P5'FBM4&.9msC9$bdBEfRt[W*CUo)W/],oZ[87gIi"P'_k$$el7p"=pEIdTP]l9if1uipRklBag_7Iu:`qKJ]!LDO4G895e/;_src8rXEb3NA`TQ+1=UXK\TK_DBYiXfLY^]t;lQJADn$(V%*:.a.H0+>':hoqJ2o([H?GV&>H[2f3_DP[Hi_>O[\]7=lPdoeP&DnD(,"2Da&MB+a`*WmeiPK)QhXl\USXZ(pm87TPqqqYT>t@a8YnX?sGnWBZAduksbm;rM@riN/rs?JFP@[Glcs:U=.GJl_SVYj8,kJdYuKcQ-N4YY!0jK_6^@1[oHc3_05)B4-^UD\fXac4$0'iks1jbp.MDHL@*V'bO#+6*t97Jc9jguGqIZG3nX.(s0$_(na4%A1GFi#?GQ>B90#.OVs;4I+i`arB7oUHISkafG"MuVa)tZTa7l:?8Up(*OA6e?3,e'Vqau#9L$8TtI3Hc9N+99_RW73,_=C9Xs43_ob+fj3s!UCD*H$5,5'-%75p*\/%`Q/3eR8=A+<=i0bIiAqo7_::EU3Q%I=QTc<(b[S1>P_TBVn>`B42m>u30r__0J8u>J8m-$"Y_R$EdHD6B$e"F%Pj%n33o+T+V'g+j0aWJ<]e8JKaoihH3D3$iFnOqKn`Ms%eV%^s/0fmF00'oqbT*#9$c'758&ugY*JhqLo5Ml6.jjCj_Td0Xkl&Oj7?*Vk]B7B!/NoFF^pQ.`84ek8*+UdZ!L'Bm!NZM=J,U[RYRk&PM+f%-JCHB\ZMZ9)qV6Zee5c]lmr`_ro=E4$UJP0@5a55Q5j9OA;<`4LUj!Tlei[]>)@nG)uZ(r))OO)?6T)J8j8"\Q7GAQ'VeIZQq.^S8qo,WaCPC+m)ri`HbN:'TQe.jhpk3ooY(5bdGe7?M,l.B]`LTMW9D2bXF>>6J'G8U;%2*CFMf.Nb5G*KTXOc8EY5TNt[OcY,)A?VmqKbk*CHggKBt)^tdAi-l.5r_"_6:\q=rYF%c6cS@ru'f,.9,glfFM!XBR_c?U+ROilGLIqtUO&j*<*%!JYYu<'!,H7aHpJe'[brguBJ,@\Ve,%Sd.,-aI\ZUt:WLdkP`BB=-c^9"r^c$7L-p)nnQi-@d4C-n4p6U]WmnJ`UZY9!#NnV!HAMG'k<9d\*p5l/l-.Un*R9)=S7]@1X<&N)c!9ii_c^P4Z>BX)Rfn?,oq!K(\W1YjDU[`"NR"[ps-i8V;)&\=\>?AbP_+W].kGICudEFGRIrs,dd3c.o$@kr3&]"#+VR<&u=e:5Mh[c+)"rFmO:MA)iRF1EPi-E/MV[$B/IpItg<2)dkW-Ce%glJ8tIf*6jYVLZ-U.a!,h)OH,t1jR:,K*b.0>aE`Ng(&';qP'DW<5TNc5QH(I2eXr9o\o-#V%dO>(jP&;+\=Rlkosse'YW`_AM]SO90B$TagSGn733F,;3lF!rj_0suV\m].qAc=0A_p)L2N0N%CFhL%;t;l0_)Rb43+0Ciar-Sa&_B!M8\3I:0X#53kh.")&ATTU?CZ"0s:SECSGl)o4=($0\:DW3[bNCA.Gd+_Ztp;K%2qZlqj2eIq'o:ORILH3d<*P[*hf'.%T>8K>L:,A5^VGWqQqNtHHc\q,*3b-oaIX"r3U&*5/muGK\!gmbJ5987!?s'&f3d+DGmJoo>;T#1d+o?9C!));j%Hg4'CS,_,!9:T_WDqdeF4j9]",u9/O$\U=V^icq*&"B#*Fc[PdK,ZPOl8]i=(L@q:=\*m6]BKZ%^758`Gg!o]FII;%PWnnC.!anV^ppF!9&YpfBKF:6".+Q.H6ilh@DD+kQTH17d>DQ9U&C!gp$`qc?pmJ[rAV7QT!ASb:rjL3IWBk+saP?nO5c4MQF:[7-oj$L')ObCe\Jn?I?_"INseMIQ,4">G;>>/'1ZYKQ8pSdNnshr/Jtj*.Zb1*`@5^c=I2/mp;)I4MVI^pji=kl>eX]>F+=F4oD>3kZ]-I18\&=&7FSq+aZXB"agb4?r7haYT=-?>MS;gJ1%qclpEn.d,q6c32%Q!-q9mnj6uAiG=%W2Ag^j6gs!p)9c[<1S"&W37A=lhBVXur3X;-1iHYchA(`1/j*,L.UD_,E%R?W%>Vj'WV&[`>WY25;o/Sc'%i1_kB!;pM29LZR"sR354kRbKsu"7^nfb2mQ]mQGadPVh(kIV^/X5UEP?EH\'i"V^t3lF!`jaZC3+Htg!tE<6Fb*EP$duJiLZ/$@3ZCiW*6pge)5tk[kl=B9-OkjNHC)@:%NWfWGeN(;LV21WH%AmX+EWEZ]#rDOM(9%5'(W%ZncG^uhp<#dcr>+;JE_QD?n;6O^j1b8-\dbimMltOD'i#j!:%@6onf8KR*6W74!-@gc%;0Vik>oL2PoRAZPOF,(F7gu>B*_#6A36DB[f(Jj`4"8L9.?e,Mfb$s0NUcj/]I`9*6nD*ij>HuO$f.:.icn!)b>E?6TLTT/Djk:L>V't*?Y[c,^E6>bI%P^^[3LV7(MH:0U1EPR+gKuq/ENI<1(2T>o(MXB"+*SeC0+AqO9]:>8OmtN"R1Pr+^j)H&Ec0+[bV1"tG;:d5Q[u]k2;u(Jb[hh%cTtqk6hbjPk6OR6go'\Id,Qg_T&rkprJ,>Kj7tcX4,Q)&RB*)D1A>nLBZBYYk$B2Nc!6"[H^WV]CmSbH>r"'@4KiTsJ)P(\^R9L9Qjtj;WK6(Bi]SDl]\hVF$',6EcMNUX@5h.[FMfTc'X/%K;)<0D!^g+ic.%Zqg^g';?`U=+'1QGhW,BSb3*$=9U`oVPqF]`6br(_OOQu`YZ<"i@#FMRF02)VWUX:q8SnMW:P$X*KJ01$9XGX#Z)IE?>@Cl\[5(0RfW]3l$pR#fAYhfB:i%%I5f)4pP[a(S2G]\:'mr3G^L.nU/>::C9TJ\$/qYI0[c;bO=3fEsq5]6%8,7(me%D9**'(a^e_nG^eaZdFJ>><>SP\]5TTJPr.&E^Qf&J`:;`Q=YiIb+)sNlrdj^XuVF/RmhFOnrk6RITA2_Ed3s39'a],#t;kZ6C-214e^=<4B^Z/37g*g@aLEV]^YiI`M)X5U3M=3s!`uS7p?XYoqkjg9,*L;JJ/#N-DniPUA>k&t,3eos]qh2ut=RqsAN-)_=/+&7@iFnOqV1q@Xh1#O+5[5sm\K[J@?EdZoRdjLegsRIgbuaNb1B`?hj)H"3_Klkc]4Y$^`H=k7.5MF'qSr1QCsMB:@dm`lCk9^plQC(5I$k(gjnb1*!j+-YSM7`*!QVhaC(f.W/n)U;AhXs3D-3&!J@,4jT:2JcHGESe!\!lj-APM_o5QCP/<=p^.[a(.6H\;nZ$!98"da_t>aj1.MIH4#V[Q.I?j8R,[`FMI);16hA13+!M_AmDO=m>;=lF,oF&KI*^popfbnH+LI(kmF3'7G&nU&L&dEImU4EQ+aErQTq)hk%)f-gA7:115pqI7W9T/eT#=+5o<=;'M.3-R_rF[f\[g6'u6@(Zrkd>[8J8I(W`\=pC4Fc"rOg:Enc.bgE=ZJN1V8(H%>Fu]1`GHhK,`5\*p*cf62rA2)%&EE:Rj_3d+WEaI[$^duB1Fs%jDYN8$"66pCauL'(;2-k6S-<>hJ[qeq)X7/7d\6A\r`<.n_Tt+AYP+IZp3$U,k-]mQm_eZKF8+#blH&f6h(]?YeaQHK08\4@.H+aHC\.A=Td:qE0Q[+j*-QLBo]T5"oF$)F#)%O>Q_g0"3Pil%cL)B/ke9R=m&N[ii*Vm>V/;^tX:3Y?nNCI6tZpW0\OhpfpaM(S\*6Vu'@$l0)d']j#'pRlqUHD=f7t+AH(oLTc43YnBJO//hSJnNS5*kjZ(r,ehT.>I=nPmDoWq2D:([#p'P\78[d(QmfAsn$Q#+!1p$tfk,FhEjg7d\:n%#iZ#I71l6G^mWa+3Lq;aD72ekZK>i)"2Z`"!`G%riPapKqhf!#I[k#&52"1%*/*nq0jct"q<88Vb4*X""`]KqC:9Dhrj(\W_Z!;(tUXVs,7B+F^M!JN;HH52b8L>5IC?S4#$)XY'Nc$RW83J[iH<`>PW<,(GS[OhJn>[`p$BNBk/n=)jg4/(ltT;0goD3#GpcUiFq@GIR:<,h[:tQRhG\#^c83(s2\S1Dl3GYiFnPTVThZ0NLUl@1BSW^P:IGj>_2D^\Oo@J*j_3r1,,;D-.;m\oP]TX*LJ@Q\be44'H)km:WPEp(sbj#mN5h!p.Kh?Ohl:T;6GNXN6d'TkJHhB(oJn=3ab'o7(0eE\F*BL$1"pIT?R$ObB/:)S0<4PNss$Vbk.tW)3!_eagKHFn_=/u4p0iTlF3HS/;]]1%W/hMX!k/5_"`;@qZdta@?PD:!9:`R3b#mL:7kd(";oI/OR%N!j8*%E.BN-TJ'6G^\aCCjY&j.Qj1tFK$i^d86]B>_\Y^<"*]$qIqQYVA^6%RZF%%ouJ"`6>%uf\C-q?(26Y0T>GWtI\d1659Ij04#*?(QkA^&0WoK&beQ3@h&JJop_kB<40TtOAiR]Dfun?ug>_QujEc!bGi#u9HO*^d!Kg4XiUtXgE0mP/XP]%9BsY2U]+(Nr7qb>!)(%?0ds\@pq6Ot9KAP]>EcT?*OJ&LP5(KMNT0D>pq4RL,f$p*.Rr(Ur#%+Ioh]nb0tPU,(h3!7+iEco>Gm'agGr8-1TN?F1#lrlQ/S%m3/aq)aE=G!>n$?f6%miAir:QXoeidkC:m'66_%.rSh8p#T3:Va>Isf-0#2j#Y-]=?VpE@2UppC@G;0K\$K,J>60]Gg<4XS(iP&<^2(_u'K,Ge(sP$@H"k:iWb]TO(1;N+P]J'/NuH<=i7o.:+(.3k+nmcHmQ[#h;d`0ZaDe'uo5fhd"3d&/P2cA%\cIcemkWmo+'A*'[u>EBc"B6W;m+(D/;a*T*'_c^He<39j6]gkg#*8IgD42P#?pkrfIkF35571tBZ>J+FmfZ-Z_Ur535G;nM03:Q,V\X^U@5NZ##!rce"K.dY3*-Ec:T'ags8hNqXfUIm].hBqOM_[^04:rOk./k66Y*DHn)#)Gq7SDe]QOD;B78l8b2J:5S.W=pb3<\'o_kW-@%=B?-J=Da0,kb+X.6o4,M0:+`lCP/'66('%&Xd"e]Gl?*8)D;N42_GPB&Vf)pZ9]f>0p_i)I(:Nh9,7k:k$Ng442"b*`Bm?[k`@jT8*7IYL4,c$2=H!t=pX>,RG5g\OP_cPPKh#F<;L$tkTDe66%EES^lN#Y-\;V=mTbV57<-QPrUJP\5VGSdjDI\V>A69B*+/6hdt@[1r?JNb5GFqX:3^N7ZZ\CD^dJ)Z3i-;.a4=iUCfuJFRr+.^,Tj0<.ObdEFJp*6JGIE&?UBk+UoTN74&-r;]LM&V`).D\mif-dQ0lI&,^@WQRDRD-1CZ8_di9:#L+JS1]-"j>nO$P$"mE1es[dc/\&YPj:8B`kS4Xi->Oe=K\n1N74'h)-@OG2k9m*9JDtC:nCR[4ES:re!d7NN@3!G2tFT]`kRXY*gg^aC,eSs).;M_!:oCgSqFg`F#P>IDNNJ!@9Wu\2:FZiRPULIYTt9"3+K`A@5oLIl"`8,_e*OI'CGm).a6IgA+P2C@9dcmJHHA_6B<^GU_i8bmK/;"rL?e`=cCJ]0GPlB&2q$Ab4*$lL^XeHq+/"ne:$C>pGm8=Ae998YndNP86DajDfAf_'!B?:%]I#pNe2OJWd>ZBbj]hltSSp@b0@+#tI&u9H$[E1>neqq^35ZU=H-J/9RKr(V)X14Qdq1RVjNtB:Bs(m9_34c2auC0;FPEP\H56!#0_-]D]R4"!1[7aTeAF2>*dZugUHFtkot/JIoA8_r%^mCFC[Gl4dhU`W'"3hplY6&3KO?a?5*c-9D0@9JFe#Y)2jREK+ooF%uX<2IbK$?sGnlQ;*se>bh:q>FrZ9,E9Ca>d$8B\OQVTV5&^=R;8Vm5>1uG31@B)\lYVM)n%j-h9/m`R)$;@OhoiYJ>\fs=*q41BcWo_DPFEYBIqu8`+KmB,f@>dP?Fqdo\'N)8:[C=,bgETTK)<#ph>29bs022Z]+g&$dner<]?2eQcq\3+$V/[OC-$+h8!ENn"8D3'a'*P"0Pm!m'.?RKjHrKj-];BDuTL[3mo\GX9N5+)(8YU6""T7NVKhqbJcfcF(;r*"hu<;hj8)I3l]!!M?Fk?@pa7$,DYX$-DTUcbJ(j?ab2`NbG[Zq1&,pjUX$5^KlOJqmED(2,l]4h[ae"_<4jP\9$uu4nV/'s!j1(NBR8:SCi<^294Jk2Gh0"73OD_#EBol]/`jAifDY4S_E&c[oY1VqQXd-]TF?l^l.4/ltuDh4gh-qX<$L=esVgM)b4h]8N6tG/M$on%+s_:-JEd5@*1_d9:QJ1cKsFTfP>qrB-oFe^G%Gs2W_q^YUce1_68QhRK)6]'J[gO8Z^4T75qEO,#r=`g'+hpFD0#5/qA%[;R/;=$MJBM<^_*F47Llo<).ZZC@oLm@Gh'lop'B!rQ/;p1&M7qQUb7DL]*\@N+.JdQ@.g9^PW(CQ@&n5FH6+rd%I,)WL(DRW'W5XP>Sql>XGURk?Y/s&89iH1Ck_V#"EtFi1:D`_U^Jc@1!9f`ejoa%LU/n)A'KnL"P^?n%RIcdJE-e>lGa.@\Pq^2Wa)OG_%48YMfIs"(:2fIE-rC^L\3\:!f>k7,H0E1JYNr@Z[r`S'\?fKhDH/j$oaDpIo?=0Ra\p;PM^@[:#rOBQ6@pD3hT=8P!nq4"=HJsoT*(N+Ti;Fed1h-7'QhKM6gM[W&5K,_hnD^PCIrgeCLHXX?,NkEtNcX.2aQpmTI<"I7>lOMC7\%4X^.X^-@E`HM2*M2-hYKs*'XLDXQ+7j+Z?mnB=YjXM&CKH^f!%Y"(UfV^oDmaJdoB4m!Z#pj$ko[BI0YlFm'YoV4.DiTZOU?g-4m(&_gqpr*IHEt*8]`uK9j'<^QQ\U>R6@]aG@gYZp1@X+:kat0Q=pA5<%Z/a#;qlS%c`P3h3k@]R"jVeX_\MCqG+g1UnJR?XbGckh(n(r?42%R9]]nZQ8@[HMcnWMII055[dPrqlJ5Ot&r]#LeRc$O*G8Zq-H+!,Cl\KAE&CIapO-,$c<*W+X]-SBhn2%KSfn*dHihj&Y=c_f7E'Y26,U;ha[FEV5>raNf!hn%&iT)I(h@?Ns7@aa/oqb+DnG=g'@(Y.e(tpB4m5I=Z$GAB(JQs/2;J2jcVB;iEg>gh??X;fOfmZ+o5;80jDS>cW(2pKL\m/4^W6,a^;nl0]Oo+ZS_M0m:S%#AkCWE;]_A34kA6&(%'SmlYMWiKgsl4a?gm7qI(n>gcFYKFffABQL:r),/*qA%^EI`XP&#+$\*G*,E-HPdY(#d+Vf2Ud'ct`3Z?uS$rrp?7L7J~> 2 | -------------------------------------------------------------------------------- /rust/src/serialization.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | use crate::geometry::{JsPoint, Line, Point}; 4 | use crate::raycasting::*; 5 | use js_sys::{Array, Object}; 6 | use nom::bytes::complete::take; 7 | use nom::IResult; 8 | use std::convert::{TryFrom, TryInto}; 9 | use std::fmt::Debug; 10 | use std::mem::size_of; 11 | use yazi::{compress, decompress, CompressionLevel, Format}; 12 | 13 | const CURRENT_VERSION: u8 = 3; 14 | 15 | pub trait Serialize { 16 | fn serialize(&self) -> Vec; 17 | fn deserialize(input: &[u8], version: u8) -> IResult<&[u8], Self> 18 | where 19 | Self: Sized; 20 | } 21 | 22 | pub trait SerializeByte { 23 | fn serialize_byte(&self) -> u8; 24 | fn deserialize_byte(input: &[u8]) -> IResult<&[u8], Self> 25 | where 26 | Self: Sized; 27 | } 28 | 29 | impl Serialize for T 30 | where 31 | Self: Sized, 32 | { 33 | fn serialize(&self) -> Vec { 34 | vec![self.serialize_byte()] 35 | } 36 | 37 | fn deserialize(input: &[u8], _version: u8) -> IResult<&[u8], Self> { 38 | Self::deserialize_byte(input) 39 | } 40 | } 41 | 42 | macro_rules! ImplSerializeByteForEnum ( 43 | ($name:ident) => { 44 | impl SerializeByte for $name { 45 | fn serialize_byte(&self) -> u8 { 46 | // TODO Try into would be better here 47 | *self as u8 48 | } 49 | 50 | fn deserialize_byte(input: &[u8]) -> IResult<&[u8], Self> 51 | where 52 | Self: Sized + TryFrom, 53 | >::Error: Debug, 54 | { 55 | let (input, byte) = take(1usize)(input)?; 56 | Ok((input, (byte[0] as usize).try_into().unwrap())) 57 | } 58 | } 59 | }; 60 | ); 61 | 62 | impl Serialize for f64 { 63 | fn serialize(&self) -> Vec { 64 | self.to_be_bytes().into() 65 | } 66 | 67 | fn deserialize(input: &[u8], _version: u8) -> IResult<&[u8], Self> { 68 | let (input, representation) = take(size_of::())(input)?; 69 | Ok(( 70 | input, 71 | Self::from_be_bytes(representation.try_into().unwrap()), 72 | )) 73 | } 74 | } 75 | 76 | impl Serialize for u32 { 77 | fn serialize(&self) -> Vec { 78 | self.to_be_bytes().into() 79 | } 80 | 81 | fn deserialize(input: &[u8], _version: u8) -> IResult<&[u8], Self> { 82 | let (input, representation) = take(size_of::())(input)?; 83 | Ok(( 84 | input, 85 | Self::from_be_bytes(representation.try_into().unwrap()), 86 | )) 87 | } 88 | } 89 | 90 | impl Serialize for usize { 91 | fn serialize(&self) -> Vec { 92 | u32::try_from(*self).unwrap().serialize() 93 | } 94 | 95 | fn deserialize(input: &[u8], version: u8) -> IResult<&[u8], Self> { 96 | let (input, value) = u32::deserialize(input, version)?; 97 | Ok((input, value.try_into().unwrap())) 98 | } 99 | } 100 | 101 | impl Serialize for Point { 102 | fn serialize(&self) -> Vec { 103 | let mut data = Vec::with_capacity(size_of::()); 104 | data.append(&mut self.x.serialize()); 105 | data.append(&mut self.y.serialize()); 106 | data 107 | } 108 | 109 | fn deserialize(input: &[u8], version: u8) -> IResult<&[u8], Self> { 110 | let (input, x) = f64::deserialize(input, version)?; 111 | let (input, y) = f64::deserialize(input, version)?; 112 | Ok((input, Self { x, y })) 113 | } 114 | } 115 | 116 | impl Serialize for WallHeight { 117 | fn serialize(&self) -> Vec { 118 | let mut data = Vec::with_capacity(size_of::()); 119 | data.append(&mut self.top.serialize()); 120 | data.append(&mut self.bottom.serialize()); 121 | data 122 | } 123 | 124 | fn deserialize(input: &[u8], version: u8) -> IResult<&[u8], Self> { 125 | let (input, top) = f64::deserialize(input, version)?; 126 | let (input, bottom) = f64::deserialize(input, version)?; 127 | Ok((input, Self { top, bottom })) 128 | } 129 | } 130 | 131 | impl Serialize for Vec { 132 | fn serialize(&self) -> Vec { 133 | let mut data = Vec::new(); 134 | data.append(&mut u32::try_from(self.len()).unwrap().to_be_bytes().into()); 135 | for wall in self { 136 | data.append(&mut wall.serialize()); 137 | } 138 | data 139 | } 140 | 141 | fn deserialize(input: &[u8], version: u8) -> IResult<&[u8], Self> { 142 | let (input, len) = take(size_of::())(input)?; 143 | let len = u32::from_be_bytes(len.try_into().unwrap()) as usize; 144 | let mut vector = Vec::with_capacity(len); 145 | let mut input = input; 146 | for _ in 0..len { 147 | let (new_input, entry) = T::deserialize(input, version)?; 148 | input = new_input; 149 | vector.push(entry); 150 | } 151 | Ok((input, vector)) 152 | } 153 | } 154 | 155 | impl Serialize for Option { 156 | fn serialize(&self) -> Vec { 157 | let mut data = vec![]; 158 | data.append(&mut self.is_some().serialize()); 159 | if let Some(value) = self { 160 | data.append(&mut value.serialize()); 161 | } 162 | data 163 | } 164 | 165 | fn deserialize(input: &[u8], version: u8) -> IResult<&[u8], Self> { 166 | let (input, is_some) = bool::deserialize(input, version)?; 167 | if !is_some { 168 | return Ok((input, None)); 169 | } 170 | let (input, value) = T::deserialize(input, version)?; 171 | Ok((input, Some(value))) 172 | } 173 | } 174 | 175 | ImplSerializeByteForEnum!(WallSenseType); 176 | ImplSerializeByteForEnum!(DoorType); 177 | ImplSerializeByteForEnum!(DoorState); 178 | ImplSerializeByteForEnum!(WallDirection); 179 | ImplSerializeByteForEnum!(PolygonType); 180 | 181 | impl SerializeByte for bool { 182 | fn serialize_byte(&self) -> u8 { 183 | if *self { 184 | 1 185 | } else { 186 | 0 187 | } 188 | } 189 | 190 | fn deserialize_byte(input: &[u8]) -> IResult<&[u8], Self> { 191 | let (input, data) = take(1usize)(input)?; 192 | let data = match data[0] { 193 | 0 => false, 194 | 1 => true, 195 | _ => unreachable!(), 196 | }; 197 | Ok((input, data)) 198 | } 199 | } 200 | 201 | pub struct TestCase { 202 | pub call: RaycastingCall, 203 | pub los: Vec, 204 | pub fov: Vec, 205 | } 206 | 207 | impl Serialize for TestCase { 208 | fn serialize(&self) -> Vec { 209 | let mut data = Vec::new(); 210 | data.append(&mut self.call.serialize()); 211 | data.append(&mut self.los.serialize()); 212 | data.append(&mut self.fov.serialize()); 213 | data 214 | } 215 | 216 | fn deserialize(input: &[u8], version: u8) -> IResult<&[u8], Self> { 217 | let (input, call) = RaycastingCall::deserialize(input, version)?; 218 | let (input, los) = Vec::deserialize(input, version)?; 219 | let (input, fov) = Vec::deserialize(input, version)?; 220 | Ok((input, Self { call, los, fov })) 221 | } 222 | } 223 | 224 | pub fn serialize_ascii85(data: T) -> String { 225 | let version = CURRENT_VERSION; 226 | let data = data.serialize(); 227 | let mut compressed = compress(&data, Format::Zlib, CompressionLevel::BestSize).unwrap(); 228 | compressed.insert(0, version); 229 | ascii85::encode(&compressed) 230 | } 231 | 232 | pub fn deserialize_ascii85(input: &str) -> T { 233 | let input = ascii85::decode(input).unwrap(); 234 | let version = input[0]; 235 | if version > CURRENT_VERSION { 236 | panic!("Data stream has a wrong version number."); 237 | } 238 | let input = &input[1..]; 239 | let (input, _) = &decompress(input, Format::Zlib).unwrap(); 240 | T::deserialize(&input, version).unwrap().1 241 | } 242 | 243 | pub struct RaycastingCall { 244 | pub walls: Vec, 245 | pub roofs: Vec, 246 | pub origin: Point, 247 | pub height: f64, 248 | pub radius: f64, 249 | pub distance: f64, 250 | pub density: f64, 251 | pub angle: f64, 252 | pub rotation: f64, 253 | pub polygon_type: PolygonType, 254 | } 255 | 256 | impl From for Object { 257 | fn from(value: RaycastingCall) -> Self { 258 | use js_sys::Reflect::set; 259 | let result = Object::new(); 260 | set( 261 | &result, 262 | &JsValue::from_str("walls"), 263 | &value 264 | .walls 265 | .into_iter() 266 | .map::(|wall| wall.into()) 267 | .collect::(), 268 | ) 269 | .unwrap(); 270 | set(&result, &JsValue::from_str("origin"), &value.origin.into()).unwrap(); 271 | set(&result, &JsValue::from_str("height"), &value.height.into()).unwrap(); 272 | set(&result, &JsValue::from_str("radius"), &value.radius.into()).unwrap(); 273 | set( 274 | &result, 275 | &JsValue::from_str("distance"), 276 | &value.distance.into(), 277 | ) 278 | .unwrap(); 279 | set( 280 | &result, 281 | &JsValue::from_str("density"), 282 | &value.density.into(), 283 | ) 284 | .unwrap(); 285 | set(&result, &JsValue::from_str("angle"), &value.angle.into()).unwrap(); 286 | set( 287 | &result, 288 | &JsValue::from_str("rotation"), 289 | &value.rotation.into(), 290 | ) 291 | .unwrap(); 292 | result 293 | } 294 | } 295 | 296 | impl Serialize for RaycastingCall { 297 | fn serialize(&self) -> Vec { 298 | let mut data = Vec::new(); 299 | data.append(&mut self.walls.serialize()); 300 | data.append(&mut self.roofs.serialize()); 301 | data.append(&mut self.origin.serialize()); 302 | data.append(&mut self.height.serialize()); 303 | data.append(&mut self.radius.serialize()); 304 | data.append(&mut self.distance.serialize()); 305 | data.append(&mut self.density.serialize()); 306 | data.append(&mut self.angle.serialize()); 307 | data.append(&mut self.rotation.serialize()); 308 | data.append(&mut self.polygon_type.serialize()); 309 | data 310 | } 311 | 312 | fn deserialize(input: &[u8], version: u8) -> IResult<&[u8], Self> { 313 | let (input, walls) = Vec::deserialize(input, version)?; 314 | let (input, roofs) = if version >= 3 { 315 | Vec::deserialize(input, version)? 316 | } else { 317 | (input, vec![]) 318 | }; 319 | let (input, origin) = Point::deserialize(input, version)?; 320 | let (input, height) = if version >= 2 { 321 | f64::deserialize(input, version)? 322 | } else { 323 | (input, 0.0) 324 | }; 325 | let (input, radius) = f64::deserialize(input, version)?; 326 | let (input, distance) = f64::deserialize(input, version)?; 327 | let (input, density) = f64::deserialize(input, version)?; 328 | let (input, angle) = f64::deserialize(input, version)?; 329 | let (input, rotation) = f64::deserialize(input, version)?; 330 | let (input, polygon_type) = if version >= 3 { 331 | PolygonType::deserialize(input, version)? 332 | } else { 333 | (input, PolygonType::SIGHT) 334 | }; 335 | Ok(( 336 | input, 337 | Self { 338 | walls, 339 | roofs, 340 | origin, 341 | height, 342 | radius, 343 | distance, 344 | density, 345 | angle, 346 | rotation, 347 | polygon_type, 348 | }, 349 | )) 350 | } 351 | } 352 | 353 | impl Serialize for WallBase { 354 | fn serialize(&self) -> Vec { 355 | let mut data = Vec::with_capacity(size_of::()); 356 | data.append(&mut self.p1.serialize()); 357 | data.append(&mut self.p2.serialize()); 358 | data.append(&mut self.movement.serialize()); 359 | data.append(&mut self.sense.serialize()); 360 | data.append(&mut self.sound.serialize()); 361 | data.append(&mut self.door.serialize()); 362 | data.append(&mut self.ds.serialize()); 363 | data.append(&mut self.dir.serialize()); 364 | data.append(&mut self.height.serialize()); 365 | data.append(&mut self.roof.serialize()); 366 | data 367 | } 368 | 369 | fn deserialize(input: &[u8], version: u8) -> IResult<&[u8], Self> { 370 | let (input, p1) = Point::deserialize(input, version)?; 371 | let (input, p2) = Point::deserialize(input, version)?; 372 | let line = Line::from_points(p1, p2); 373 | let (input, movement) = if version >= 3 { 374 | WallSenseType::deserialize(input, version)? 375 | } else { 376 | (input, WallSenseType::NORMAL) 377 | }; 378 | let (input, sense) = WallSenseType::deserialize(input, version)?; 379 | let (input, sound) = if version >= 3 { 380 | WallSenseType::deserialize(input, version)? 381 | } else { 382 | (input, sense) 383 | }; 384 | let (input, door) = DoorType::deserialize(input, version)?; 385 | let (input, ds) = DoorState::deserialize(input, version)?; 386 | let (input, dir) = WallDirection::deserialize(input, version)?; 387 | let (input, height) = if version >= 1 { 388 | WallHeight::deserialize(input, version)? 389 | } else { 390 | (input, WallHeight::default()) 391 | }; 392 | let (input, roof) = if version >= 3 { 393 | Option::deserialize(input, version)? 394 | } else { 395 | (input, None) 396 | }; 397 | Ok(( 398 | input, 399 | Self { 400 | p1, 401 | p2, 402 | line, 403 | movement, 404 | sense, 405 | sound, 406 | door, 407 | ds, 408 | dir, 409 | height, 410 | roof, 411 | }, 412 | )) 413 | } 414 | } 415 | 416 | #[wasm_bindgen(js_name=serializeData)] 417 | #[allow(dead_code)] 418 | pub fn js_serialize_data( 419 | cache: &Cache, 420 | origin: JsPoint, 421 | height: f64, 422 | radius: f64, 423 | distance: f64, 424 | density: f64, 425 | angle: f64, 426 | rotation: f64, 427 | polygon_type: &str, 428 | ) -> String { 429 | let polygon_type = PolygonType::from(polygon_type); 430 | let data = RaycastingCall { 431 | walls: cache.walls.clone(), 432 | roofs: cache.tiles.occluded.clone(), 433 | origin: Point::from(&origin.into()), 434 | height, 435 | radius, 436 | distance, 437 | density, 438 | angle, 439 | rotation, 440 | polygon_type, 441 | }; 442 | serialize_ascii85(data) 443 | } 444 | 445 | #[wasm_bindgen(js_name=deserializeData)] 446 | #[allow(dead_code)] 447 | pub fn js_deserialize_data(str: &str) -> Object { 448 | let data = deserialize_ascii85::(str); 449 | data.into() 450 | } 451 | 452 | #[wasm_bindgen(js_name=generateTest)] 453 | #[allow(dead_code)] 454 | pub fn js_generate_test(str: &str) -> String { 455 | let data = deserialize_ascii85::(str); 456 | let cache = Cache::build( 457 | data.walls.clone(), 458 | TileCache::from_roofs(data.roofs.clone()), 459 | ); 460 | let (los, fov) = compute_polygon( 461 | &cache, 462 | data.origin, 463 | data.height, 464 | data.radius, 465 | data.distance, 466 | data.density, 467 | VisionAngle::from_rotation_and_angle(data.rotation, data.angle, data.origin), 468 | data.polygon_type, 469 | None, 470 | ); 471 | serialize_ascii85(TestCase { 472 | call: data, 473 | los, 474 | fov, 475 | }) 476 | } 477 | --------------------------------------------------------------------------------