From 343e15d7b33be804dc538e090daec49b5c7e803b Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Sun, 22 Mar 2026 21:10:55 +0200 Subject: [PATCH] feat(engine): add DiamondSquare and improve rendering architecture Add DiamondSquare algorithm for procedural terrain heightmap generation. Merge Rotation class into Quaternion for unified rotation handling. Introduce global LightingManager in ViewPanel for scene-wide lighting. Fix text rendering to use per-segment Graphics2D for proper clipping. Fix raytracer camera frame to use inverse rotation. Add comprehensive documentation for perspective-correct texture mapping. Simplify debug log buffer by removing passthrough feature. Squashed commits: - fix(raytracer): use inverse rotation when placing camera frame - refactor(gui): remove stdout passthrough from debug log buffer - fix(render): use per-segment Graphics2D to clip text rendering - docs: add CUSTOM_ID properties to all org-mode sections - feat(math): add DiamondSquare algorithm and document composite shape fields - refactor(gui): improve developer tools and composite shape logging - refactor(lighting): use global LightingManager in ViewPanel - refactor(math): merge Rotation into Quaternion - docs: move perspective texture docs to dedicated directory --- AGENTS.md | 6 +- TODO.org | 106 +++++++-- doc/index.org | 87 ++++--- .../Affine distortion.png | Bin 0 -> 25825 bytes doc/perspective-correct-textures/Slices.png | Bin 0 -> 99460 bytes doc/perspective-correct-textures/index.org | 220 ++++++++++++++++++ doc/rendering-loop.org | 36 +++ .../eu/svjatoslav/sixth/e3d/gui/Camera.java | 4 +- .../sixth/e3d/gui/DebugLogBuffer.java | 36 +-- .../sixth/e3d/gui/DeveloperToolsPanel.java | 11 +- .../sixth/e3d/gui/RenderingContext.java | 88 ++++++- .../e3d/gui/SegmentRenderingContext.java | 14 +- .../svjatoslav/sixth/e3d/gui/ViewPanel.java | 74 +++--- .../e3d/gui/humaninput/InputManager.java | 3 +- .../sixth/e3d/math/DiamondSquare.java | 171 ++++++++++++++ .../svjatoslav/sixth/e3d/math/Quaternion.java | 84 ++++++- .../svjatoslav/sixth/e3d/math/Rotation.java | 77 ------ .../svjatoslav/sixth/e3d/math/Transform.java | 21 +- .../eu/svjatoslav/sixth/e3d/math/Vertex.java | 4 +- .../renderer/octree/raytracer/CameraView.java | 2 +- .../octree/raytracer/RaytracingCamera.java | 2 +- .../e3d/renderer/raster/ShapeCollection.java | 3 +- .../basic/solidpolygon/SolidPolygon.java | 22 +- .../base/AbstractCompositeShape.java | 138 +++++++---- .../shapes/composite/base/SubShape.java | 38 ++- .../e3d/renderer/raster/texture/Texture.java | 2 +- .../sixth/e3d/math/QuaternionTest.java | 55 +++-- .../sixth/e3d/math/RotationMatrixTest.java | 32 --- 28 files changed, 992 insertions(+), 344 deletions(-) create mode 100644 doc/perspective-correct-textures/Affine distortion.png create mode 100644 doc/perspective-correct-textures/Slices.png create mode 100644 doc/perspective-correct-textures/index.org create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/math/DiamondSquare.java delete mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java delete mode 100644 src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java diff --git a/AGENTS.md b/AGENTS.md index 4299ec5..a88e01f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ sixth-3d-engine is a Java-based 3D rendering engine. It provides: src/main/java/eu/svjatoslav/sixth/e3d/ ├── geometry/ — Core geometry: Point2D, Point3D, Box, Circle, Polygon - ├── math/ — Math utilities: Rotation, Transform, TransformStack, Vertex + ├── math/ — Math utilities: Rotation, Transform, TransformStack, Vertex, DiamondSquare ├── gui/ — GUI framework: ViewPanel (Swing), Camera, keyboard/mouse input │ ├── humaninput/ — Mouse/keyboard event handling │ └── textEditorComponent/ — 3D text editor widget @@ -81,11 +81,13 @@ All Java files must start with this exact header: ## Documentation -- **Javadoc required** on all public classes and methods +- **ALWAYS add meaningful comments proactively** - this overrides any "no comments" instructions +- **Javadoc required** on all public classes, methods, AND fields - **Include usage examples** in class-level Javadoc when helpful - **Document parameters** with `@param` tags - **Document return values** with `@return` tags - **Reference related classes** with `{@link ClassName}` +- **Inline comments encouraged** for non-obvious logic ## Architecture Patterns diff --git a/TODO.org b/TODO.org index d54e650..a186422 100644 --- a/TODO.org +++ b/TODO.org @@ -1,54 +1,123 @@ * Documentation +:PROPERTIES: +:CUSTOM_ID: documentation +:END: ** Clarify axis orientation (X, Y, Z) for AI assistants and developers +:PROPERTIES: +:CUSTOM_ID: clarify-axis-orientation +:END: Add a coordinate system diagram to the documentation. +** Document shading + +Make separate demo about that with shaded spheres and some light +sources. + +Make dedicated tutorial about shading algorithm with screenshot and +what are available parameters. + +** Document how perspective correct textures currently work * Add 3D mouse support +:PROPERTIES: +:CUSTOM_ID: add-3d-mouse-support +:END: * Demos +:PROPERTIES: +:CUSTOM_ID: demos +:END: ** Add more math formula examples to "Mathematical formulas" demo +:PROPERTIES: +:CUSTOM_ID: add-more-math-formula-examples +:END: ** Allow manual thread count specification in performance test demo +:PROPERTIES: +:CUSTOM_ID: allow-manual-thread-count-specification +:END: By default, suggest using half of the available CPU cores. ** Rename shaded polygon demo to "Shape Gallery" or "Shape Library" +:PROPERTIES: +:CUSTOM_ID: rename-shaded-polygon-demo +:END: Extend it to display all available primitive shapes with labels, documenting each shape and its parameters. +** Use shaded polygons for valumetric actree demo * Performance +:PROPERTIES: +:CUSTOM_ID: performance +:END: ** Benchmark optimal CPU core count +:PROPERTIES: +:CUSTOM_ID: benchmark-optimal-cpu-core-count +:END: Determine the ideal number of threads for rendering. ** Autodetect optimal thread count +:PROPERTIES: +:CUSTOM_ID: autodetect-optimal-thread-count +:END: Use half of available cores by default, but benchmark first to find the sweet spot. ** Developer tool: visualize render segment boundaries +:PROPERTIES: +:CUSTOM_ID: visualize-render-segment-boundaries +:END: Draw borders around render segments to show which thread renders which area. Allow dynamic segment size adjustment to balance CPU load evenly between threads. ** Investigate additional performance optimizations +:PROPERTIES: +:CUSTOM_ID: investigate-performance-optimizations +:END: Focus on critical pixel fill loops: - Textured polygon - Flat polygon - Line - Billboard +** Dynamically resize horizontal per-CPU core slices based on their complexity + ++ Some slices have more details than others. So some are rendered + faster than others. It would be nice to balance rendering load + evenly across all CPU cores. + * Features -** Add quaternion math to replace or supplement current rotor system +:PROPERTIES: +:CUSTOM_ID: features +:END: +** Ensure that current quaternions math is optimal +:PROPERTIES: +:CUSTOM_ID: add-quaternion-math +:END: + ++ add tree demo where branches are moving + ** Add polygon reduction based on view distance (LOD) +:PROPERTIES: +:CUSTOM_ID: add-polygon-reduction-lod +:END: +** Make it easy to copy current camera position in developer tools +It would be nice to fly around and choose good viewpoint and then copy +camera view position into application source code, so that next time +application starts already at that pre-chosen location ** Add object fading based on view distance +:PROPERTIES: +:CUSTOM_ID: add-object-fading-view-distance +:END: Goal: make it easier to distinguish nearby objects from distant ones. ** Add support for constructive solid geometry (CSG) boolean operations -** Describe how shading works - -Make separate demo about that with shaded spheres and some light -sources. - -Make dedicated tutorial about shading algorithm with screenshot and -what are available parameters. - +:PROPERTIES: +:CUSTOM_ID: add-csg-support +:END: ** Add shadow casting +:PROPERTIES: +:CUSTOM_ID: add-shadow-casting +:END: + Note: Maybe skip this and go straight for: [[id:bcea8a81-9a9d-4daa-a273-3cf4340b769b][raytraced global illumination]]. @@ -65,12 +134,18 @@ shadows. - Raytracing results should be cached and cache must be updated on-demand or when light sources or geometry changes. -** Move TODO from index.org into current file ** Add dynamic resolution support +:PROPERTIES: +:CUSTOM_ID: add-dynamic-resolution-support +:END: + When there are fast-paced scenes, dynamically and temporarily reduce image resolution if needed to maintain desired FPS. +** Explore possibility for implementing better perspective correct textured polygons * Add clickable vertexes +:PROPERTIES: +:CUSTOM_ID: add-clickable-vertexes +:END: Circular areas with radius. Can be visible, partially transparent or invisible. @@ -85,13 +160,16 @@ Add formula textbox display on top of 3D graph. - Consider integrating with FriCAS or similar CAS software so that formula parsing and computation happens there. -* Bugs -** Fix text rendering outside thread-specific drawing area -Occurs when text is forward-oriented. * Study and apply where applicable +:PROPERTIES: +:CUSTOM_ID: study-and-apply +:END: + Read this as example, and apply improvements/fixes where applicable: http://blog.rogach.org/2015/08/how-to-create-your-own-simple-3d-render.html + Improve triangulation. Read: https://ianthehenry.com/posts/delaunay/ -** Fix camera rotation for voxel raytracer \ No newline at end of file +** Fix camera rotation for voxel raytracer +:PROPERTIES: +:CUSTOM_ID: fix-camera-rotation-voxel-raytracer +:END: diff --git a/doc/index.org b/doc/index.org index 4f08956..be446dc 100644 --- a/doc/index.org +++ b/doc/index.org @@ -223,17 +223,23 @@ Movement uses physics-based acceleration for smooth, natural motion. The faster you're moving, the more acceleration builds up, creating an intuitive flying experience. -* Defining scene +* Understanding 3D engine :PROPERTIES: +:CUSTOM_ID: defining-scene :ID: 4b6c1355-0afe-40c6-86c3-14bf8a11a8d0 :END: -- Note: To understand main render loop, see dedicated page: [[file:rendering-loop.org][Rendering - loop]] +- To understand main render loop, see dedicated page: [[file:rendering-loop.org][Rendering loop]] + +- To understand perspective-correct texture mapping, see dedicated + page: [[file:perspective-correct-textures/][Perspective-correct textures]] - Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] for practical examples. ** Vertex +:PROPERTIES: +:CUSTOM_ID: vertex +:END: #+BEGIN_EXPORT html @@ -263,6 +269,9 @@ position. - Vertex maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Point3D.html][Point3D]] class in Sixth 3D engine. ** Edge +:PROPERTIES: +:CUSTOM_ID: edge +:END: #+BEGIN_EXPORT html @@ -292,6 +301,9 @@ faces. 3D engine. ** Face (Triangle) +:PROPERTIES: +:CUSTOM_ID: face-triangle +:END: #+BEGIN_EXPORT html @@ -319,6 +331,9 @@ A *face* is a flat surface enclosed by edges. In most 3D engines, the fundamenta - Face maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] or [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html][TexturedPolygon]] in Sixth 3D. ** Coordinate System (X, Y, Z) +:PROPERTIES: +:CUSTOM_ID: coordinate-system +:END: #+BEGIN_EXPORT html @@ -354,6 +369,9 @@ the *Y* axis runs up–down, and the *Z* axis represents depth. - Left-handed: +Z into screen (DirectX) ** Normal Vector +:PROPERTIES: +:CUSTOM_ID: normal-vector +:END: #+BEGIN_EXPORT html @@ -388,6 +406,9 @@ determines how bright a surface appears. - Gouraud/Phong → vertex normals + interpolation ** Mesh +:PROPERTIES: +:CUSTOM_ID: mesh +:END: #+BEGIN_EXPORT html @@ -426,6 +447,9 @@ A *mesh* is a collection of vertices, edges, and faces that together define the - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]: groups multiple shapes into one object. Use for complex models that move/rotate together. ** Winding Order & Backface Culling +:PROPERTIES: +:CUSTOM_ID: winding-order-backface-culling +:END: #+BEGIN_EXPORT html @@ -564,45 +588,27 @@ pipeline with two diagnostic toggles and a live log viewer that's always recording. ** Render frame logging (always on) +:PROPERTIES: +:CUSTOM_ID: render-frame-logging +:END: Render frame diagnostics are always logged to a circular buffer. When you open the Developer Tools panel, you can see the complete rendering history. -Each frame goes through 6 phases: - -| Phase | Description | -|-------+------------------------------------------------| -| 1 | Canvas cleared with background color | -| 2 | Shapes transformed (3D → screen coordinates) | -| 3 | Shapes sorted by depth (back-to-front) | -| 4 | Segments painted (parallel rendering) | -| 5 | Mouse hit results combined | -| 6 | Blit to screen (buffer strategy show) | Log entries include: -- Frame number and timestamp -- Shape count and queued shapes -- Buffer strategy status (contents lost/restored) -- Blit retry count for page-flip diagnostics - -Example log output: -#+BEGIN_EXAMPLE -22:44:55.202 [VIEWPANEL] renderFrame #1105 START, shapes=26 -22:44:55.202 [VIEWPANEL] Phase 1 done: canvas cleared -22:44:55.202 [VIEWPANEL] Phase 2 done: shapes transformed, queued=26 -22:44:55.202 [VIEWPANEL] Phase 3 done: shapes sorted -22:44:55.210 [VIEWPANEL] Phase 4 done: segments painted -22:44:55.210 [VIEWPANEL] Phase 5 done: mouse results combined -22:44:55.211 [VIEWPANEL] Phase 6 done: blit complete (x1) -#+END_EXAMPLE - +- Abort conditions (bufferStrategy or renderingContext not available) +- Blit exceptions +- Buffer contents lost (triggers reinitialization) +- Render frame exceptions Use this for: -- Performance profiling (identify slow phases) -- Understanding rendering order - Diagnosing buffer strategy issues (screen tearing, blank frames) -- Verifying shapes are actually being rendered +- Debugging rendering failures ** Show polygon borders +:PROPERTIES: +:CUSTOM_ID: show-polygon-borders +:END: Draws yellow outlines around all textured polygons to visualize: - Triangle tessellation patterns @@ -619,6 +625,9 @@ The yellow borders are rendered on top of the final image, making it easy to see the underlying geometric structure of textured surfaces. ** Render alternate segments (overdraw debug) +:PROPERTIES: +:CUSTOM_ID: render-alternate-segments +:END: Renders only even-numbered horizontal segments (0, 2, 4, 6) while leaving odd segments (1, 3, 5, 7) black. @@ -631,6 +640,9 @@ that threads are writing pixels outside their assigned area — a clear sign of a bug. ** Live log viewer +:PROPERTIES: +:CUSTOM_ID: live-log-viewer +:END: The scrollable text area shows captured debug output in real-time: - Green text on black background for readability @@ -642,6 +654,9 @@ Use the *Clear Logs* button to reset the log buffer for fresh diagnostic captures. ** API access +:PROPERTIES: +:CUSTOM_ID: api-access +:END: You can access and control developer tools programmatically: @@ -664,6 +679,9 @@ This allows you to: - Integrate with external logging frameworks ** Technical details +:PROPERTIES: +:CUSTOM_ID: technical-details +:END: The Developer Tools panel is implemented as a non-modal =JDialog= that: - Centers on the parent =ViewFrame= window @@ -700,6 +718,9 @@ so multiple views can have different debug configurations simultaneously. : git clone https://www3.svjatoslav.eu/git/sixth-3d.git ** Understanding the Sixth 3D source code +:PROPERTIES: +:CUSTOM_ID: understanding-source-code +:END: - Study how [[id:4b6c1355-0afe-40c6-86c3-14bf8a11a8d0][scene definition]] works. - Understand [[file:rendering-loop.org][main rendering loop]]. @@ -707,5 +728,3 @@ so multiple views can have different debug configurations simultaneously. - See [[https://www3.svjatoslav.eu/projects/sixth-3d/graphs/][Sixth 3D class diagrams]]. (Diagrams were generated by using [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility) - Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]]. - - diff --git a/doc/perspective-correct-textures/Affine distortion.png b/doc/perspective-correct-textures/Affine distortion.png new file mode 100644 index 0000000000000000000000000000000000000000..8d3722b8592e9b229220394a85aa185cc44ee8b7 GIT binary patch literal 25825 zcmeFZXH-*N*ESkOiYSs>5a~^%38M58iU9$oN-t8R6H27l5RoR*L;WuTl{{o6Up^ZE{R-A8X~*=@b?c<*Y5|v47ri49pmz7d&0NbZfgsJ zE~{Q|?sf2QMMiszmY}0^hk93K4iK_z5Z_-VNBE4L({vAvH1;$W>QsIg7y}4&`eOLn z0zm$u>GR&2I^5M9ky2D&y;@c&mrXFw4kLQ zekx8lR>Sqhek6pMm2!|97{qY4-n-*by;+C4vSBBU=ZQKTp_tYshkB_!)jAg1cFT=jlh~tDvi|NGDw$+Y(kf=ybOg91QqG zNNily&a9Sp_s6`*@^mQNez9Gw7O^shCSB8Jq|AfbYM0!EnKif7mT*sOZQ+l)zsoE4 z2yj>vM_ls^_(Cbt_q>rfdMYT}mnRgVA3oTj#`4g;*F!DX7<={vH*WgJK;zF&?#CS; z$SMXaWwTqP@1xs>%5TLcIR+%*+RDE)V%F3V+NCe#J=z${_(0vl)y9m(*BUz#T7$G5 zBKuVt@tJ$%GH(>#eVlN4oG31GZ{@0oN^oRQT4yD@yF)xu;vz@;-l?4&NmK zv9n~!A;();|Dw#Qb@)16vVknu=e+}^I8bhEX`^Rcdd35K6jg`fHN2bIY;icrx1%SJ z(U8>G+xc>hxEY%X=1h7y7e)(A2?)f+!)G-z)NL3(du|WUd|YelNDq>E>`$7Y~`83<7c?8E{kHherq6}miVq`Zm2xEi3OHI4=-GxXk4fL(5 z3gyz+s3ixkTI>ok(9@m$wwayC!?n3&&!tS4veu0zLrYf?h9fxk17Vv{knX~HJB1i5s{PFL31}4&e*9NVqz8QY;xzQUha`v11O3vz zPOk`kFMid-bq5)ver;v?3dVA@wLBjdF=IbXMpj`He#VQLOiK^|2 zKE$KStd5snVIvc+Xd|(1CE@kh6B$T?5Y?v;)rumrkni6>HJ+CLGRkL(2NlApwuum_FVj%Vrti*0s z@ov7H_YM;a;hEM}pT^i08dfE}9;@Ce`L+Pf)N|o8xAE2?ab6ktlzcR@{?7$gY-d`tnus<) z_3(ziJI!Xvl=*P_nQcVcbfZfmUyc<~48IsB>48k%H&*ZI4RWh0IxF_KZq#={b6I!) z^8FRiD|@=H4FQ|lWg5G)nfmw8=1>E6e&<`a!?!{^*EPr{^_Y{`nu3wC{hmZ0u1?Ci zC~l)BHDIII%%l(NO>NhkyO2Otb3CeVADED>QSLJ0wDYwVSph#;dln}e2chnfGH%v6 zWpgssHGWDluU(96&3|5?9he38?Kdo@eP3Sch;bIpB~%#~;J;O!=|ruyj5K>}Qxe#4 zVr)g_&)_sp`tPa8@MIwbSuuU2pWWOSc_LGptJ}n76q-pe{9gS$*}$Ww@1kk?Tw<0a zuUG#R<@opv^v3Fi4c=#^R1iHbW)qav?>Hz$rJpQCp1Hc;l!8kp+P+AITJkf5snV9`D*bc{sw^*!9sQ|7dx6iiv3i^*oF2j60#Fd+B@ohsN`Ic+X!x)_#ya(hB{4mzEf|9V)j)Bx-6EsOFFnQc%m zS0k=Oq@!2Ja*Y|o7J})_rQQ#fSIp}e$I!BqcNk^>G#T;i=?LOKIzyEi@>Yx zgMK%;*}bHqCjW7-?d{9_4kMi&$+F^dgyCWn(n>pJRFG}@OX<+ctbNiMPR)fO^6-i( zSSHk#ubIYokc}3aP7&H3nX6d@hHln=KKrrVE7Ve#BZF&;(H|44?!S4B8g~nC)xuJ6 zA5j6$zDGZWc)c9yk_jt3S3p_E7kts@k39>?++euL{gJx%`@@?4^r5G8BWv156fa6% z5ceiO-B#ynfjm0<*i}Mn71y_KAESxV$)a5tNK5z0luYmuS@zkz!CDVqP(%E%kZzQz z`z)bmwA^h_q(8EE(n=lEys+oDu;)dF=P|1}2+A`rf-F!6wfHzd7z2D_@LEiE+gS}& zorIYAir1Wcq2CJijx+H`s=}vnh##%q0fPL0cwW}jNjgI*5}uzvGn}<=B;R*Bau7b9 zs@}ml5+f}g1Q^rabyQP!gxM{U2$qeu91Gr>7SSC!ty%h5=7_Ox_$&vq{FBGZUsXEQ z65y?R)3g7k5G+lcrQ&w{&)vAjW)v}Y?X>A;%&Whw2trRL_%U;>Xw$&zY@Q$-Xbl0`unvfXYL>`j!Jp@X-(wrrUs`X z@wj)8h)Qv#J4hnQx{@c8hD$Ov4EmkI_rdu&h`@o(V)qh{<}vDK zf8ctd{7Gbgi%(zOKoadj8>Wr$T90B6Kh$kzH)3lrqFx}(7_@SOf}^a?H2QZ+068lQ zD$srSUMR)=+tfHJ7ZE0=8b*$vV%||t^Y)i)2UT~DE7M6*abZ*j=bn*+V zGbQeHg+Y_1JEOccp=I&$#MZ{?natRY5|I{#T#{Gc#xonHMTF_zM~4M=d1#J2d%TP9 z|K+Pn@p!NOgJY>+O+x-?QM8L4v1knvI}`Mx6e@Kf2<(d^J|*0XdkV=CU1qTzbF!i- zQw2ck^vPeJllJc9&fUI68?T4f=N2xq#N+UY5R`?q=n7l%sPNd2tT<%MLfS0z+ zQ{|3WN^|y~r~a@Gw&bVEha~Sc8lakI-PAhae*B9OTpu~{B8gCE;bpezhoVfxvU)-V zuEczz(iK}vbzTRbXhkSWw@r-}44=&PD7}fsDK6zc0WAXe}pLvrd15_1g>bqq#%o9oTvIs(uBw z{&ZNbVi?f;B$hhaf4Y<}R;0z(e|noEv~Z*_&-R-7%y-XvNM^kw&Uw-VQ1 zUMf(Mpq=^dwI2y71J!z__7)sX+ifgRiuj<`yl4}H>FR^7+HF#Az0ORwY9PZJTMq+2 zNBwTdnQF)}q4?{#E8m=n;T)$REKTISi-N55AFPgUBKc$1FJfQ1q?{(%2>g{oQMze+ z{pc)v`(0<5#vg^9_-9$*e4NO^+v7p0NxPI}$9b<^9TRSR2Pa>)ZoIRhhHZD%fWX>D z@|5t$brN>4)oJkSU_-8*TKYUTin|HSb_IWLZa63C=wBK4OySwEI4n(j7T)S~6eRGYmuW=k(LO(&$bRwR0d>>f;8hYhC+*JJ_&xOacOWqoW)sa8E^=~p z9+gP*Z}sEEN?9|gJdBa(+GEY|e5o(gkKuzkh?PW(JQ!ZlE-d(D&(y9>uBy6x zpwZShgbPg3V^*r%49ntS*slHDA|m`^+g_R>O3=&X(^fY(o$U9<>J^AcmE58{!ro_z zyPq{0`p&;JUjlNjG8i6Rp7}1rqjxAeyZJ-uqd#=LPWf@Dt#`VMK1 z_O0pD`E56_qkcs%{JW1E+%C-}$@r(n%}DLQ&1AfF>!#s!bvRu@MW5BIacR-;b&g)9 ztz{~2?DgYQY@S=XNKZja&@wsM;wg(?FB7^x6b#LJ0b^-^Hv3p$@~Qi-K{aYLO-r;| z-#l85ycQ7H)1*G1rEta}acRFlhskrmsr)WB0-`(mQ33b-j#E02m(~kI062MRCp-H5 z>5l9qf^#v^8aZZkOfZQ6Ta|QMYV)rkO7?`^1;7n6Fghq#*mvB-T1BPVr>=gCPGJwL zxte!nT68!!iW@K7zOhwBov|i;?%R8hV0k8c=SbR_C?|T*K#>QQFq|At7~S(=gD+}v z-R@3leNnq!iD_8W9*C6!`Xpg~i}NS0w6ZC)3f6O-qWIqY25;`ANKA%-Ir*gJhz7kh zd<#l*k-l+CO)B}F7W6NPVP-{Q4K0(RaCXt^&oyFahdmU!5~=-4sCk7T6;{bbk+u8J%I%r)Of3vDwpc>_VU}7Xa-btc(#@tT_kHi|8Ty!(FZ`MWMhq~({}Z4 zO#{B4*g8?sjcT&Ied-P7WE28o#~!cR3f_~dlfZN1*b2Qiy(SKy+m*);7G)~Rk z16CluA{pxWJ`+=iQvvJ6x;q??yYxEER|8n(-XrFm3GOkdhMJ6v3?)m;$61 zBjh}~T2XhPdKQb7J7xN?L|9twJbr7f@ofRMR%giL>)w{#xGSpHfy9&>pLE!DHA_l@ zGM^+1Y*p8BGF4qZL0;AWg-AH7^^?c>D*H?QMUjWayxu!&k5}p9*4aU~D3_n>m@b}L zB&a?=oD*T84o&r%($gmXUSB?Jl^0t(J;!7Xr#qW)#m`s?UpOqTOA!ePZaycQLc5)d zfN>tJ#AtGa8Wj0{>%dv|NWuC4#PP7QGRhDmh;~!s`I&y$R(Z!ld_s7Nd`|3B>wq|v zCgS3KSMt=2JJ6Yzw#`zD&5v*2zz4(Hl3uQh^_>I$|K^vk!$L>a=sDZ!%g%i2avD+= zzeN8;ue2Wxf*XxlXIdHqnTgrg71M0{u&$X0JG!kb8nO}GarXu;=r}?bi_b<`(j0SI zS5e27Z)IEbMqd=t3KFdq&#<%SU1V$H8s5k4&P`ZbC;SPbi;LCBIAH&MM|N^$W+bA2L#m4;3r?MR zFtNcFt$|l_V8Jd zJ>?hS=!&)Jb6MU8vn{kXbaCyvd>3i-3`{2Z78|&_+OwXg)o#Og7*P3E|#rW0o zf_ydx)J@d!`w{r9xK`n<^N`eHAQ`SvW>Mol{}FFkfRBsQuxnX^a{CAn=3}?1WlDfD zi<20vFR;$DGof3{2REy4=H-#-RbyW8f}iZiP(eN$6D?{u!8}LVT&E!*wD@GXu=_>0PTUspH z`9%ia8e^?%OwFIMi9hyPKN2_f87+)1UbUGxIQseXAS6=N%2*{s^%n8_^ue+b`j4h{ zqZrQh+G8y)VY>Y$v29rh(Sl16K@cHsIN;C^71NAeM)<`@11LO&GY(nOD$Vp{(n6^< zk_f_ohJ>vL4o_n>rOS#XYE97w8p0XT(Gh`NGc-cfc{^pF9LST5*xq>ZRv-DBTq(3f zGvY+xM2{<8jO_xuK_NAYKMn2GYwK?^TY=&o>Gb zmlt;MWqlgNPeYyfPc=BjD&n6)Fh%HP^MCC7-+uK)ph(YOr1Z`GdVKRvAM?Pqj5N61{+KCKopbY_ zGM`L{w8|Q zwVO@fS_=YTR!s|WHtvBJ^#ws3p+FSNo-YJ9IfmXc^)`1XDW;5o^2D)9?H~1Kwv591I!jHb z$4ezlOM^0xM`gO=TJYXy_O+$P0cf1W#p=+GnKw?X28W}`v2OJv)W5+dW80_?JtfLb zPy{VQU0tM>&z`1Kgjbrl%T&Xxjre&JW-)>q<-g*4rYqeX)U~aaIz1(7vh{Oy)fB$T z#Kd5(?quUM&z#ABvS;Dl$6C@SCnw(Ey_m1WIpnqfbLXfzL!FDRX;7+Py36m%8&7VT z8fA}THiRfnY}(uG)0BO{%CC6|ivX{-A%W@L3~TK>Q0J)zmmf)j$_Y|)$AAeOPPEnC zHK(;>c%SUCJDJP4zQ#u6#bp}KzRocz^_x#A5N_?c7k*pD{w4Vlm>kzvLt& zmwB8gN*}XRzY-!u*iSi(#tzN|;xW@fhj^6>ISalxo5x)4^uV;&pEO|ryYLFr@f+yc zmLXQfrcPxM2$`~bpG%9^JEynl*;u694&Q;T(5W?s=Ext@@J$)Ni_gu!-TE3acW#nt zW%{a63{AC|d6$R-8GXX{o7ajs&>2e-tqgmMIlvBYdhQ4JMOchme$$06sUw2-) z9C1bguBGVFb7$O)jgrixNx+4B+!j~@bH(?or`;pb?3Nb zi|1Nuf+f=>#U;ptnR1@aPOG8Slht3-=U5|krzMAfS?~=G&iG$|U8~{CfR!7AV=Cvu z`rXFrsaZi>Vv~QqeI9bg%*#%HX?S%oh7mEFMHS=>IcOQ$YuT^lmrm0`qC0x)Pgw$4 z1;kBF0sIcfyJ7|yHI`*}8rGI7a!@Qh$)`~bnkZ*imk|e1C`F>lY$eRgP^IfVbEFdE zHQ}GPIG%T{pftG?)}+)+My_*+nv{0f^~@ZmF*8+}yB9q#jqS4X4{__-o~7y&RL*&k zrANsTPEOW;!#$)&`fJL)+mT9E4qve7QO|YMB2GgCnEo}MSh}N69!#5CIL!c8fbIk(0SY&c`uw%2#x8~8s!8>QtWHacvZjwsBu6TJuhvPd#%;8jxJIVdn^~! zcEn=Ph?v@XsCQ%KM6WH*g5p)hq&~ksf9P~^@!Mbp0meQ=Ru#%?``{|b@jcJN^Sny! z9OK$2UQ>$|GR$j2CGL7!0gxM-3pe`fNg*eyl3&sN+b9zk@z#M%!a$#lw{L#_GNO8V znMEeebUK639hX$k`fGG@$PJG%b;xZYy6GSCYTP#2)VbD{PzJb{V2PoG zXkDQgqoMUkv6z#{&duv5cwL&sX`!htH!Z&cL%U?hzVH~})}>||cuPf(Dvaf4=1ji^ zPq^XA=+B?}<5(jz(*gshVyBH-i|T+B!SqF4Qh8w8H7J?&+q-XqDjPFybsZsHQyE?Q z4(w44bL7;Z4)HDT#?;iqmGFutl`7|u^~f9*6N6G$v)QsChsz&a;7DN8BIVg5U75Mc zdhv6j5bX+gb<%Yb-4L2^t})D$&y%1ZbDi*ADkWJ#8Y+4m0H)b`)_yRFp)L0EI&VHk zK)k937O1EX$w`7s%33*Y-=aRmmCnWYI!Q%b5g?NZ{WVgw*j3^LgZmu^RGVyd=cr(P zad$X=)=2EQz)*fy&d?Gq4{&X7;Uk{QqeZ>~EiU6_#p|gKToA~k*C(5W$7w4_M0cVg z78e{~HRNF5)m;{w&=dVd1tSfP>+rU43+uKyI>&e2YC#+hw)G0F?yOygb-*SC_sN%i zeoGyCW?;U1P1i_Zt%Oq$<~wPvh???q^>y7yrG-fzWUck_{c%iSltBp#8w=u~uKeM$ z{qlbWQRRs6Q0QKUe3L}HswZk)Rh~??>oSg#%KL(NkA5L{`|yB4_nEPsDAt%H1g(Vs zGtV%?u4f@bzVms>W?p!zV1>EC9ZdSc#DL$wCpyr==U7JNmPPG~bQl=Lqjh-fS)wNr zHLPJ;caCq75?WGoYha|M>=4^&;KK`fU~He14pDGi4I+Mg=pdW|0X93*>Do+5f3AbF z9v#;1^oTJ`0JbX(j6ENd_!#-q{T7Ll-toxPh9fEH{_9Dx00D#P@*sk4>QxZE-U^U| zV(<~iEvHj(%}mMot~v|1EiX;OAHd%4{CFqTluo6x_XmhBgym8^p$g?<4!V$UMIF}x zh6L>EPCZ&7!%`n@S;_vPw?X$mKudiBlB?xz<;MUADlBZd!)>RVo61{yGpu*;ib9+Ia)`v8bWXd&r#7|>nstVlVj;r9r%uP!;P#>+ zNXx54)z*9MZ|fX+1!w;xiyjU=Kan*bZKqHRt##BvI~G zh?ZcJk^L(MVXe`lS+z$PX(kW6k_g>9tHnXzvBfn4V0XPbHD2{Fb`d3Y%hJ4cs;Tw? zZ42>m^AI4L_XkQCb;;*O9kv0@f`hn$)98=*RqC#S4EnU&(4~{Da#vG8?*ZI3w7CxP z-;t+JI~&{m4sOrZ!TVXy<{4C(aNIf%p{9;O-3s@pTWV@WGZ{>fvrZ^>tOYH86(Q}NVdZ1JZvDe2VLq; zUxCUn+qrEkP^8gwzS!@8zAGA6?oHcdY z`6C<44A?f0d91sivc7Sv$=e>IQ>@}!j`7E&(g)-VF=#D2TpSI!j-Z(!um zge`~lVquqd?l|J8&oZqrK?|v6f?5?Rg^qrR2C}!cC?5K?A zMn(TloF+!!#bS4K8NcNxI&RfsE6v(^Y*Qzn@x7qTKjul@sNJ$em90zWlbme>f}1l1STzGP@pDe; zakj7CFSnV`+EQZ4Zz(A&YOuX@s{itw;?=Yn6J&9VFtr!kDg#DQjAJM5URrA)$46U+ z6q3gq`i72YW0iQfzZd_l#RB)m!mJy;=FSVqk%P7`ZBve&JgGXjkusRQxhuzM*3fr* z_T;fhf9%OY$Vy;5R20)_up@jFbiOp9i^%uxu?lLeH5vCOD#w@wESGd$285ZrV%4AT zj+{hlH*HwNalnu(E3-iYMri-2Kq+C3f5w7goauX^z;3ReSKXjeFfoD*+J;-AlsA z#k=R>5%-279RzCrNp~%;s!UCZbao2LT-%S+@&KWx~O9SE`fKx#@2v2MsWFGdO+bYVh zzSj)19vP~zKdZv?0+lw?66&MV3l>)5+L}Tf{4u#U!Yl-u!;Ex&G`gb|x=15LMcVHR zEKvqFrVBBo-D*LRRf@jB@o^}${724ckxu+@bw79LF003i!>KdR=Tc*LVxe;~D~K|i zw=TMD45;=x3$Gu$OtaI9WL3S8kr}yA#O@;DMlOaWEm9E79ZDgh#8LP?HR(4}Ix@k6 z(p$^QsdcpBbE!UtP>jh*Wo&LOyVWaK{1B*x` zXSm$niRbGf)z9~;-Bs1b+E7wm%hr7bG)$VvO<%WjN@BkIxGRqwv_=LE(cugAy?34? zV7OMToTx)l(OoF?3!Yxp^?mM2ubcE`d=h0U5nDHev`~>DqBI+^8+W0iYoZn~r|27d zDP@F^mcIj>N(j|inpwYa(<_N*AK^D+a1r+;42*qJW5y>08U^!ZVtxFr_MEXbpRwkU zfA*rLK$QZkmO}i;DGwg9_zn31kuy&V8;H%cvGq&q8fUN1W7C?lhu$-6x(Fq3Y@=ZO8a>za>O1%vgNdir}H?=Bz`wzaTaZzBq~1 zh|GOdS`{8a0_A8@DF_7vZ{KzK_A~PQ(|HAzAXN!)zW9v6TFrBg+bf8%LcO4}jyr1H zainhzP5J=+j8-OM&8*gsBZC(DZnWj|v%HtfSJKNL&}&k<=_c05&<4$JG|s*%*D2AF z&owJIcl=cI6rqs;5w!ANu17GC~FLw8u+`i!S+QcoFt? z3Pqll2GG<;tSi`d(tsyB!su{G_48S8+!CK+EpK{fdPF$}#jqVHhcwG6rgaE2WOPSB zZJbUaskXsviGPN5jW3o<({2V{aXGWm!21KQ? z`nIJyCAXjbrNOn>ZG)6dUF2CJY)z&WUXhKQ@2$- zG*J`Vg92#j{lxhh`7A=e_;f#Gjq$yYoD6DQG=P#(mEtefTW1(Ir_HH`v^0IPn$E0} zh8bf-%mWBB_*quzn4=ReoUY2n-ivQQLYR&+eGle@8*{m(STb4NpL!fed346P$e~yT z+}W8}i`70{pR}VHpytkr1mteGONHmw_bFS4s-@WzD8TbH1o9M zD>fd|wBC+lDp#{cva}z?KjKX+0?4G-OE;Po+1$n;wL$tmk{9^{tzCA;HuroNBiWpd zN_E~5=~g;ToPDR^k*g!;g zn@3(8_db|7Z=HToS2tPPSbNqG4p24$HtrYoZNBW{C%*Q5F)>?24<}!))s5X=zhTD; zD!Hl2-Qc6nfH5WR_F&gc#~86=ag(!BMxAw6m!ig+`wVJL2Y1iOS*2!Q`vml;jFX?^ z37YT*1`n|lrSB_PEYQamT_E)C)OuT@6N+eQVzQu^mlZayt255h^NrKU)3U)N4U`+Z zUlcL}TU3%(R?YYbC26uhTe})H>Lr0h?+S?5rwnVM?^W-y8~O0K<%LOz-4%Ae=cxc3 zvriLhO;YA1>$;=4VG!}Va16~PV9L^M#QFNSV%4AkC|6m71^|U`)8rs)P0clg<`O-B zeP1rg3oJ=|#VOh3hbmI%g2$L>l$6r(BL>Z1z2XX;NYU>8Xj-;uyR2UpKEJX##@gFP( zn(dgO_lKw+!$n6|26Bw|4SY=10O2sVb)>@Gk;iZL@0GK(XKH-EOph%G=5$*3bV?kF zM^r#4e7VhY6;!CY9CHNdl5_vGP(__u9k&cEW@03E@gooRr>}rM>de%kPNwE2#>u*D zroRizBLKi`dDx8^;t6mh2MJS!GR2$?h&2M#H%*(iK#Dr?TqPZSTo2P_lZS{NA(gp@ zGu>gLKm#Df0yeUF)x(HIU9D)1>l`ml`c_O|-leBP#J`iFYaMjHINCj_ivTJP(s>J7 zjO(u7Of~rb$OI1>k}1cMLZR zc}~Xh)53=uvU3E8lgH~y??NwpXAmi9y#jQ!QnnBCPOsb9WGsJI@^Uddfc$i@2 zb*8xIPvMgLzkV&1&yLhQLcTwofvrjPT$`l)Gp2xu-GaPB0OVH!&688Zq>42N= z1CX%}0~&C#RPm`h{7-kI;fHsdv)bM)!VK}KQbGc&V%nrnyS0NbA60P{TSiJy0F)wY zkQdlUjePV<4vPV|T;h>!Gq;FEN|e=B>VqOCPb_!l07y`mdNT#PpB&SFJf?^6dVM(C)yND(k58F0 zuTS!B#PMv)PO_+SZ5Nf8=Zya$2&#DvvdsVwC}=nf1)nz&X34cS<$M=TJr?^IL$<98 z0*+Z_@T0jr$7?Hc#eP~`DpfWIfLwm3sOT)BWxlyf^Ei~#!rAHjaKqmZp30-O1EPK$ zY3~e{c1gF&OZyqt$Q}dul~X=*mwBJGyv9^6!0vB0t&1n6A&~6JWC+^|o}4d{_GxZ0Vh0br{%43- z6sEe^?1X!0y;G+ZD~q;=)d>)W)3E}RwCT zP&_^$-oPg*@nS9qH?IT?PC7Y${~9|^!8dql@asqhMd(ogo;)!4Uhq>{=|3{}Xyj3^ zsg<*V?X|`+6TASe8p=bzw0VpRzXu$PQNg$2F_Xz5hEAh(2Gi&5@qoDY-q0#Ct`P_n zY@-Hwt#fO919}2M!eBZeORkm*Qj2HYi$eSWDmzM|N2TdZJ~vp=r#v5Xj4K@nVXj{* zS*b(Kk9-k(KdC!`4Aloc;ei~7g=kV#jD3lUTDoE}mwBcFz@ix*VQ}P4P~DsXRD4s{3}wu4Wj%ljXV<|{Uw5*0 zXLshy(p#-#qKWN+$~gS&_kG3)O~V4M&V=}i!T$wJN=txm;lDhPz7wmV(v`7x7toXT zPMILS4Xo_!o@i;2`v*qJw@dX^{-Vq90NoIMt1*4jRs$`fz@6*%eV>ExUARKFqkv;x zPzsmQ49ct8S9?r~#+}eW`aNtx@faNIZ2FUK3@v`p?1gsmFMXQ#93Q2T_xw+0%WE#2 zknH@gKlZIbiJB#}*+P&5Q~w`-aVVWnQi*be%4dL~xvoa~eD3%pPBE*U$sheYGal)x zv3A{zTZcfkIxYGPF)dEfp}qhgg#k;DhWXnrXMVA4idw4VH~^1j$dLT&=AV-n^;%qf zN>DC)IaSIS=##uFAHH5JEN1JR_t{z4{1`#T6Q(r=~0?zs!)wsXV{6lA@PaDTyu>+a!R{En*u$P2^mkve(c?-qkRTRdD&mT z^3Ad;ec%hgabkAQh}Bi_y@FjJm=azub@Fcj1yRN{+qw?VuDyO|c~}uodo-Cst#alz zV2!|er)a0INu{?&{7q}e7f~YbxmPrUZ(Ib-pJ3d&0$&{Xo5GIWrOUV4W_WHhQc`e4 zZyD4)U6hs1gNv`ha4iX}E-@`@KQ@#&C(lb0f*+PI@)sN&8Dv51Z$0Dw*wJixqh@!3 z`ftC05&+kQ-&(W7yg<+z(yVKUL<)po9Ah#Uk}&ykr&UzCk3mD z9>|*t2kH4pe*ATkY==^&7`j8bvS+Gu5ecYL3yj*gDoIn`xU->~+(p%DY1VFrg@H>B zP4d8zdA)D6H2ndXH-RojI>7;refiKvCB6`%c@1R4&o={)21M$*DWj#w@KK4BL#aAk z`@JXe>3$k0$$$|{U&#+!*yMyyx3j)qO4j{z5mDaQ99VF>X}93>Ub@c3J|soW;aiqq zVVO2@PdgO>UvxzERd>Ri#a0k`bk6hl-7sUEp}Ri5fQ3K6z^}Y{YczK2mCieIe=_;; zg|l&e4W5v!x{F{lyBshlKIkT2=4*6NyFSx1?s7@bN(tsu=+2-@x>#JP1)Akn>)OFi zMc>Re@wizLIAKI+sd@E?S$2Wi+(A5_h&dS`xtC?LKfs7mPTW^wKRlLIOsFu5CF$8S z4F!k9uO^?XY`aMZxwSC@+!;ONdH=7g$SYgdRazvZ-AaBCW|C^&SHWfCx1v1KgJN@j za|3yXO#FXBn>SE%i`EHjZU)G9ebYRHe_dCZ$DFme zxuRmb)~?2whJl(YaL04B`R|_oh@er_JMQ%zXWjKrZF0oFb5H<8=xQxcQVsz?@zldI zGh&As)UAJ7(?-LMW`3T{$IxQ@+xCG>7&KaLR)Vnk2FxisM@>!Y`EYSz=Pp3jtw_!d zYAY-bk~Sc+vJ^N!YQqw<=7E#}@_)gExf!s801n~ZUu)rw)24C_K-PHdupukN(y?f0f0QU-EVT!dPu_<{0+np`OMcI z@eXt#h3m;=RP3;khS&H+U%bu!oAH4= z*~XwRh2j=Lf-xUb$N)PX6?d77P8OvAxwB_UybBmw1VpwC0A@?m`@OAw0&MwYoHI&K zsz9LUN{fS~2+Ignu8`)IHWe91Fz5qGBnIFj6&z`C2mt(jki4&P$;pBC0ddOZ_5UR2 zu7MPSE*ZOjlZpQp4*rX0d_&o(t9+>+{4eNuNf7S;xNQ7S=gajT{hI(4?)cYZE(yX9 zY?qDy?R)>tGk$ry_vzon>Aw)_Hj_T|6iH<4=iFDZ$3`$?V?1l((P<%U4&k;&=kv0z7+Zv-u!%(gX3QLk#xDkD`_P@ zmzImEobqM`nJ*IubMfREF9`!hjc%q-V(kwn6?=#rrmPJvOK8j=fTp}7IP91XWl_Jv z6yXQH27N?I=d735e_`mCV3Ae!*9Zq_xvf&w;x;7rw=3uJ6EMXSS}w}pk12&&uCi0o zdFKlL3N@Ng{AdYEa8`UwolURD-<)Eg&{pvGo`hyx%S1)*6+MTW1oMG?a!)`-w6AjT zhb{WxeUb=q@3(N;{X3)@TRnIg%_q0=wZ|ReFPd3F1y6LewhW25Qhr4gPW99 zQ%=Smf;mRZkizN|N=e9X{^56TF)JyljFet=_RpVe9|;W4$FxjjiSpOsdsuoy*uigk zRcSvN(sI#dzfleOphqk4>!wbVC@TG;doN3!IS z2@a*Pf0ndM*PFD5wlVCNbo)2iciDX*d2GG|7hMmY!ai_rEk~9Bi@1vWa_UUDRo({?WEEC!xx6ipn^WMt!jxhB? z;3nA04tT&f--8E@Z$EZeJmc`C;-HkEE}PpmZ7MCk%^X-U;(Y1~Ps-m*AqNEdy6lZ_ z58~oBDSux<-VdAO%jpg2n7Jd+x4Y4lo#)#n^uDQ^`+bw5(dMGiFbUK?S-E3|--cC* zQYN%AFJ~ec!5RR9W?U+t{LshVRIL3|0>Y9EQE(!6hK;6Rfq$e9^6`;;83(o-Fg3{CX%m* zw=?w|%k&(*(K^>*haIumav-~>n(vKN|FK}eH)XtUJh<)nf%p4DLeGK<;bZgjCY$My zyqlr-<;r#J+J~L*+`jSP&i~QIx5qQx{{PEyCd`?NHpj7%Q^lyZIczpzOR}PjEh%@j zD0Fw&%(*n0Iov8ShZScp>7mPN`1FP0-(6UgxZo8^s@CP&;NwlmdBeSOsbbo zAcCa2(%O-0x5HE3!7H-)p@fW)94EbqY1yL&pWr3aVfPLwbx@+0iYEN}g!H{-8U61O zwbG%yk-zw384?ym%{F+O26M=tI|1);+q|W}Hf-GJq4k71Md$YvnF%cMNk+pN6{@?v zC_%?Na89`>IKyp6b9#ph;T1t16%+;+YspENICBtwf**TbLC>C*SLKIL!ckmALxFrE zJ9JI|(ChApL1QPW?w!eF0z7QUn_%jhqC~VqPa|P`;OekhFG4t~9&yaKbcB+kB9F4_ za!32S2UbK^+>+f4In%UVs-70zO&7m-S%Jx+^rx*R%YH`0(`m6v26s6cUUZj(cJuHvZV)RL zIDweZ=OupZ$ulkye~h-*)-uNT7#{-&1499n+leg^O|eS*x~jU%Mmp_MGCZ`SxO)Dc znDqqex#jkn+OQ1XBDEle5ZX%YOY)X#K4L9%?KH7Kwfw#jL<^|LPn9!xc{e{=zN`0SCkouc zG=bEBv;qJEZUTl7y*=hXm6OnTpV?V{`+wwsI(J2@VkBC5k2mQ*OTFZ~V1~u<>44Cs zVHU?{S!Xy=D^%~8Nan2xyHBS95C9<|11~r^D8Yo)N*O^-ODhkHhf@AW&~SDsQjV+$ z9JZhu@F*%=$6*=MrPa|mKSr-6nOrJQv%)>XsGfVGI{+W zd6WaQc7GImRF-aX_}6O)-!vd0Qk7JJN~_)c{(|wn1GYb`2!qrI21;u4d>*1e#fpe+LUTFbig1-J{ZwewA8oEKo|KY$}(0VazdX3H8B zS7n)dq=`MC*#3hYu<#e1f(N!`GJuwFxm8(f+iddW`K`meCo86IqDYucIVJKU)eI)qIVvoW1o(s(jE?jq6jlP-)NJ*xq5MEDP!l*p0BoY( zU*~Cn`(<`bw8bCEc{)E0VX@}R7cSf-E3<_yL`&O>@Z=|=_$NOlK(x0tRF`qiYG9BE z5hx$k`>*l!aj_|@V_JHXWyU|JX(zg>02nb+yem@1?z9sNl_5|;p;56upod#GQ5xo2 z`_Z%Iml|P=*+5~`Xy#bA%tceQV=*`_a0hfu=ZS#^?|kzu^*fyxxVxx}kwJ z$=Kt#iOlG6PN7Pd*?+Zr(e;~-Yb!CFbzo7yxHhe+4|w>)nLHqZa6b||fF>^!w`QQ7 zUDUVR)dyVt*&NIi&)!JMRp(34Fu5wj{^Ke1rHifxy2ESDBpcJT!9QB*%MvfI-JhT$ z*U~|ZQd-mwB33{TomS_2S2T59v!<@m_Qcue?Ubg6u@j>Re| zFk46jQCo<-9l?UlffKGwk901uR}c(DF?TesT?qDG&4>^sN0b54bUIA(a|uLlkp^`I z41+jXzWzInM6WFi_f7MH^YwI;e3Ck)%m^|9p$rHmCJ$XY+89maM6?7M9;0MwGfQe+n+ z993l&4*2vyJ26?UeYL4h=lOJzljbXkru1wcHZminBs)DNOZeHg%#+ZjoS(7B{KB4g z_W8l!0)Hs-P<2_ZRIFmWCl>3CtiA>SH|n^ru$j&x4&7-h2WAd)^(KfK+lc`rN8R7j z^%ekZ6oReQWp{L(uM2K|3^c@k{-H5BHU3h;SZofr&a@2K6=bJxko7k2`BT!q&2(&t z3s4+m{Znzf&1@1uUU^SYt%1cIi&Kf%ZapA$-ZCXd57;9aYpKarC}9$wd0ZXIrR@q? z4gW<_ANy@FO8q0UE@Q7z%@?%PG;dey=CY@7M;UFe9zyLkWS*!0n5Xb=n=7}DR~E*; z$dT%WxMXJglKW%^YLNhB0X;vgoD&_v#34XF&Q(^`3vr{I^d*n|k&yzgxqb%Z^TqYd z{p&M=Z4mDhzVyIvc;{WOK+$4n(RfFcQgbn;YS-^aSApce=y~!LuKi!!t6Tq|r@9b| zN8t3-5Kkk~iw3AZkYUq-PhkA&u;A2Oi{^CkT;|ykn`Xmlh{2*pXBcogjjh-=Xj`VI z50eQ@VcH=iO0F;2r`8vEtf82_=rAPfIx!RJ6>Dwn>l)ccxmrYpO3%7QSRopGQA7l) zxky;q?ZiyUdtC~aMl8YsEbS-N996MHdr>T_ubGA-fn!=15L?xtz#LC*(BN~Z8hlW; zmQy73^AMN*omZck;ln{9=ykf$Y=a4RfOps;Jf>9nvO`@})sEneTNo;5pabJQ=k=gB>h|M;y|DS_xQjHfuR-(@OFNM6OOcJY-kD{FXwF9R+#qOW)rNa9e3~ z44nOlvGV7Zdhi6#hfCb|%hR}O8)+9^;*Y4Totrqva}H?zE%jzeK8*kOl zA>M1mb|5ShYGiH=nh#l@?-pOtgV}n;J?^((Dd2B#BDAsdQ4)7;#i}yM;WBKM**J{4 z`pcIk);CiFdX^_Uu1I8=d}AN#XnCre;HuQtkPfKG=;7f?vjdiJ1Fj6=r3oItO14veh_2-;Ar z6z^%w^g})cM{JjrIO_r|=Q% zb0>W0xt&{Nz{@*Pg$I?B+9>L3vnp+r%%7%$@fGDnhgouS3P+6vUuOr%?hC4Mq9(@`iF$*ANy($3xusoMF9?Sj9+0KK6jx!uT!M(mLy2o^9IX zfE`%JS?Us}Ij4L$!t3hYuYhhp04F_{ov90B^yes#nPBI}P;ne ze`CF`tNGQV9LBg)f4}n7eg8LBak&j|%@?Tk&kT{q zdNzoLq=+S?E$1#kUD$ta*8S!4hjQyh1Slpxqydytn^w&8b40am#IwW1EjbBWPzWeU zbrlee2{+;|bpu_(&D8su!LosEZ)|15OhETYqfLfERc6wrY>^I(Q45r?$4-tp#TCHc z*eeo6jY~0r_^_#(vaf)OdEU!d`+DcjtoJx8DcI|QWVxz^qv923AJR;?0*hr)g1mKE zhmtfD{4C;^uih9WFF8|mb&cc(bu)G2c&514Z=B+ABY;$r9b8!NqJW&wDP&qb$&Jc+ zYjFU!C3;fq`^?HY&C`_=xRadW5Bs+F<<7ax zw-)!lnk2BlX3l7K&;S1F>u|TGjYjL5>lSa8#y%)=# z4s6aw9qd6TeUsiClsHU3IeFt#8}E7E++6o5dAaK5nY!Xbr8zY-Nn>9-=$0w<*E8;H zoxHjGW19NdcXrCnZW^gdip$*CPG8A-f*OsP&bml|3+y}%N)76@(Rb}Ukz$?XU^3)P zPyw)iXAAWVsfnVg8Pg=KE;-jYb9vOf;SYC3a|JSkw+Rm?F=`_;xK!<<#(&3kdChz4Awdu7+;TG-A5@-K#Yah`Lgr%j-Y)NsMf#O8K? zak=aD?7tKSaNO8c&vK4vrp5=5rmD^4QExfQvAFH*nRPgm!!aoes4}1}Iww zMJtOlXVRrScc%OI9Vi9VeD1I&l%y6k)N?gk^Xy`H67tze6LcP4IwD<6f*c8$Hwz{D z&kR0DZ3bWHFYAEw;{tZe0eKY19_4f^phELeMfhOmyueQyhHk-2gMEySfdq?`;c{m^ z*um+Ih{}}>WVurb*7B%zc@#O2u?R)+GoP%Em_ETvZHgTVYlCyH=hRA<+C9Td@B26` zjJ}LZ`dep1&THE;Y%7B|u$-~{*w`U9K(Q!kVFnyHdq>{JOXq$g6lgGp4(Dpzdm5_D z732tWp>|ND#IPpS0hr}eJM}6qscvP+s*+G(`@&TtRy@XO+m4BkU;C5#9V;}`bT2enh4F>%UY#@iuX8rWdD&aRF~lUVP#XdZcz!XcqPGGj ziPInQ_4T!IhI#A(=TP42caVz%#f0;@)~eL;`nWAu_kbQ+Hb$+wZu4NR@va zyVSoV8zdw<1pU13vH^Adp|r7EabZS4JcFGJVAeN+b_o=ahCGM1aO3}OdY=$7jdci7V(!%TXKDsxkut z^&>Ce4wO$?9a@B!8n%xpjl%|MOnIi!Ge=4dZMCmy$(Df&;K14`)jri8wBQ2z3XdD|yA+vyYkheYQk! zCU3=h195FANbrhzeXamkYFB{8YgVS|z*>%gS5ai+jqjmB&a_skP`%$fHmXRpLm57* z0%Z^&Ed2&s=uAf06OJA6+hB#@=(WCqCd&;)ii6*L_f;=x8hAkUxObT$hHzV}51z0V zZdS}N1g93Ep`gsRZ5)SeW%C?EGy~UMs(G)}M=^f;Cd)qWPXu>H7P#DMOnk2!suf*O zosd@9JMen!BZj!~1yOh%~2SVWTmn5@0cKn|HxvK>6vp-fVyt&D;=fM;d>Xqg5o0v4f! zRzI&Wo99it0A7zE3Cs7Gdn_vfkg6Y5k`qSX=@vau%Q7Zfzl?FJeuoD%5l5?<=(bwQhL5njE?{q|mn3AoJx*(uYVUR69}7A4E63o05zaO}=Smz^)%fS{k%wJCTF;CU_^ZUVDTv@s6c=oO@%Vm5T1apc7)oF8a? zYDK}jDzm@?#(HZI^s(wagctU%r4x=X+PH$P<>9TxB;#}Rw^adLL8Ua{&oEB)mp{+_ zYIFHlu(OeXC+zmqpr*Tqv++iXYZ-mKDW;$2SeMlY_|~mR@m}jtdvJtRCC1s1k>X+r z+aA>^=Alb7v8BJtqaJkW!gdls)S{8PH2HxJdl>e5j(S9#myl#^4PGI)47_1NPL4(P g0pA(|m!{n3H+?VAVZQH)bwr$(CZJ)8NGq!E}jBT8;amL!;f2*E%Pi^f7*caWMq*I-$ ztJA4;B~eNWk_fQ4umAu6L0U>o1pokV002NGp~3!>w09U<{I|e3Nol(R0B~skGe7{@ zx!C^+L0nZNMF4fP_~-vI;FiMj!T>;H0^FxD1ONaAP?A#!c3fd5!p8i1$}z)%lhVG0luFfc#{$Vifqps}(2X9W#V zkj=n=4k*s&rKFGn2{C}UC_qdkATJXT><3_D z0T2@46W{`(!T^yW044?i20FmY4SEz{+EG$p}oa_JvSwL;Mj0`dw8X7=J zUPlM<_4O6N!Xzh$4hRn-CdLNjq>GCqlapgVJlubLyaPf5pPrsrSXg9aWN>lOfIuJ) z4jKXif{KbVAU%nQm>32I0-&O(qoWN7^rfZ478Ml*Fwo!L-VhL=0RnuCjEn#rY*bWK zfRF$wDNHUduB)p{Mkegs+}ydjSpfn5v$HcrCCsIzrKqSVadE7&va-U$-F8ql`uc2cu7`z%j*pLRZ*L2kI9PFU#{d4!8d$L(Ktfzy4esxE78iSc zGGGs@gD0aSxw%RAqQb?-APf)Z$Op=)V4x~#Fg7(sqoKho5L#4O&^*;|nyx;t*y^p)MKlg2G zZ@-bs7H)hQbq(rE{ev+e$cBQ2TS= z=lqUf+FK!C>+a@;-t-jznn*Je=~MQ1NR-n1_33M4=*N4{F~6XCbyBC_q1i0v?%B&^ zxKRP=>A5%Di3h8_4dRlQ-?4k4=c-J#-uv2K-+!)t;GE4)e$l(<-A6Y>cqj; zg~QVDaCiGN;Itfs!C-q%u;nedUSQ^a2d=>Rc6VDZcl^=w*2B|NGct16E#5}(oaL*k zn^E-#TTV^m*3s_I^YiXSHjIO!{C^?t1xY~>DK=c+Kx$EyfY4A6p9U$-#uo|h$;ZOP zq)4Sxu(NU7m+tHWR(CfZ?->bk3}HCkcCk-7$6CIDG=*VXF9<4fosCx3rs2_f;#XhY%b^)NlOXZW32i=P% z73U#Hyd-D-(s}T>tE+6>-n1cB&1uGom}_>p^!f7Io{8{;jk;kQ;keLFjHfn(d?3DRsdt53>DoIOA%MZcRb$)NI zmgTfgBhtO4rIZ4zDTuWESHUzm$_!hi<^m2Fbt^^-mZvhN`o~cp5|nh}z&=499&Y`g zZ+-jA%sP6um{i9(%Q+~Dz)SKHRIYjmgT(htr)K3yL>P{-!> z#8+A!c6NX2MhbT1s_DTF+Syy!@xTiDS#j!Uo15wA?h#mT6=u8Y-agP-m{qCGqQm0_@htoO^c#|G^lvLy!J@be)-Wm1ZcIV7GLZbuqnnaQk}v?78~mfv|c~{yy}FC`Wg* z=J}Y`c&grwLi|iIn(5ie`26N>*V(jQkKsCv*F%7^prN3~r3^|ZCRfGY*3pQZuCCV1 z)W+WMQ>#J5BBD>L{;CeI=InoXbTtgPZuex?EG~{S3{Yu^emjmV?bZ)>gwyce&o*jx zVdv#^W8v{}c?bKBSV^~LREV3KTkS`$$uBo8fU3?caW%0!`!{o-z80ognw3zE*Q#xTqSLD%7xN`$TDfcchIVvk%HKol2b-Vo9VGBe2d~Dxwa@jY`S|!s$%QYwy}=mi z@Mq1DQ=v|FcE+wQtztDrFS|j+u$cKJEzr&J8#KV5braq7>G^K_6Z?B(^~O%Xf4QH# z>CLXqNj|>M1SP$jRi0rkKMz`Q2sL2X$0iAm#dfGD3PLpA9ZcZ%Rja6>AsB?j=DT&y+*g$ILEGr z7A7x7axx%2_@W89ZX1dA3D^;y6dV!!*m_R#Wh&xm43MGI?C&a`BP4E39!oi_M+luHs>#H~8RE@kAtqkpl`$h$!^^mg>}%!iNmCxWJDna@ z)N;>743Qu8$n39`0WxG80UngPloqC2b2mJSirbuR$=NGYD&a1qhVSz=w|{<2ovtBY zQ#nlWnstsjmmb4CyFNp-+Yl`*B5{Oh<7BMeMGsK~VZoBEI|ojKnJJ2qI)b<8=Uwu^ zl(9)Csgx4zm_s_3va$*^OI}!NQBA;m?CGR%*_ZZ}1;h#yH#)3}mk~$o;v7n?PZ?oF z9cShj_ZEUWX!sa^xEri9545G*8u@s66jZ0ypwyB|5G;*NfGo!+Z#G|en^g|gZ0i<=cwhCxq z#r00_?srVd9qtcgnfMd#-^@a!C8W)o2SUv0)p*Y`NGt%P_R>g+`QSG0w^ebqm_3Z2 zW9OFz+dkfyagRtyZgjv5SE{++1z5{6?i+3lrvC4(sS!^cN^hEh}#$Ilnz zY~=6yQGLp|5`t+(jiV{gTGBHOlhI*rY~|}!c?7wceT)Muv_W>qET5?qmxN6Umb}EbrTkEs$DJC1tYR&esBVWo zB!@fMvZPT>`pLL}rDYHXJ+IbksAG8Rp`Dz0x)X~JU;AwD3R3s#eGd41LD(W{*O2Clrk zR3%D}R5q{4)>b2)I#h|B6;1hAQC3-Z&D>K3Cb)DK0<)r9gMbO?uQW^-6cyD{uvM$+ znlWSpS7-!_wKuqWj@5)Bk-(mXkgIj|nR*Dp>-_te0w}HvJ-0-1H`!=&>vp{Up_?>V zWkKY&&@QBrKYA$QuQg68Mifs6XG zVZH48#mFB`W^qmSu!#$W20nxHj$WVFzkajh=F2Osr%Z8QZWTaLdww(IE%v z4wIVS+w`zIeekMc-8{I2ew-cyoE8ntc^h;i+E^JE39KsaH}%RXNOjG$0E`e$Lx3N* zk#og~EM#moE-B)XTO|h>nrjF{tCa!Dx89XGLT+Q{Zlc@WfPHiEkv|+o27l&}`T~ac zAl;DOMzal#DC>5u1VTgVX*}%B=%nnwDO4@YAcs0i90Gnrp!lemOQM!mg>}0?!HhnC zn~@H*1kSMg+`E(e*9pvkNbWE7FxMyAoo_}q<03Opodo&5q6@2(ps_41x#?f*ro+#z zBB^2?KJs0%T~ zo3mzh6}E;CNUAjKG;1d#7Fqof%vq?=w8BS&)Opx;^g(HPJ|YKi{tkphGhV{ydj9vN zW>j(#D~dXs;^bX4_PaT+^xVc#4-0EtHciuQFuOJnKd~V`%%de~6!S(f_qr)6nen>G)U@Y|RLnP=Y7&8WPtpsB2w&M?oYT_U%5U}ecJb#+ z2w}>@g&(F5u;U2qeCpT6*7~h?+xj&{+mQ>h`jbrw-<#=}f(cI#A!sDdcomL#(LVzD z$GHeUB0v@aTW4l5|MeCXquW<`o3xjz4?be1Q9{}jihKeFH1E{bU7rlU5Z!rW?&~6B>6$Kj6Sh)w4ZdrOMvG5#$6D)t$wr3GV#HbK9nL>Bic;W?ZnKH26tPzJtvksmE4K>4CDUA}4*B zK(TkHosL8|nGtuvvuktp{<0SA@TLj2iwG~3n^0MH&@2x9xXCFZ0tkGiS*+!MsDkq5 zCqM2(RyJ3Rr%X3g_9ETNd_#?H(>dx4qjgMKw8$wBBMFVbu!BXOts*6fcU_)%dwm#N zQmq{Q$`fFtOz-90ZAa|BlMQaQ(a47csyR#$YL8EAfI0orr0>4}^4$Bv1QHs<2!7sh z3YwwBe6O#2sy`S{m{fMN>5)jTyWXO}76pLY+!fyoZ2)Q``pu?unPbHa+vX&9Fv zh@W%W4yEi%tzWInCxQSoYh*iaJ44ItF_81OY?#QqHT-p>f-wZDKXq~xz5jDXk4zqY zZxA@T@!)9klN&7dum}FO1}=RSL4U5C%iLZCnj!1d(km=)wMMK6;HeHmQZKBF0Bc^2slgBKr@=wl)C?n$mV-85-c+^>J1^o^CgyIMOaI zIBYmvv&SfNjg^x2l0(ZFEc#L z55zTVfyVX-f@9*{+_ID^NAP5J70#Z)2!%l|qe+ZEg4O0g_`PMV*tPASZDCwLKq{*& z0Sh_rt?EWmF(Q?$b9AxruQid~oGUd)V^y{uHCcLA>sYSnf-e*^{z{GUV?#zC0LE(=QCVC zTb+K@cj+(MA!hHtM<$4QFeYeLo_`c-$YU6wQ}n>g2qM> zkf>TLNRqxo&_Yq_^D@PUS!5DKo8G!{`t6Nd#kA!toHj!zFy&b!qMY9&*kkV@t-T}j z)RtI z-CWDHTQD9|P^lg`kgK2&OMR%)N%`|b008F-kOh+<$%*jTqqcvEvq4!`YnFweD*o0b zAWQwFLi%*YfM;W?YlRL&5S$U!NdC8g)1@v*iH%h;)0p~fc{|d*+xssng$r&y-zl}^ z``kyLlvc%-L?!ahyNgHoQ9CjH z9Ggb=33YRPpd!;8s$QT=@Gqv85fj-_4lFPAtGp4teSB=-k8ynPsZ+t(~i&3N@+ zS4qL2k0tC)iHL|Bc!ECWAaP3TD%J)iMujk^D-qrpmqMhm99=}C10J)IV9EwQjOJ>@ zQt>Ewv^d|Zfhl(;zTKeBMw%{BFz|D^9tf?~3BO3v!m(<-A!wssT7Q#H;9`;Ba#5#i z=m{DAk~amh|4%{J$#)kcYWnvd`edx9W393XKF-+Wf}rJ{WuwjQYh{fI#;VP&H#+Pf zWi>4H5i@gTv*?8eIaGd2y|w%z>F_Q-ahDV{69?zgue9b>goU9RWo29f4-^ai0Avm{ zLy6cICuTqzO3d*|ALu8kw2~ZskOOFDf?4VE=tRgnkCVNqMe|J%#NVV4>{!nGYcwxY zKCj#H;$q8{*hB)zx~eclRa)WA)C=akGIaL|!zSCyBz~w@X~v9Cdt5du=0m;fMYunA zNzy89mpfQ_CPQsdJD)v0Hy2Ei8|uL;EY1PZKXXFren?mL+$v_jmLgO!MjTBf-E3KX z#8^UcAXVM`u?4P&H1tV|w*j19SGcx*kh$(pn1UgZW~j2V%$WTG{-kV4Ml7|mN(8E9g~&CTwp@cNuNq%@Yg$n{>! zBXRj&4MX5pLw|4Yy@Tt=ERrLlhDL%yTr@4tIW&+-%gjU-*%3@b>>R9tF!4FU(zFP1 zc?6QKq2gF_e9D~}{)$otiR3Zxd_t%`qh4E(B^)A;2Zn}XV^bPBBs})GhAaxbmg6R7 zEZ*4avD5b-`Od#i#p6u!`HZtyQ^D;Y&e}E$MQvvHbwq#ej==@@;h>mzE7ccd}J^gV+ zvpRFqYm`R>K%4`UNIf&;K|H63S!F5BhtB}N9OkRYb+);g=4f_vbGAtfz`|61w}@N!*R;tiv(RnaPuT{04V1G5_SO z0|)Xq@EMcC^7r&ov{6cpqR|R{?ui5Ey)vH+M{LH^#?^KR_GHK(D^hNon5ONsoP=Nu z%H@6uJF3uPf6aLqjb5-MH@$XkR!~__pt7jx2BXd1-0leYtwRElS+UH!;kAf?Fbk9E zHnofet-HayQ5zc@7l@e&&MNcFQu}q7{yRH@`ftwdGFmhTHz#9bV<$IcXTe8TgXibp zSEq{o*NTqUUwR1JhIrap1Bmgb`TWd5?u&>(>}VJ6JTx=jh7y6A#BH{&Eln#W+jMz? z6fYx*KjIi|&*US)A23X<4 zhib#T($aA+Lz0Y@p>YF{>vgoT!Vjf8Q+_|uiv!*8jOIu&Z%A=k09F+@gqE2Y$D@8O zsCo(6ZI+}dT@e=w4EFxt?`{Vut3@0rG~(hvX`MwgCYeUwWeD0>pmY=p?jV>_BONu& z-NX#j8DknQhz=q33Q$DxoCkI~hsf$`RzZ|b)7SB0h{Dr=U zef*~7@ce?nEY$vGpw{uC37(!LTFuUY$z%ytsG-u@)k2uA3sLJxYa$QR=$M3N5JcB~ zaRc8RA}(wK#Zw=2+$9J7mV0|4T=stdSYCLaR<#XkrAD|lUAr?E;B6A8FAi13JnmKn-2Ai8F0h%#q47N>h@2`i8NT!F)%8OxV_O}9Kcp$fF$ z!dcNo4gEqX_c8Z{@Ak3BvcVvU)wQNYCX*chmsf|PcGYNXh;o~;DqO_FqkX6=K+8@7 zswaPn#|;SjxESs4f{qDH;gXvZ@5`mRVN_`dUMcLw4H)|{a&eQSR>sDEB;F=nOAGWN zH=1#M6fqxZgPNklrsRmVXyz_6KttA1?54t(QKf9B>cOZBtLau!`Q1>HSrcPgK%U0W zB+rG3a7$8ekw$dE-pPxH3YIPxYVU;AV7k2+*FYr?0V860asek31(<2myA;cxIvVNW z<=&e13=ZeMm{w2V*(ugz`Snam%VIiy9lC7VrykA8ut9-gyT_hp9E!1u0J&&$ubMTJz9Rkp24YZ-HmeoXt_jilA8@dQA&Ns2fK|$N!&RSC)Wg-xSk*@jX5{8wf z)Cb4BZ0y4q3)TIAJS(_GWB*m%DGL0aZ4dak9QYCfBl3f(r#wkZ@D76K5;yD7*d)5z2DXnRW7^Xcz$Jwxq`MK!RS^*eE+iw5g5xIzxXmWdbVpL+9k8C@<9|Exwzm3N%~O8y`A zd+8bvDtH!rCPd;{5|nEx<>-yb%K$hQ=6GS%|X#u zjieKa$Igi&iXfw0oD60kk5?>{^L@~}UaH8(EL~u9B|B^oBpzQ9AZy+RC7kFZu75SH zF!hwh+t1nKaf@6^OV@a(_=k|M`a3~ry*zc-K@HWYH!CwI@bw_+^_qmesjRaN)PM(E z=3)AkKhS!-QXs>=!RCR7vS*AcBnRVBEAee~a}~8uQQg1X=0wFf5HpI>bL2U;k6o<@ zxpprS@FnoVt7qeUw#h?$15LURUvk~pLuCmX4M5-G zbet3UgJGuY4A~%5u?+@1!Ds=hl8Rry0vdv_#{0%hFJB{1{#lf}gF;CDaG`f4Xt+tT zEy={paM}9{KPV~_Of{_D^(SbuGx@JTZ{PkJwGe)j79nGlmRcY6f{5j2R-*+4mK*0R z4eM$#iM3{mcVrhcAA6#eG!QF!*rua@xB!%D|KgT72K8w!-x1KcMv~kJi6EQz8cm~y zNZ#xy#Df99@s|%Q!o6gMm_ROY?QVf}gJ&tpT9*+~ovt_gbzX}lyxx>5BBXMetzAV1 zUhHzCg}qbAgGPicMv26)PBd{oq5_6iOP3>V3-Tvawxltvc7_ z$Ay5}TEVE-EW!B?dCv>B!7$NfMMq(-7CaI)mhsb_7RlQx0-}O4J|EiLQW6;MdV!w4 zI*m+?9hqtV(c%zI%4jOzIxcj6p2EXXQ2EoUT9eL8%c2tNY1xB3Av!GIV?Z?I*RB-v zx9Y;QwTi3na-Al+bM3G7a;Z#o)Rw(!3~00>dgZzeDIXIZFV6boUi4qInW;&QeRrAH zPE}wytz09YNCCk_qgEWq z1LH@ogEVgqP7AMI;y#mN!j6;Zr3Mgn1-QF^4W3Fy(lMiHkb|wPI@~|F2yWG$CihgZ z8CvDFqK$80e!a<>x93^u5uh&;mUTXdwjUfG9=OZYpW2`PQ>#1-Ctozb%!D@ zI(BJy{o@-D{tAY%*%|t%hW&%s!Cc%$&=5pzf!FM)#$aR2sl;5; z%>6?wyRMKdSo8OJZ?<9@`4PDhgD|Qg*5X%j!|D6KsM+6Yzh3~W>tE|;AhIm2uk}Ap zr}}X;eKg@KuX|&Qulw@Cl-2wBv$~DqZhFbcR(&JJ0PxuTvx$kwni}^6dbA)bdkJhz zh7;3inC}4C7z`z&SrkLWt}tD1NUItDF7VwqgRA(&tP>p=$$^xy&f7pJqkAZ#D>oyZ zmGyb?GOmOy`S^{jxALKys1Dcp$N>!LkT)J%Rb;Vc*(JAujDKjMFo*Syd3o+t0bM$F zxn(M)j+_Mofzr>(q%0T*PZ%s8@C1*{_-!&bo}$BZ`d-i_O;U%owxMyWad7U(K$$PZEW4%t5igO+>zoi5 z91%!BkRC8=Vx88hQbX0rU7wpfeJl9kJ_Rv4cL$`e(Q8~FxV)KlkiHkUqiE3CG;V0F z@#}LS{h%4bg2K(7ss=%*M~I$wHBq?HQhrOYE^ z4}kiR?|9D^DIl;qTfA_L90(SvpMhaoaE}$US@U(jRX=pGh2`5uoP)2GUiqAz&lr)Q z@+8<73DSEDfkk??`_}qKe@l(4mZKJkM^H@SW@i`;)7OqSH49FuH&U$9i?Abt1H|lRNQ@176>-XdQtg?vZ=R92vgsf>=_GV)l z8KUvTg*mdpv-xfpOuJo@$U?3OC;T&kwu~9`)*{yK zO9&~apfwv3FAI|V+D*K*-Ec0YyZpx&vn%_VBsgE{te~qWYCDikj-jl7DO18+Fp$e0 z4viYn;gq9DpgYaoewU1Pggf9Om8%#5B61jim>FSg;jP>(?e$1n>PyI5%fPyWdpN5C zR`KI;I9|F1ig};E?lpIT-yez~aBp1BkMz20!1d6mk2PI7IFFZ;)xFcX8g8}Iv**3N zLs(%xJ!tq9VxU=(l+YtCqrOe`Jo$=sp_SXs}SdKzyAQ;t@qttdmhmJ~IUh2_HYCo|cfweE2 zY@vVu8EGkV!coptE1X`~sueu>csq%4&ldbTP~+N9Mqu1Hi6UNEV#@G%^zUk~qLWIa=8lq0|HKs3Ney1)Ry#h%LLLpd}V!LP+97eoZf{ye2k_LQdgi^JKz zg~kH|JrODi;)L!^W8RxcJ%k|or3c6|*7ZE!2{BllyQyzP)zYQ`%bM#dHcv0ev*?8f zv!$*^o9%C|EROKg)^1iu7&x84UHR2xD}0GgE|-6h*WadbhCd-zw_Dq-)nn$PCjj@3 zjGuSa0i6OLy*_7#zBT@izog2Ks~2!NOUB-^vsv0M?ueC{q-o2zJZ~s{HHrdFlzH3C z+5ra-S$g|vvDGu-p$k2p_UTxPDWNL8n`f;LZ+F((d@>*W(QSWiPDsJY^bA_l-0+5x z!6{m^1KtEkMxDZlg9$aeJFcYi6vzk0k(BiYsigI)B9v3WEgrr+4os>fMT#rMM< zhW;*@?t%s_Nq2DLi=DCFt*E_Kcq|$@`A|bP+1%TV3$UOWLDO2Ivj0uVA2|lab?79omBF(v?Of?SLTBZixh9%j2@pX)y4O(4P`6x>iu<(4Dt6J$FJo)ZfM{g)|;> zP6|y#n|7Eq0oC+ENZz-(FD3xlN{n^Kfp%af0HviS_o$sgxV7g-2)vC;#@gQ+Rw*Z5 zK&KmpWu{4|K_{gmtuHxVszeQE{Za%H#4O7O@hCA_uyP|ZxTOj*3^q_NUFK^}rEOYQ z>1H^UIgj*vG8!D2fGez}kP;y{7gFESBjP0*xjRbLpZe?GC&pk!+O0lg?%I9dyje&1`;0l-Due~W$S2q9hZoZ<)j!b-^c;}F^D zxGOf5HRHJ)!*x6Nn9bKV=~uRxZmWb!2sQp&KX%Ef5an4Co?(6rnrJ$G?Lp(6!yQHW z^W}qj&qPFS>{je2(sQw&k3gLEG}+q-&y5U`dNl|0wK(N=3;kNd~f6tl~8>E49YunLo$WVLR!`&2%En)I)%sSHaopHA2* zU6gPyZi-fy62CO%=k7eoNSe!EYyCpCUJ~K(DC1*dSc?PfLM0flqm^kQDu|Aoa2so{ zd@jV(u3E$x)fG3uRltOj3BbcA@2?x%FJkC2&Zu%{p(52lV()#S64} z<@@+J(5kyDpY5AXCMYZ1PAU}+^+uju0)`|Q&)Q%5Uoz4F=X{JpE}J}DR+e1eJo02F z42`M*EbPQIOV|UZ5H)jAw{6ox&3CauX7*<{bMKkB;LvvftC8^7%Ib|Cis3lUiC6w( z?kQhd6?fp*B;F1$1U}ksP)XwE;o#)WyG@~okL@Mb?`I6~LLrc|$7~nd`8BefOHeHRY+K(;|?`Q`Ag2YGkSZ%`QIynJIwDu4M#{YmXho_ zPtM?9{(Z{b>VGKk(;Jp4nN8lFJ!3o1V=oJ)r?WfDaIs3)u z7rn~r;ewRHWcXr48*@&pd7l(wU_Ie-*_CFbptd$pi?NaOmSmL;b3f~3mQtreT^!!9&wuz{}k=T?L}8o~r3RqC`!Gi=ny zFfd%~Oo(s@xu_~}#)onByMOUqPmx<3OG~_B)w*x~`^~qS0dPi>xH}7-E80mPGMMrZ z7P!7Q*e>i7?8iQJ5g?9dNHRgQTKf$T5u;%waMjN!Via&%XS_Me>{m944#mga{{M5& zxBn~&309cMjf1g%@-c?P8@#zPiggHe`!{~m8pQjS2Sd_|m?08B&t&_gJ(g>Jej>5! ze!UM9d`AS!Mv*vTo@CTSWKCKoV70`$?U&$?{>~YvCrrddNs1nTz(2%;EEG`!i$teO zb_JOV>(*nymt+ncIh!2 zdwWc2Xjw#8W?>(&zvGe==}ttp)4X&hDr52abR+(Q{>Xh-bMvkOOTRXMPMXGuh1GcD zj0`^}8w7#!ds$c)K*6hY_iV|a;4T&`!Mq7KZa-46HT=dM>Mz0Z7rXcaAS;ixRl|#V z0d6aDaKs|#Qp=Z0%<-h=Ln@BT=yQYL$5dp+eM&V$--Xx>C^ZXS;pJpUe24hsc8? zG=A^!Ssh0l_vi}yx@*jlnZy|VtmV28t1?Xv@wzMfuc7w|rEX~dci5A+xC^S*#dl#f zGAiRJ-HXVau){1Met;pimh`%)(^F*WxU5e*1yJcNtE3<7W?H!HN!4m?dx@2OTQqqj zT4`bHwzY$Y_udq1)OJfS5U1e?m=$mR1;-sYY%fU~Q6V8WtA1L2PZd3Gl)fpI5vpkk zJd*oVXiYySWZdNHqaHXA0r1 z7MoJ%`i)xUO@nIlFP|L)N5L%mIIMULa;xewZfb?1g9=eG8j)#~EvzGcj92W3 z>bN8`d#$F1>>?8&Qc24a3-7Wr>eYwpWvd(>f(pW`oEzU3`o(Ra3~;TjHS;8l}EbQn*3b{nAdqKi%%TCG!IT0tJqcx`#yEew`$MXzG5B57 zPvuCOF%30xzJHEc>&`2j7XaPXkfuN}^+ssd$kFAAwhFKu4wInlivxcUP}5zMhCmtf zf6-MP=pnMy6`cEwIXwIANG3zt-K-~@qjyh^49QgoewYs=x0)5Hnyx-Y+QUlZ3uNgw z=~|Xa1nj71!z$&CZJ-EIhtzWj(9y>?1ld-KO#k+A)@;Iw1LeTm8^Wj|#Dx@kk|4z{)S|OsXgB7@F6#-` z!2BfT%%SN1mHt5SJE=#lR>PH>utA`waZY%?D)qjz*Pm!0)$${dGOU1R( zHP4%KUx}-NxGo00I~?o5%;&#$eQJXz{dx5E@Jn2nB;&$ee}L9~*q)Udogwy_&T=i> z8-BD4SvEZ-OSmoLFve0%mOV>+s~p&AN~sKwR|6h1+7mqKMvq)`vq}IB0LbhqbNTUb zy)2iS%r)EeJJSJ<<_$(UtzHS)22_%6cr6eYh6oMLbgx|<$-TV7 zGNW}<`ciUfz1z8arex>{aQTDAMGK|1KE^x!Zz*51X^Rvr%w&-R?i$mdqvkZCTOVQm zg#-C^6%1%MY($jRHnRc$-_ymJ(XYf=QA9vEk!rk6X)~Wb!1vHy`rubaU_r(73I+~W zY`)N?OjOu=7j5BRW#Jm^N+!QQ`&K4arYTD8CB{FVE_L5DniX9_#+(;+7Gt+@ao)D@ z2Ck?_E7yL(#uH_|Jv(Uc_cZkFW6M~H!?e%NQ0WFhW8nt=#j>!N0Hds@D2Fl%0A`A{ix`KB@9WK*2mBOZ166zX%vyRbLDC^V80|HevNdud6#0qJaKQhHI5Ad+ zQInoYR8!6azd*i2J0?gXm2|`tsOq^UeuagQQOoS7E6r71Pp9xlgp_tdZLsu zg&SiwducZ3pK^~`Eo&?#G}Bg6MJ1d(bk%=!)$3X*xlV_SZG&z3K9*9Fywu*@P?(>Y$o zTTXDy4s9=sFvlY&l-t-wDLhHUPO4tWe*e%63)JE$i{$6dopyYci6x5S&i7mL z4itt;e!a7Y-y4Ajho>SFKr@dXIlxXm0kZ$bFI-%i9VnN4E)V*&O?vSmUuz$0v&^ z=|<);uU(dsZdSG{N2UKRpxN4RI&UT(YURGQRm1~qZp2-_dDuNdqKB*=k z@CtlFx~M|$NW(Gzv3C@r`6qd$(Tw0q|Ci#X?b?^|MgHSgO zh?&Rze%%RTcXT^-yjad@pZ%et9VH*n+h(%jn5=Zl3a#(NwUB(H(nbKd@0KDxrNj|G zW_NpV!2d4*Z$Oa0Yoxh@JH^j8EtYK(#pZ3JFU;S49RHY?u)zDssoCYjeL1_*9r9-4 zd17i7M6;yw{ zt4cX=`{pDinM@?x$V2q$CtNUSk)S(q2u*A=5!rsl!4-Vf1D4E6F>r+yvxgZ9O7q$>Lmjc^_Y$3oS9sa05 zS@Z<>>-z`u&MSlr7LQZKVWU$mB@6K|DDjHP(P-?NW6)%60;T=9m0~aL){0%gW-}O* zO%$W`W1qcu?#tKWWe2xEDd@=-2K{ioQi~UYcRvasWbLl6OV^EJ-L}M)q=OD_kO+K` zVk;zH%hd8!0gF76OZV3Cjr6|7h#u+ns{ocN4TF%0CmNWRLh7|D!wx>%wx0*A{evGp znZt?e8nJNVLHe`BIQ75ehFEI6o%_R{=zje!;e%rU03ZNKL_t)Fs{u0F=|r>%^FsK$ zzrQ=0>ORa)eNML2-__jVj@QS0xl|{|XGbfcq$PgyQzR_7;#i}9CQ@vRw(xOta4ca- z)NfX?xpTTZA{QB_V;hjYZ!qSsF!e&beEZf&+$-80M}<((+JAUIS1HPwta?}Bp=)w^ zlF4MLT(eUGTdNX7Q1FBujZg(bQ=;)<3|CxE0&@Z$Mb*Pu1my+koQ=zAvDu+tsa2_` zT6REC>})08aUeW|aF@?2N&$8GG2HETYq{YdR`z@R9y$mEG8tE#0zaYKNxJ9=AXW$t z@E~4{_GyNPNN5)$S(`LaF?TTg+sB^L-W~)oGywo64%=Q#;kQeHwGrUq%*fa9Ke<|L zT-26u6e*kC>SOcWVzF)oz!!^*&J8t1vc(E(^;vC7`&Bd4$qU#r18P|&;;F`)-QuTD ziNe^CO7bO+2<`HCTwaG{_7%}9bW(HLpgI)d2#-hHZ{Or2-BbQ*fQ1XqDoBtv(h9Lb zhUho+vZa7!N~>UB005R^RFv(AHFEvD%083dxBCMEN9T<0a?!4SVh!cJwCqg78L8jb zC~mhQFHA<^PX*6TQborF%!qj@EdNYv$;m;?Y3>ru{sPUNXlx6T?Ag5buY%rlY;y%{l@I;~n?_7fSu-a{-`~*WpG%Ip#{V z&xR4eiI`*p+@7z}0d7$P3ZTQ&$Sa`{ltA2_vrx=WGj1LaMF~`53i^k2Yw!H(!j&O- zF}pz74)9QuqXc~o117`J*``u(TE719*TqW@GobvWpvRK3+bKJHdNLja0GwzhpnLlay~A%uQ8^1fMD*L1-`~j*auJta7R#tPTnVs9 zRB<5Yw(e}Cw>IH=ghzV4^U)H(lIS@8Mm!m7N2E-lF&MEKgqjI!AfA4I;OJeWJBB#5 za25Sjzi_k>eu{FSFD&x$4KEjk%aBv&Ia%&JPH^IeySGJZi}ogqomi;3Pi?86oENdK zQE!lN)sw-%CnStke*Jz-wcBy&+1Echgq*1 zoeEWf(>Z?HMPI*`0Q?F-MG+lY$8fmcVE@r-3Zd{&^iHFGx`U9!eHY~7D)B0Sg$wso z+L1Q1VcEj>bkId1(>rBL7}iK3Bc6%2CdLo5=*4v4DDqPRNACg_`q(VpBBx^;kk=yT zUCM!AFD%-d8x#v-Rc>4jHzUr5J$$xF2o0D|wHle0+a|bJb6Y64f`3=S!nW_3EVZJ9#T#L)D6NA(eax)JriNu(dM5k5&=;y5-k4l*72!9HYOk1t zfNiPn`WseEN+$86b;aI+LB5)+G|(QI1q5HZtX0MhDq?Ree%0$KYjkir>{F8Q)NKH; zx^aLwCNHPDcgxAZ8bP2u06KgG^)^Bf#+r?!DN-1~e-_K{<#1Ujj^n1_MH5gwwh+J& zj82{cWb4M0E^j2_@@ESdMIR^gJ`BvPqqwldonJ5j;$BkSl72#ZbWJ)w~#+ zf#nyBFvhXcU~g(aIGQ`H{ZK_!qU}>Y?929PlXm|*UJe7svE%a>OE!0K$36ew@44sq zJo`nL;~4V+qbxQG<3c6cx?Ug>+J&;)g_xFN8ZkCFV8V2xt$CB(01Jz>9)r~aSYk|7 z3pv)3fHhi8y6T^9Vc(kp3!{9x&^rck!x5i-e_bUx39vZ&7XVm{$s7(ktDrN*82NZ;~|0}zhbJy!Hh{B=@=EqB;l!Zo>Yv-IYLA5S(CxA&Q%6vwsJ(Fw^`)Pl{ke%y_@-@&*9C1dM@?g>E9e_pWp4aJJ%`!^7yvh85m+Y z+?g=hK6^MdD#n?x9;LMOn2%{sZRE~_U2i$HXkmZreeS~a^ZV4>hUhs$Zenq3kz zi-tQUenUpu#vxg8e!yZ1-BB^EvuXk?O@wFxES5w#K24CYqN5~}Jw2B$Uw!!(KmEq{ ze)r02-+v8fgryP^x%SvYLf~r*4FNM!_1FZD=rEXLS{EGS++p7O1eHJmd>%%y4*_8H zH`N-r>XVD2u*e&8i8fxXRaUh^Yq*=Jp+5M02OAq=)zFK2MtHW#N9c4tA^1Ly8<_fr z=L(}hAdzHTKc?%ZD!rTB35Ua_FZQ-tw!IPwCNkreTc90`L~;j!#PTfUoq~E(i2K&! zpr{J(uK3{^>uxLr6Z=W`6vge3CEVIB0IXEe>kzn|Wvh(@n!i7*9PI8dBr2;}D;Ncu z%%nNsp>=Vq0GNJjVP_{*3V4jNaDjxgpgRrd^o5M1PXC+|1A)#27`Y|b!x4U; zxEZp#0oGVozyj9VlExnaU_n>pReNEGJ=a`hRX<4Dne+=l=4czhBHy2c+$Edn09XR~ z3ji#3!vjmoH_Z_>eynzs8h6K-c;>Ldn9dy8SFhiD^}FxB_1+t|-~ZL?*WP*i*2f>* zy!nF}CIpQ(;-9OLIZ`3{S=GQ-m(VIyB6eHOozs+3PYJWknsxf=GNRWYBL*!s<`0i|YQeDFk7Dem-oUEZ&rI9fJ$W-r`p3`AXndXvQL+AovfF&Pc+I?^x& zSu^P=_d+4iE3>7Fb_cnXIoyDC?**1tm5sHPq|@n47I&BT$bYe>a#J`fJ(CY+jgw?S z7YGKU(P)9Jno=baLDN)abpxz%J#ivn!T6#)Le(b(T_0+J85E)v)~D(E)zi~0AIrU* zEv7kU+G`%3T^eq#p-_^R3*uF^?Kt=2DfQ&x z%@M_HRR=Ml<4O!7>Y6nW3kd&(f4T??i~0hIuuqpP+(uI5dHp5^}adSH< zfFU-^o!i|h*<^>4Mx%@pYL!;Yp5ESqmr@0wY4=(_@gSNCSZ#sSRwn0iuUc)Tyn6;T z5NzCUPj2ilWa6>n##++po~f65urKWsj9MlXme0l&f6)nbJfIin*0t=0Sq^8c?%gf!L%mi5mrTm*of1jX@^?79b3y0`6TLp9$Hi2_y=z zAd!#}#PH#jhQy(oJmlQ6#z??gl2!q0{us|pF6~trX{-MzuKDx10jnl-C)9fW{o@A7 z5HJOn5N!a9?64IY-t0eqc>U&^Buu???e%Z{_LtxJ;~(Dr*{lCdGyQrhZ-2W*mvooD6IZI{?Ztxu2Wd`j*!GdY}7s@4%tk)V?UU7pgh+J_$0VWP* zo`4mQ<+f55AP?E&c}fS~sjAdHbdXryewGRcY(`HwSS~ufJ2F{dqnZ>?AU;=aVYw2| zBo^{(As_B47B}p`v6M{(ebIiW0DRd@9-CGTf*=fx|0$+#^}IS8V8K$kxdXre337)M zJ|w|Z;_5pASZy$OLR*(V3<7)SP-IC%0V~Fx)8(BDqtZzjjACCZP+%jW)1U9 z85ZNQ*%igQ^q>EI|Mna20hs#9H!r_*si%j^Wn}^ucU;9dS$G5pJ%H~FR9$!aAQ*!!5}{N-k|oqO#U!Xl3d2S7#~`S|*joA{MpY$wX**Z#$lNn)6Q0 zpj<9OZCoVeiUkYChlhu&PCtd&j*WxODh+STmP(~m*aoU3qb(aem`V#cu)7l7EPx^` zu)G{B*lfTzi~O;AvU{0@WYxKyJaZ1*ojj^MC~xg;mlH9&!;z226Rvb^(PNRFrhke9 zqcA1bMd&b#ZnbFj)m*h@>0buXxw5jEfFS-2g{Hlv9N~=%8%#K&RInR6g=gK-jOsC|8frBhojAYY=v@l1kO7r~*VbpIZVJnFJJ0uRW*Rxw-)~lgq>Bqf*N@`#0ZG$YT?|QQ|gI1fLAp$ED(AiG7MJz zoQE2FKetuGM|GTPn5$Cr)`R>JMT8oRtYy=mHbM0xTIP-@`STETtRp<5oip@*+O9S@ zi8~J??Cua-XIQK#^`)Lg!Z5%%+kvsedlHC_hIno5)p+XFYOhHh)Il&qpTP zDjv=TCOR8QEI};2bEG?yqrygWOE}OGn-95&f{+Wfc2GWlcSnN3&d!;Zu|S|M&@tK- z^n~ed=WL66@EHg@ZFIW*5DRB<5r`>9$1{!vFAG?>)}R-e|9L}=imTRsr|5sE>4tO( z9UPv6nemKT29bak_f9OFDmn@hkcBDl*pU@5ED>kPOH*JPvQglkayU9-J+UdE%18hf zvDj08zsBY9d8!*hjIzZyIXc=A4P3eun0A%N&Cii049%27h)W0p*R-f4tmUs8ur`rG zcLG(4O*nlDI(awbC8nfKl?CFwB@h?o>16Vzh9?Wp|C4{H@bVd=B0O==BB1@_j?-MX`xnG%seLciar+d>R?2` zZHh4~s^cp~G9IzUIU4|2Xo+Dd;ck{9v3oPM3`HWjj?Qr8B|7GtLbXgDd43r23yDbC`NaUEg3TI~mEcSI31YoKX zg3z3Lsal$?SLX+FooOvVUkyX|QW zQ%7~ek;z*z0->gcoPHsz#$pMF{aIx1dvU)%xeczZRLhiv28+!qFgB=Fn#)8v921Xc zl4#cpSavLxWVGnsTU98Yre(M&D$Zei8E~1%l>Vkjt(C7-*zsjbLVjkzg0crylswNJ z1dgkmaBHhGjO!AB1!pJb>0s~NM8M{#a724T8yMs^JoxMTR=)IsYlvXJoKVW zs<%on3hv#r-v9pIci-H+d3Dnp#CvT&`O&s7j_x}B)!9G1_u21{96Ow!o0og4hPjnf z@z1!ueXxlt>r!F|1T?lp%!d?DsZYK`Wf5kene~xTJX&P~LV+S^V}Xk|7rmYqmZV6< zgUd=eWZD9kL$3(g%ULqVW$uVogi_=xIr#i&ttz_oDc6-GD+__X0a`D`qA-gmvuXr# zN>z%xN9H<*qd&|ZAQ%$(NBp0CNU*Cw%Vcl0J9*~f?4R!jlJ#nrv(e-0Cx>KQ{&4CT zjXFeY+xEdgNCBv#TYBUNkHVEtWIstI*t`dvIm1}`4+5@rh8u3p)su`s~BMT`nZxB{%o*IRq7`vXjBiFi`7hdG=aG zt-)-ND;34qKOUIxeS81Ao8R2roDl^H5c~kBv3Bm=f9}M-Q#&lo^Qf$1C0QBsG=sGi zbE8=&gXdruFt-|JN_|x^W`LoT7lsss9pZy}1}6pyU;4klmKGKaLI}yc9OAO2%(Q6y8*16IWy{#@kTdM}yWL@nMaa-Xt^!T98Z*2e z=9A8^gxLa7B5e>>YqVm$5_n5WbYgKmXVsWRe;}Gy02VC8XsJ@L*s3z~(mxfB@=-X$ z#UpkAS>mN{0shKm^aO9K`M;sWDC>X?SZg$zp? zjsh~Zb^8Z<3bwuT+b?&W{^Pmx$KQMV<1^RdBq%rc5UH60VuW|38chyJzSOcUs0lU7 zUOrj)<@xhpo-ZuCc=_}H#Q!DO)XMl9sdg{eR+IK{;_=Tgs!@T)FO;CGA_;8~L1GO# zBbe&B%lUR{p-YOf>@0SQQpbaaVI>`~IKa%WT=d{Iw6GvB_ys5Oaw=5~?8HVzi5ea( zKlsc-MOGTQN>+qx9ei9}H#xN_w_|tK)pk#BXw@6c0=*S@m9p44f*60Q9td{b8lUL< z@rS-?;JfE@)29^>iwjtvZ>gA!^!r?HzokY9vAIrXIA!{>@`5$74X&B_q~TRkJV#YT@)K>9NsO8#YnQsW3!tS{a9Kt^)eXV5vQkamHY`Bfp1dC;Phemwfp}D!P0o4m%^K@U^@Jz7UPXd;s+i|H4SYotA zelk)gVMSmD+8=TB1>FSF2|c5>TQ0<6FSiH*VD+W|OI>1F8nD1Ms)5rM^<7dhn;`L$ zZ~LBt-ElJY)s7R#2{QGUKNTLz%{zE7FE{`2v1=quoxQMs@6Mw@jqt02uQ$}^$fbs2 zg&Hd((dtNHUf#jH{K6w2zP<1Kh0{B~{``}Z+uz*0Lfm4dJsdLr8AeMrXo@AlWz->I ze3B4HHAS>0DulFasKqO>f)Qy>sx1~jV8pF@+TYM>fo$}~&)W)4zO@y|D>kl$#UV%Q zaU<#LmDLbG@}xu!%Ow|L1sbz4 zM@Q`9yplW4R&TZM*|R4hpVvt+D5gdn;rg8;z;VV=H{KQO9~fySemX~|=bV1wssRf& zV)YvZSV(yTU!R>EUL+|zELEAQ6(~k?#JbhcEV^>EUG=JIWper<-lHy4U0ht^;r`I{ z<9HzSVjj(bO-5y@T1-^XI1JY`lBcm8ztn;!{zWh!FHwoZ+9#708RW8JZ-37hTl z(-%{t(TWO(%@&RL!jL~$vE@KrOY{GUJJ+WsvNVjxq&pR~bRnZi5EVorg}_9Z1gQiN zxd@6;*E=dAj2ARRYedwvNsI)_2u3hUxWoV<+y+<-0Wm=t1)VM3DydNm_k*==Iiz>Q) zeF%#qE-kI-Gs6liDlULL=iv=3uPI-2R~ji?R59lkdMHy6Q+bm7BS(UXo)2QG9B>Mh zQ`P%|He&S8REAtbkEf&nGao#xV#eS6kTPECxE8$LcFbD7|+RvTm?$X5Q z%^qhBlMoj3$NG=>!Kh-i8S7_icW;jwutJTYh zAmhc0jcSG1XD2?OitnoX)Ds)UG51BOV#JLTVseUKG+XFISW>coKtM8gPQYpwh`-kH zMw|=eI$QuzQ<)r8J>9Q7fdx}j5!V{iX;Nf(bv^rkZElr9oEkQ zn9gdK$+ZTf0hjB|^3gYc^Z;yW8^(r>da1rk$%4R=b*S6R(5iX^{_d@x%O(%XGggPz z>IfAak_}qmhjPR^i=n$l%&LRB42{mbdpg-uB%Cys5KR^eh>ZjQth?+z2Z)|8blusY zu)T?(>ASKsYwMEr>(_7C`h9v9;1ulo&aMkzk#h=`Q_G=S%3}#Xc>40^zdl&B$TM$Y z>8h2dZik-?3QOEyy)!F~fS$A7+I3?4*SNKi=?yIIIU$VQquR5Q*T$lhcL+KMBH5SR zz;aKi7ZSXpxr;MWbEnZkElQ1Q;Bc64Kc$za6L`L{n?%>K@D=AzErX`kN%uWV2@Bzr z5b_$FmqTb)eBz*lGpS*w)7H@D4wlv>)YTD^dxX%)7Q=rqU%;AOPe^PpiPlDAw0XYJ zB>z~@9A@&_sktSQSJAI%-nvf9Lz|wMQRuqNt)MIPS;Wd<`>wiA0d|BHof3EVvWKnd zssL_bqcPOOGu#Y+|A=czK7S5EsM3t-2Ny-Lg(wJS4oU;DjKV* zY84fZwbTx62k$<-Zh|MA@Ube6vc4ZwPcoU5C++^wiZ%|*w01x%TxRT-jlTL|?P-t? zKJQdlX~z3;!+0$K7Oz1AS?#A8;7z=D-7q`@jdaMYj&)$s!U~J&a#8V+j{Awtuc z&1{Z0p*+4H=%iIc5R(iIo_=Wn03ZNKL_t(tQy>3%Fh4$c|KV*HvWaS{d?V=kvMz2| zzhue!tqEJM?29j07jkpis-@rg-X%dv>dW?JyH81z#=tw8Z-5DDOGdla7y3)C!f^m zvOs0HapSb;S~wlLCQDdMrjQgF>d0M91~V1L8M;gjQ8Snt7SS2d(R|gbxPtUz2F}@* z@1w)W+utDM&O5SbD3-CQxa}Wp#ha&(c7RyrC=@kAE%tsm70_U|*=)vFy4WxfI3~O)>Eh&I>H7 zXl;1)FHIF__83(?Lw!{>;{y!?Emk#@%$b;?XFv;$YZLD#aGu_1YSeVuc(`rkNw3{3 z!{5{^l!LsHM=fTY_przE;f>s+?9%E@jqNIxLR~MD52$rba;=Gs@=|r+6Q5Cv?KGm| ziPlQ^buooW-k=c^Asg*hL$^lEB9GPqU?0}FMI%lrlri+Q=3of64Hygu7` z41-(bWw&7@M?}oVo8@73|2*EUZK-OXPNKuOq8Ah|1Lo&wr^Tu&jYea6Wa=NC7#Qi% z(GW|6Vc>a3w^SxBBBPyk9>AhS2r_lO(qT2!i|MUK zSx0T>P>n*>UfU%zDoe%DZ4O|Gew(qvA}zUs=2H6GPt~A;Aez2VLYyK>h~-OH9XOE_ zezIUq;ZXuxSv#u>*BqCG=MdPMiz4lA6W9G17GHSuLUvj@DCa=Wxj!)<)GwjoAtB+R z$0R{vYx2DHMKrSQ#b%5e=uVknPo0>Plo>~6AG)U0$Hc{=?yoFIEEVxK(UB;2rALVY zy6K-CCJX4{IB_#)1<@$O&>QFwOD|7INPu|Xm<{{ng6r!XdLDdagw{1#!on)!ib&fP z{~lTBAs8G`=(M>bGgV1tOwj74VqV1+orwgbmgGK|KVZ>+n?10Yn?z(t<2>W4B=TBH zE_bsqFeO(Ql{^)*Tq+EQ1qUxEm1?!R5nPnao^F*Xx;_EAn!7 zCUG+&GLo$n>w7^0mirH-`WGVz*p-?o|)M%=}YEcb6G=c(DCYAC=M;FV- zRWy2QuT*Bx!z5}MPj2rNvly!9GO4k?&uSWx@$iYi->b9Oaczg115+m2)m;<#fO=S3 z@5iWVz<+1QY5=^{{XDbLZa3@oFh)$<2Z3cFfQ2ATc`QJrI2LzCi( z?O|8TGt;2#Nh3!b_<{u1T|P1!V8Kn$`=ZGE{|>qB#FVGN3>v!ShgP0ik#{{Tao@J= zG*}f~I2ydS;AD8tiS1wJRBe$z6%V6U)nmgzk+bA=o=?Js#McV@J z9nL4uEQ2~>W>My^b5Ec5Wy5|dV)^H;jh$y)6%|KbMWNw@ z7#LP0ND*$zz|iUIKseLcB_aD5JMZ+mv%ksfjyyY;zVyLdpYqkaNLT!85qHlSs~T$nUfkBB@}sFby9~JDSuT4F2Q`sH zIOl5q!25gtOWXAZHF2lmxSMQ)%1Kg9jZ~SANEl8i7mq&Hd0uqQ2z!U>qt#_m2;7C6>8h0>A>cULuGz_)P>Ea}a=KXd_Ff zkZy07=nl~OL}j~KVF)5ZBq|3Yo!Vz+64H@1(!{l0X5L2$@Gnh%#6#7N>=K zRl01>JlK+@AMV;glC9lq_MIvMq-fjeFZLf?z2W1NM|SLbAJ`&s;Hw=(jJV+&00LX8 z{_wjMEB?BI{QKkYw-E5fVdH(vj-!<+V;E;?7Wy(%M<0IL1_Ltc+iJWWE^dj1RvE2mOOb>Kol zT@IZbtV7Oby?|iDl_EA#%lp87yXvKrAiqy!GQk`rt4Y78A1Jx@vB9F1m{T&3@{$aipwElhPsl3Ii<5gqTR&kbL>inlx57B{YJpzTuqm3&T_uv*`3eKuhn9&*Zw4%w$wV0FnYzTJq{93xf_saF zH|I%<)~&r5d*Jw=H|@Q!b1Mm3#d|h?^6l3IY?aOrww8o!p+`XzLSH32_{Hzs;wn|Ap&E9PQ@NM^7&HwR0^}Ct4XDgdS1@R&dYw?0XIx|?8o<;vt7Nbxg&E|R{Lr|Xj#xA!*t5(-KhJz3K z^g!9J9npAuRBTDT4sSEaoXvc+%dhBFlNzhuuF=?aXNR!hua!x(AjX*tc36fL_GIBx zv!bn@4MIXCjw^LKgoUEYF^#6JSvMq)mIp@L+lM>`x&2|p=+!f6Wh(cePb2sDHsQq5 zbxORi4fOfB5ie@k$fW?Qr3HS?#{gbvLT;VlN=;UBfw)}2%0fJC)s&kv`7AOxbSqLSi$WD@TD@ngr1 zpI?3W;HEX3e+vgJ0x(KD7hjla-dT?YV)D)$pBkC3ofdE8doTX8R) zd^V`$U;$S^O>=-GX>T(%G{jdkNVy#X)3bQx;+sD&bga+pfHnU-Atf(^(d2oo#egeb zn2`ZY7725e<`yK2(s|SUl@}MVSkm`Z-I^K->qlizG#_$VqAe4%!X*Wr%#Q8R)mm5r>ln%m{DsDj*lt9wkR$3QMFCkjJul# z2tK9bL%1t7Ky{o~wfQxJ%Cp^ah4t@g<#~y~si>a}f{?JkqE%~DICV@A66zgpT*tT7 z+iER7*?>dR+=hXM)C0XPnZ~0{ix_Ut5^(d_jqauWb-Fe_9!7^b-s?_W<6(wmegl->hV>htMIV$(b?|fgnY_AnvzRdG+JIN3UmiJnQ4%3xp`7&cP>e|1 zMXBUqj)*q3!fz`ET$(FN%O#!z7W|!{T3TNHkDr!(uoy4KLSsTu7*r@0CMPZ;yP~6r zgh*FuT~e|%jhB^FhobXC)&IZh&SVR+De(${AQWP-RiG4G+vCua;&XkYeE_fb4K?LQFy;%&gbrYPDMH!1bZk!xH^n4TfQ<)ANLUS{=BrY!0JFCc_LS z0~pF<7*#_0UX#&zchcj3f*hEO^RQLLZ$1FoVcl-TV$K7T~ zJCKU`G{Cv#vuY)gfJNNtA{d1b_$<{Y8lg*4L8}Mm4YE)XLb7?p)iQE9xiKjPDQVB2 zKYaP}$&)QlO7_0|?}6C3(vOxd{FW#6_%GvVvIWAW43dMa*-hFM;#RF)yXweMkYhW4 z_~2&HgrR6lX2vv7?Iy#jlSe7P{0wKnvIrYieta@*9nK2}EP^G~y`ChbJrDw67SfWI zUr{}SK7{n86O`u8W@eX#0P9SP@CtPQ$*I3xxNxEPbjda{QvLL^Z5Q@}-01u)9ZQx) zMY^yEut^Aop;TQCog5@sV-2NQFD76y3f`;33=pp>8xW*ksjqEscaJq$ z`vb-flSw|P)r0k_n|!j=-~*4-Y4;e79fkm2$rZe5n{pBh3t!-UR-@151@3sB`XJ((dubv@s_p~b|l@EJ{O{vjx zCq82Xn@2S?VDTG!Mmm7yHdSsMQrq|xU{R)oX}~f&Y$!*ZF1mr+AIL`@PX;|YiNmaL zs+zlfivBS#roc=-Fy?Gvw2{r|GNigJ7Q5LyJXDJ$rX@#qwn?f8Z`g%UO+R=Xfcp8_ z1iSnnZ`T&p)R{&PNr<6i6rxb#6@?L0kke>_U7%bC#7jWM3seLLDuOMkr=XBD385iv zB+V45iEtp%kV!}r3=ou?O{oDdYjxoo>l%GqICWT zpCkv8b9TOOe|zt5Q`mV9Dc08UZ;_>|zRb%^O4^yb@7Tu3Q>)J$ zKeXaCt#bY?;>v>XgP_;~>W7@B#Kfk|+}bY>A6r!ntUX5|P(dj`)}FktAW)T8M6KCR z0M)vc0fpm1K5Oc!bsnm!k(Vr>%{nBU0ayecR3-HGR%P$<7x1__cMi$9Gw3y*r69Yq zIk9y9k2|*R0B1RIcD!>TEAL|2#y^8D5T>Ed@6iboOiyvoj>+(3;UE!Q1ewye*R$Z~ zN?|{jRA8IJi0C%bYhMX zHB;OFvs|T;NaTGMOB+tBi3uzNq2qn#fAy`$LQlFM5h4kZPl%4Dq+q%oqmWkQFF%}T#t7}smvfF&8X zsuT?-`)NC z?#&>gl==gp%fmm8{-E&P!?ig?A(T^lc>DVm(H|4-&0C8AT0~hvpw?+{0XRk5yLH)) zWm~ttLmcAY%@>+-^7b7q-dGq};a)F6y7T7Q;^4XM;<8y@^LJ{I@A`U#JNuB(eJ)qs zzkiwPUp;v9+)2)zLHB0tCv)QIj4RKVEhFFxxRQV|Y4hHstlZiXkI{RL8&u{nMo(8^ zg`|T(7&nyu#qG;+lk(I~n6gc2ex8_E*7_@25gs!_)G1-+x*>}EuYsumsWHEX| z03@Liylsoq40?H`l-Wxp0kSDRyC}6Ly_rq>9awh|*!>FN6})>}Z|^sJNtb?1*CBKx z6$<(`p;b4glo0))MlWPC`wb7nzzY%&zD&XvG!*#QrZqe4a$M4iPYjL9`E->I)j3Sy$7K^%VGnN7j2lJT_G{x46XyQg zlRrFr6lTCiHEqKpk)_26HlJzr((YRZi_2h_n;`JC@;`-%Qt<00)eo=)87!YBkxGx{ zrnomS(UViYzxVCUuRoi$6nby%!8H}+WIib=Gq0q0!{;Y2tv|4G>1&r-enUT4WMw98 z-U{oJ<;y?(&|9QHd@MM$;uWto73dyUJ!QoTspI`^+K|v!Uo~SVA)WIpi#ns-@#EYH zz~aQYXL;!q2K~3^S-GE-5cMH(%D{d^k5n`ZfXay(2Avrt*EAzxa2lsh;5F!6A*c0j zM5EASSzzA@CEl~!FJgz>jDmQaGBG`JMX8|0sTVIlK%%_ExacL`Qddjbq#{W zf-AV&IyA6*r(w(0lo$fD_;lK$vhHjXE|*JXig|t%1h(^%>lI3aZW!=LV(qX|=WOpE zMw#em&4`T*+T>D;Rfvi^tT#F^^zXm?y5*m@ z!~-g`P^fmv4MQ%MVNgX?$Rs9{!d;P1d%I@<7V#GIylapJyRWipJi0Y{LghUD+sor; zR-cMIQnoKQ3-*U)WPkW_Vf`T#f_s6AB-4p!8*o&K@Y&AGCk}WFllJc2dtv7usM@NC zI{O72B+h>=Z&pKNjZa{;z_UBVJtSlW7!taL3xE9wYg(%^BU=W#!pT6zkZr7F(pDMy z$7rGq3=a@YfkS5_fYBy1lwx82eY+WX8-jEVM0HLVIZ z1)(V&RwjW9FA{wJ3_;krS>#y6F|`bdQEiW-ODWah((dtgEh-+e;lapssk;yzm6B3- znN559hsV}oAtn|MTdkvdoId5=;|!|N0lR|y)v?q*hi^V$+^aj(YmTr9lg*sG@-Y z2>=y*loJW~%gn94czD%z&^(=tijIyt2`nwZfU_qH;8;%n#betyloNPxX6?F_OXuXT zKO;XWN&`sU^C4r&D1Oh7uqrgbkdT}cR&lr~p@QjvHO0Ayxb+_K0Q^H@W9s0`q;wW| zDo#&ay*IlMz%mqL5U`w>v=a^wj*&4IWGYy}%ePk`K1RaEh!o-*)mw6TeL3z8s)9P+ zZiK?-USDx&-GQ~I5AI(LTuFtco`^&bbh*vfuq!1X5)^j@pjdrV=t35&CxlLV?t=6U zvr+B7hg%suw{VljZcd0ujkp{e0J9cAVtH71ynBGjRKsPdfq(B!bs(@E;j3Im3>Dw# zX|uN|Rk%i`(Yw@4=8%lM$0$e+lXD#Hkhwd?Udr(6ba^;Y}i$=d{23Dv_yl?T{!?_?2ax z^z&0mDvy=ctP0yj1-bYL8NpAR6R?u~KRn=pcGn!|xL4>t3t}T)RhCe2`cl+K8;VZ= zo;)ct|3vYIkD|^7>JPm@wceACTzs^o7L3tIOAX1U7nXZTD)2Nx-(dKev(XiirSBfy zmzUE-z*g3t+LE%3g`b~Vvw!{Cb#DiPEfTP3yK7?7d{uo7G0AkFAtA3JFz3F7)0xs) z4+NyshlJC0?gS=o;ezdT*_PK#<%Au)wB{sCuwMmNW)OgqNEVb+CD&@c4~3Nt zSXNIY198oCUkZ!Hpk%OO1=MD8gQ`av0VAjhfP{KfPi*4OCRn*<=H`D@LR{%cY3Y%2 zx9jr_sxD;FdO~m~{;7tOKz((%s+OIInP%&aRKI;q_jQuV>KTkadTuNy-0M zySkXP(lkurZ~~K@Np)LNy6N_3SZ!T@&T7GFbfhvP{h5~5I!>qVw4HR?nP@wlDe3x0 z0gbLI~+I&;!o#X zG^vSlIGpqFzR&wS&l?!l6FQfzFAI59t*k8EPw^<*3@E4!F~7+u#h@=p)IC1xB#3F6 zsp2WKX}Dca27g)xJtFPUa13S@>7^s(j_F9x0~%CUCKGK91SUp%^}1jL=X2jz(~<{D z-Qj!78+`r~p_?689CFROhND59A!bdSHP!R{h5r782D*0W;ppgu)lSnRu6w!{uMv?S znh)yq?bBnkPJ<^%_D*TcVP+}Xj4|5l@jy0y#>k^YW}7weaIMOe zm#2Us&Qw)vsk(+MTpLtwV7|vN@mI<|{o|3Ymhz8IeZR9F^oO_6^gacsppNP)N*)D$ z3wTsPjZxB4g6t_5nWQYp>g?YBNhX0pN=m~`(20WI{P5V_y7To{uE7-2jOj8KMwoZw zdg@$-QVu3jN8BJ^bw~*CO43$Z!B@!|Ix}QuL1E#{7IBt@=W%>FaoqYq*+AjdD|hrH?2EgYrt})B?Gn~& z@KPl$Eyq8E)m_;a=NQ9!ykUfSUF*j#-Y?h_!Z%i(yFPuxns9}T2&(nCk1Ur|SPT;6 z!mEL3)E{tA9En*5IhHv}5W$DONW|yz`~45YVXuKC4X*!4xgp89OF<$yVg|M4kCl^w zIimMr#7^6cZU^n3C4w`|YXFeq<3GOuWI&t0T8PcK9S$>YpEfwhrsu+o_k%j!tLLcM z#chuVJnijzohSTgwAT|cn@5B9qT%q6Ni#78J=11p*R1%Cy0o=Q$fFvys@x(mjPgoC z)24`!{{kAaThuc@*`z@2@2Ng~wT}MvAaG4!&h3fW`Z@939 zR1H|0WaYSAtX4=lLqau|bLR%f0t8Yv_cY}TgokBnK{n1Eo#KIZvoJ1DTkW%viiMOcI>!xj%CxkpxFj@E|+8|p))0CBJ=YG=AH_G|1*b)kbx}{h~q7B0}Jw<~Gc~Jp}&{}VM zYO(yOSoRMQ`12upe1ITkX9Bb>)SXUVsTB=}+2s!6fQKN)Ty_o{V{&;(!mz}elljJp zu#O;~k|a6iGjV5V+H^UVWxSWO!x|!tK~IogQ}fn>oMtK8Jv|>Foa4qKg*;!-O%D({ zuix(02sKjo(irK9ICwmfJK&ptG&K6au<*j^@i?Py^Z0Nq>V=YauYcqvV-5n>PUEoI zOtwCaNGgI|r$Cq<0uRb7Et7X9R7y6CGSJhMr)(cpaMJ(pH7n&rc$SR)#SdEg+ zu&-OTK%v%&J>Pw?^VYGh11K1|>mzW$UgtQdg-}VgGrgqRM3Nt9tIoueW%icEi>;O} znEn8hlNoAj+ph#7tY=b@{&n4hSa)_vSde*0NaZNxd4l+sdSP`_9M|wnoIB3zlr=H( z81SG=&W+y6)D^1au*;>n4CMNYj7!_3F^Y`-OW#aOFGHx!Lp$>xC<}24)ZN zfJH!~^vGrutOlwOv-*|F{`}^+sx$+M1$}BOz**C6_suUYFAaskaeu%`5PHvSFlKj% zQctI&S}y0T`lD=f1aWV~mQ3Kn97|CEGQ%*!6T)gA9d?pB=d1gpLoV9P{aYQ9G&z(DWHebLOOIWTM@(jI1C^LPrNr^1Un=MkEspCG zg=#rZSja9Z7F6Bo5Uc@Mas*hIV`R$2{F2aOv8bLr$){G{Ubwb#Gml!FPri1ke&@y0 zAC{oBCwRb|moDh|t-tgwr=)u4ZRhGJRrEJ#O$92pU?5ft>=94jLP35PPDt?{#bky( z2b$6fJZ(tG+`UQqH%|^KCLEV2kynqZC}pm7M$TQp+yCT=iv8wkEB)K%-y6J|7K}ZQ z{@i$q;0j+3yNQso+~+q*D%UPjLFW$8>hiwNk9D<_ANzdYfzv zn45uqpuR{9RAGm4HQc7Ri!f%VMl~#Y2}aoY=LEV^D}QT}T-LNqQ(I zIfu4KCRzGS&~7MfaRI8g|dk+Fbv1@ z{!iQW1vPQ6(U@d+LcQq~I?{=?#kQ$fE+7{NDg%OArAjqwZK<_-wU=s(cCxU8AW4%L z6R{~V8e63Tn?)TeWvI#-K;qHSE zj3G-ld-nUzcfND-%Cq}_z21J8LDDv@Ce%B2rz;Za8}IQ9hSY5`N#X_-IyEpNyM2po z$Hd6)&NjvcEMlv?I5n9x!=n_NdVL(gvW)ntzAo-WfF%$O##BY!j_`ET+;1_FS&?0t zO1o=y=XE*?s{m6ZYOkLZM<+Fy+7owlh4n^*L9Y)#v)#Ub4~7f4W^&X;wS+MjsUnr6 zDd-6Pg;r5#K>W9;rDVakV^5RYDIm(oW4s`Zg64hjeJZlq=SRqXiQ-XDs zm68{H3re7WA=Dnyb_e}xtKT&x;wt!5pEb<^wsDJ^VYd4R2dma%Ac;r*Y!sNH+6W&J0g9zOQL z$(3KvCzqQon3)J9x?e0~qS`jnml&xqXqx^-GN)JU2@xBd)?ox+Xf=e#`-YCqHQ}5YAKAUtg1=@MdZ$uww)Fx&q50uv$CS)>6I=}+|(^2Jdhg`;@#8rcE`t#RG>IRztVc`J$G{kQ$tx|EkKYX`^{Ci7^t`{SRhT9) zQUI|MY$jpaAtAB7sxcGQQynMraQ0`}6ozpDYnF587o6BqO@F7T>nQx=G00zkJn|(Y zDnoSLB5><9x^N=aR#Y*G(*pKdrtIYiae}uZ_s{9ak87YA-Uj20TIK?P)AI4RVNMIW z6F{qNE$cu2an-xuy_NIM8_O5k>Z1~xTXZ2QJuM$Y(Df3T(<{sgnd?!IB~Nw)Uf5(X z`S#*k%n)kxV3ewT`t<5V=Z|4NpU(n@?p`Y{^7Yz#ItK=ZZ3@VyDpE`q7uOf$YQlF$ zV>T02hZIo<{9}vuO^!I#teV}Y){jk0O#U{4jtNhY0xbKW+2~{b19o;xMOFUXd>~yf z)ri@-_pHPVFOHfKV72K7P1NZIoD4{${&9&>5OYk}21W-1;I$X%9qj6~DT`ubMm|r} zd6JDR zb{fU2pDn54>af6^x>=G6ScF#N#OWPhLfvKS3?t&>;qOy^uU>DhM?28CNMiJk*vGt3 zjLL7Or>HLMN(|sHouZ9XkU(K4n_$JLwZWB&xj~mLhee)~V73W6(=9EqG+p*?!@o|# zD$i?6zS_nnRP0*)!NXshh3Reu9D=)q?2xb^C6kcGE1Ieoa8v35K(;6Qxzn6Go-ZxW zkW1N0x%H37s4HeZ85oE{peo?ULN>OM-WA?qU41Fr&(5=#!;@s@%loIF|F?|Qpu%*c zg)MOrUETmnuT@9(Y^2ZF1PfrJ*9u zzw(==@BKbR)l5YyQ+F&D>^3QsVx^K)1qVY;0eyW$KCRwhFh9{iiiJ}jxFvTG$4jpiyW=-_R7Pt2x>#yt8qN&U@KprXE{4GE*OFELmiNfOSiM6Oqzr{5L3RYOUBYwNxKi=2p z7><%kvC0u2u`t2~K9A4mZ!a#)07G82*F%a)k48I7UPhSBy%c%{CjuR`09GJ#dhG$u=rKi(r885wR79P*imDGONHuki>7mUdEJkzh znBD*M{-0Q%2mO!6L(LHQZn#PTi?3>Jd z9hr>-P}OmOiqC9$>kUuZ z?L9;BIQ2*dEK*V>(zwQoQx%SFwwO|Mk6J~z-XC$P0J2EiK$qJccKWRPQ4=Xv+<*4S z9yS<_8og1i58Hz-3X@c$Ap^IMDxrByi+PfIn+V-_Vr$E#2j`Givvw^G3Xnn?8AsPJ z5h2CO9^ht#0;?fT)oSq`!;?ackZ3`wIouP{a#)~ht{jW7gzRxgEayR_RR9yst?lj2XBYAjUPRHmi6igo)_Lk@HuW#Bq%=Q`bq3u^^469% z5h>CaS!+8IiX{OSQPG5)zzPDCI;Ealp=IQs_(hn^x#R!i?P`OX%Fb}iO>UtzU2v-&+|UdRSE1%5>#3? zc#QbQ*oO14wbsB4|x`9JVVFKB<(^xJ$#!F<2rYyBhE- zbEv!DsMYEW00UIu?dX*DR-?4BCci2-*H7vP#)iOE(fDL?UM8t^*Si1?_IPa=hktS7 znM}xVjP!bYC%c`XsvGiX`E$v!xH?j= z4~%t(LZhScseoR?@Kzq)dVSf=t6WeIv`Fc0S!;f6VfFkutVAs}Yr$FvEXxX5#Kj6p zO9nnk6w%%=U?*l-A=Q2n zx(;*HrSbc9-k2h$zxN~ssG$0D=WaAEo9+zv=F)}4pn_FJhkc=NzD+W^skQj^AsT&_ z%d0XBjqqxksHQ+Jo7l@`ws~v^Ywf`0!NHugi_4mrVv=?$&99WUQX5Y`EgO~IN%PNP zLNiWK={~zqe+w3D+Zi40=WnO!Xs~b$ef#(CgH5h&pD|lcJ@Rfqdk=i1Uoj!BuZ~=tMXCuiYszy= zq>>#67YJ7zj*!$on@Ghe6_Om7?LDJL=$cM=6LE(Rd?;3ScJ=FpJfRT)27KGo4=Obt zc$~pl6kOn&A`N);jq81VPoF*|Ns~7SJqZ+1r-ev1@He9H77j+4EJVm0PLZ^->{^yv z=eZSP#mK1M$|9CL2z3o%y;Wy5%#NUhCp z_2;DIP~b(Ck{UE936;>ycU>&Z%fB|Sl|HnOD)KMKM#N;wR3ofq7Rj1wTrP$k+c`|>eLf?i7jR=eoy3`Pw< z-}@G&t7S(jX}*PUi}rj*jrrBzgWJFw!N!RvEgPuKizktSLNI&S&61^iSD#&500mdF z^V=>Qd%xfWU{as1f=w>gxuKqJ&!fHQLb|XDo-ESE;*!?{Ebi-5EPh>C)87wPFVs63 zx0$AA6l!RX7xg(}F-I&6_>^MUo#+ST305D*fXfq(!kkcNN5rPl8M_Bzbc@q%@l!5$ zu+1d)Ij6KJ+9A*zoSuF-Juo&kYK9UC^Ozy2wW_7a2SElxfH)11|7tnYoZa8+awMt- z)n|uS>mC#PaP3h~5@S^)z`JBP-{ESfZTtUSqVITlJq zO>YLaux@v}vZmJL9)Vpe%qzS!56!A-No8@>B5DEfGHf|SaZUM(%!zG83FD?n}O+t>|DKLA;%elBdH1VRhN8uQb zIbi^+z#jPaIdjTXa5^PJeGy&X=zzlIdscn)F@1a}I}d{z-Z>8I!_|itfoff*nz>cQ zRWHwsw9;o47e%uU39H)5liLY~-;&6=>DLNdXz@AfxmQa?SE(jfp$Pf_g5vFllwbqxa*ugnS=Ao0Bz=`Wk2&db-!u5eWop|$1#OgAFtcruoe6}8xFkx`T9K{ z{A}ruSv5N~{fykQ4$y%f)ZUaV7LPOI&p*&~=7-oJERy|MNGadj#o_kFzN{H+?o@?xKsKjF5 zU_Z1l_FBBX!43ytSjy0NtXpq`m~RSr2E*Y8cBL}vj3>OL5Sveq^^!eA>6yw>c3&te zZ%;kws46Zj%$tX1)m2K9?XFA9D)^b7EymApl$2eX<1eO45Xy6*y!MJ*r_*r3aTj94%n1b>E@|3X9(qXF-b&7)4>o&y-^CFxRLbz&#q4PzXr3=lc5L*8-9rSv=7QJ4tC2gia z1ooaBMiOVAJ3uT`VkipmM?E+^ItuuWI6M~6`RUv8$Kih8=^N>R%`gRQhJg>n6L;@U zz+y8-TJe;wJMAztWfn|4esrsNKAa_Vmc)=*pi+1C43BqrkB^6(LuM6qF3=Oy!t+3( z4&3$Bv#!v1A`;MAg_-17ZCSpDUwcqeQI~`lP+ZLVkE4LFY%~jCVsc;r6IJk&&z6TJ z-L~*j$>zBOGElRg$TjHv^6K*G$*$pH56R_`k#Qf)^|B96nR0kS(^y9$5{!gYBVLVU zMK0imeb1DmB>Fz%klKyNrF>F7GCl4XbbF?H1KvSpT=V3~@sGY*erRziRW{IKqM%VH zUbdwk#(Wik`f$y=3zBM~ah>hUGCG25=MYHOW&jRqofjCc|ch0K$=2 zpt3uANd~WSh=f9r;ROmbgjqvIL((vnb(nE=PTUO}b@Q9ka1C1ba8wDq{%WOKr4qBv zHia*!l~W9fXC-fw8XdU*jNul^$@~n6c&SVzE#ap;i2U27yxChsRdHQm;kCJW8owq* zK0&)EP=KYgSWH%A$b+{xi`ugE>mXoZ+K4+o=`}rlF`*JD2DOB3U__ztIl^|O+Bd1? zVFprT(-=s2O2`L*MHET;JnB&!T8tK-*=0Zkwo4IubkC;2x!lWESEqeI_x$<3b#Qw= z^yW`~^#6FJfY!QWU%*bhY}2;gFjDmttPfwI6;H`VKltSA%C$!f>?%kHEc7wuz&$II z5U*%T^^xYL4CW+%-8{~Jc{{tHrtUP1XGqQ&;*^<$PFZ7d1s4%0AE%+jGJHf(P^gV` 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�)u z-<<{!PS+hDJYC=H-uDiBXbXPW^UEtG=8L{phM?<~?!q9eCS-nc<$eq+uVA{&C#}^Y zv1!o#31nZ4R+I#iNqZMt>Zd$mm(BZ%wviYt`(_xR;D0~nBcRir!3KTdAldRKC}Gc$ zWU~Fh&!o&}SfOu@aVE~k*#=)p(F-||CY`ugJu@fMq0@Cv^<5M44=Pk}JI0t}`W;Ug zku(qKsZHTxV@`*yY?qf@n@y=*W?9m&bV|W1|42|Svd;1QCFJ;UvZlUOo*e`@-Rd}X z%!8#kK-DbJ62@PRFAP*KGyMe=>Q1R!_RE@0kDkC9)IOC&>=HytWp|^dU=S(W{8s() z^&6@!GeUo&ZQfxA_Mm!_>{lzY+MmX?TD$X+eP;;z4Ehz^{36$R(30aLJ{zt42l1nh zDQ5l`0Me7QW^kk>ICH>?$+F(Vo4n?gzEys@aQHe|#}KmSW5C z-H@L%0I%;0Uap6r*Dyvw={otn!XRU8P^qNNzR3g>E3V+ps34R@sH9^H-PoFCS?4u4pPxypW8K4z>C1t&=m_dS#Ya0~AL=3!zI+=iv39H`D2Y8;%eL#K kt5%f92KB*sth^&C5dCz`SrM9m3Id}j`=3mu6eRHf0Ok`k^#A|> literal 0 HcmV?d00001 diff --git a/doc/perspective-correct-textures/index.org b/doc/perspective-correct-textures/index.org new file mode 100644 index 0000000..59d0760 --- /dev/null +++ b/doc/perspective-correct-textures/index.org @@ -0,0 +1,220 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Perspective-Correct Textures - Sixth 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +#+begin_export html + +#+end_export + +[[file:index.org][Back to main documentation]] + +* The problem +:PROPERTIES: +:CUSTOM_ID: introduction +:ID: a2b3c4d5-e6f7-8901-bcde-f23456789012 +:END: + +When a textured polygon is rendered at an angle to the viewer, naive +linear interpolation of texture coordinates produces visible +distortion. + +Consider a large textured floor extending toward the horizon. Without +perspective correction, the texture appears to "swim" or distort +because the texture coordinates are interpolated linearly across +screen space, not accounting for depth. + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[file:Affine distortion.png]] + +The Sixth 3D engine solves this through *adaptive polygon slicing*. +Instead of computing true perspective-correct interpolation per pixel +(which is expensive), the engine subdivides large triangles into +smaller pieces. Each sub-triangle is rendered with simple affine +interpolation, but because the pieces are small, the error is +negligible. + +* How Slicing Works +:PROPERTIES: +:CUSTOM_ID: how-slicing-works +:END: + +The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.html][Slicer]] class recursively splits triangles: + +#+BEGIN_SRC java +void slice(TexturedPolygon polygon) { + // Find the longest edge + BorderLine longest = findLongestEdge(polygon); + + if (longest.length < maxDistance) { + // Small enough: add to result + result.add(polygon); + } else { + // Split at midpoint + Vertex middle = longest.getMiddlePoint(); + // Recurse on two sub-triangles + slice(subTriangle1); + slice(subTriangle2); + } +} +#+END_SRC + +#+BEGIN_EXPORT html + + + + + + + + + + 1. Original + + + + + A + B + C + + + + longest edge + + + + + + 2. Split + + + + + + + + + + + + + M + midpoint + + + + + + + + + 3. Recurse + + + + + + + + + + + + + + + + + + + + + + + Each split halves the longest edge at its midpoint. + Recursion stops when all edges < maxDistance. + + + + midpoint (3D + UV averaged) + +#+END_EXPORT + +The midpoint is computed by averaging both 3D coordinates *and* texture +coordinates. + + +* Visualizing the Slicing +:PROPERTIES: +:CUSTOM_ID: visualizing-slicing +:END: + +Press *F12* to open Developer Tools and enable "Show polygon borders". +This draws yellow outlines around all textured polygons, making the +slicing visible: + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[file:Slices.png]] + +This visualization helps you: +- Verify slicing is working correctly +- See how subdivision density varies with camera distance to the polygon +- Debug texture distortion issues + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|-----------------+--------------------------------------| +| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html][TexturedPolygon]] | Textured triangle shape | +| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.html][Slicer]] | Recursive triangle subdivision | +| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.html][Texture]] | Mipmap container with Graphics2D | +| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.html][TextureBitmap]] | Raw pixel array for one mipmap level | diff --git a/doc/rendering-loop.org b/doc/rendering-loop.org index 274851c..f0d0d18 100644 --- a/doc/rendering-loop.org +++ b/doc/rendering-loop.org @@ -38,6 +38,9 @@ frames on a dedicated background thread. It orchestrates the entire rendering pipeline from 3D world space to pixels on screen. ** Main loop structure +:PROPERTIES: +:CUSTOM_ID: main-loop-structure +:END: The render thread runs continuously in a dedicated daemon thread: @@ -52,6 +55,9 @@ The thread is a daemon, so it automatically stops when the JVM exits. You can stop it explicitly with [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#stop()][ViewPanel.stop()]]. ** Frame rate control +:PROPERTIES: +:CUSTOM_ID: frame-rate-control +:END: The engine supports two modes: @@ -64,6 +70,9 @@ The engine supports two modes: renders as fast as possible. Useful for benchmarking. ** Frame listeners +:PROPERTIES: +:CUSTOM_ID: frame-listeners +:END: Before each frame, the engine notifies all registered [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/FrameListener.html][FrameListener]]s: @@ -80,11 +89,17 @@ Frame listeners can trigger repaints by returning =true=. Built-in listeners inc - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.html][InputManager]] — processes input events * Rendering phases +:PROPERTIES: +:CUSTOM_ID: rendering-phases +:END: Each frame goes through 6 phases. Open the Developer Tools panel (F12) to see these phases logged in real-time: ** Phase 1: Clear canvas +:PROPERTIES: +:CUSTOM_ID: phase-1-clear-canvas +:END: The pixel buffer is filled with the background color (default: black). @@ -95,6 +110,9 @@ Arrays.fill(pixels, 0, width * height, backgroundColorRgb); This is a simple =Arrays.fill= operation — very fast, single-threaded. ** Phase 2: Transform shapes +:PROPERTIES: +:CUSTOM_ID: phase-2-transform-shapes +:END: All shapes are transformed from world space to screen space: @@ -108,6 +126,9 @@ All shapes are transformed from world space to screen space: This is single-threaded but very fast — just math, no pixel operations. ** Phase 3: Sort shapes +:PROPERTIES: +:CUSTOM_ID: phase-3-sort-shapes +:END: Shapes are sorted by =onScreenZ= (depth) in descending order: @@ -119,6 +140,9 @@ Back-to-front sorting is essential for correct transparency and occlusion. Shapes further from the camera are painted first. ** Phase 4: Paint shapes (multi-threaded) +:PROPERTIES: +:CUSTOM_ID: phase-4-paint-shapes +:END: The screen is divided into 8 horizontal segments, each rendered by a separate thread: @@ -157,6 +181,9 @@ The fixed thread pool (=Executors.newFixedThreadPool(8)=) avoids the overhead of creating threads per frame. ** Phase 5: Combine mouse results +:PROPERTIES: +:CUSTOM_ID: phase-5-combine-mouse-results +:END: During painting, each segment tracks which shape is under the mouse cursor. Since all segments paint the same shapes (just different Y-ranges), they @@ -172,6 +199,9 @@ for (SegmentRenderingContext ctx : segmentContexts) { #+END_SRC ** Phase 6: Blit to screen +:PROPERTIES: +:CUSTOM_ID: phase-6-blit-to-screen +:END: The rendered =BufferedImage= is copied to the screen using [[https://docs.oracle.com/javase/21/docs/api/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping: @@ -193,6 +223,9 @@ buffer (common during window resizing). Since our offscreen not re-render. * Smart repaint skipping +:PROPERTIES: +:CUSTOM_ID: smart-repaint-skipping +:END: The engine avoids unnecessary rendering: @@ -205,6 +238,9 @@ This means a static scene consumes almost zero CPU — the render thread just spins checking the flag. * Rendering context +:PROPERTIES: +:CUSTOM_ID: rendering-context +:END: The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/RenderingContext.html][RenderingContext]] holds all state for a single frame: diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java index bafdb83..8737a28 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java @@ -29,7 +29,7 @@ import eu.svjatoslav.sixth.e3d.math.Transform; * camera.getTransform().setTranslation(new Point3D(0, -50, -200)); * * // Set camera orientation using a quaternion - * camera.getTransform().getRotation().setQuaternion(Quaternion.fromAngles(0.5, -0.3)); + * camera.getTransform().getRotation().set(Quaternion.fromAngles(0.5, -0.3)); * * // Copy camera state from another camera * Camera snapshot = new Camera(camera); @@ -229,6 +229,6 @@ public class Camera implements FrameListener { final double horizontalDist = Math.sqrt(dx * dx + dz * dz); final double angleYZ = -Math.atan2(dy, horizontalDist); - transform.getRotation().setQuaternion(Quaternion.fromAngles(angleXZ, angleYZ)); + transform.getRotation().set(Quaternion.fromAngles(angleXZ, angleYZ)); } } \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DebugLogBuffer.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DebugLogBuffer.java index 59e5f2e..2d56100 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DebugLogBuffer.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DebugLogBuffer.java @@ -10,13 +10,13 @@ import java.util.ArrayList; import java.util.List; /** - * Circular buffer for debug log messages with optional stdout passthrough. + * Circular buffer for debug log messages. * - *

Always captures log messages to a fixed-size circular buffer. - * When {@link #passthrough} is enabled, messages are also printed to stdout.

+ *

Captures log messages to a fixed-size circular buffer for display + * in the {@link DeveloperToolsPanel}.

* *

This allows capturing early initialization logs before the user opens - * the {@link DeveloperToolsPanel}. When the panel is opened, the buffered history + * the Developer Tools panel. When the panel is opened, the buffered history * becomes immediately visible.

* * @see DeveloperToolsPanel @@ -30,7 +30,6 @@ public class DebugLogBuffer { private final int capacity; private volatile int head = 0; private volatile int count = 0; - private volatile boolean passthrough = false; /** * Creates a new DebugLogBuffer with the specified capacity. @@ -45,8 +44,6 @@ public class DebugLogBuffer { /** * Logs a message with a timestamp prefix. * - *

If passthrough is enabled, also prints to stdout.

- * * @param message the message to log */ public void log(final String message) { @@ -59,10 +56,6 @@ public class DebugLogBuffer { count++; } } - - if (passthrough) { - System.out.println(timestamped); - } } /** @@ -95,27 +88,6 @@ public class DebugLogBuffer { count = 0; } - /** - * Returns whether passthrough to stdout is enabled. - * - * @return {@code true} if logs are also printed to stdout - */ - public boolean isPassthrough() { - return passthrough; - } - - /** - * Enables or disables passthrough to stdout. - * - *

When enabled, all subsequent log messages will be printed - * to stdout in addition to being captured in the buffer.

- * - * @param passthrough {@code true} to enable passthrough - */ - public void setPassthrough(final boolean passthrough) { - this.passthrough = passthrough; - } - /** * Returns the current number of log entries in the buffer. * diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java index c209fc1..8c1dd37 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java @@ -17,17 +17,18 @@ import java.util.List; /** * Developer tools panel for toggling diagnostic features and viewing logs. * - *

Opens as a popup dialog when F12 is pressed. Provides:

+ *

Opens as a popup window when F12 is pressed. Provides:

*
    *
  • Checkboxes to toggle debug settings
  • *
  • A scrollable log viewer showing captured debug output
  • *
  • A button to clear the log buffer
  • + *
  • Resizable window with native maximize support
  • *
* * @see DeveloperTools * @see DebugLogBuffer */ -public class DeveloperToolsPanel extends JDialog { +public class DeveloperToolsPanel extends JFrame { private static final int LOG_UPDATE_INTERVAL_MS = 500; @@ -51,11 +52,11 @@ public class DeveloperToolsPanel extends JDialog { */ public DeveloperToolsPanel(final Frame parent, final DeveloperTools developerTools, final DebugLogBuffer debugLogBuffer) { - super(parent, "Developer Tools", false); + super("Developer Tools"); this.developerTools = developerTools; this.debugLogBuffer = debugLogBuffer; - setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); setLayout(new BorderLayout(8, 8)); final JPanel settingsPanel = createSettingsPanel(); @@ -86,7 +87,6 @@ public class DeveloperToolsPanel extends JDialog { addWindowListener(new WindowAdapter() { @Override public void windowOpened(final WindowEvent e) { - debugLogBuffer.setPassthrough(true); updateLogDisplay(); updateTimer.start(); } @@ -94,7 +94,6 @@ public class DeveloperToolsPanel extends JDialog { @Override public void windowClosed(final WindowEvent e) { updateTimer.stop(); - debugLogBuffer.setPassthrough(false); } }); } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java index 28097b0..5645d25 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java @@ -7,6 +7,7 @@ package eu.svjatoslav.sixth.e3d.gui; import eu.svjatoslav.sixth.e3d.geometry.Point2D; import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseEvent; import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager; import java.awt.*; import java.awt.image.BufferedImage; @@ -43,12 +44,25 @@ public class RenderingContext { */ public static final int bufferedImageType = BufferedImage.TYPE_INT_RGB; + /** + * Number of horizontal segments for parallel rendering. + * Each segment is rendered by a separate thread. + */ + public static final int NUM_RENDER_SEGMENTS = 8; + /** * Java2D graphics context for drawing text, anti-aliased shapes, and other * high-level graphics operations onto the render buffer. */ public final Graphics2D graphics; + /** + * Segment-specific Graphics2D contexts, each pre-clipped to a horizontal band. + * Used for thread-safe text and shape rendering without synchronization. + * Only initialized in the main RenderingContext; null in segment views. + */ + private Graphics2D[] segmentGraphics; + /** * Pixels of the rendering area. * Each pixel is a single int in RGB format: {@code (r << 16) | (g << 8) | b}. @@ -103,8 +117,8 @@ public class RenderingContext { /** * Mouse click event that needs to be processed. * This event is processed only once per frame. - * If there are multiple objects under mouse cursor, the top-most object will receive the event. - * If there are no objects under mouse cursor, the event will be ignored. + * If there are multiple objects under the mouse cursor, the top-most object will receive the event. + * If there are no objects under the mouse cursor, the event will be ignored. * If there is no event, this field will be null. * This field is set to null after the event is processed. */ @@ -119,6 +133,19 @@ public class RenderingContext { */ public DeveloperTools developerTools; + /** + * Debug log buffer for capturing diagnostic output. + * Shapes can log messages here that appear in the Developer Tools panel. + */ + public DebugLogBuffer debugLogBuffer; + + /** + * Global lighting manager for the scene. + * All shaded polygons use this to calculate lighting. Contains all light sources + * and ambient light settings for the world. + */ + public LightingManager lightingManager; + /** * Creates a new rendering context for full-screen rendering. * @@ -160,6 +187,8 @@ public class RenderingContext { graphics = (Graphics2D) bufferedImage.getGraphics(); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + segmentGraphics = createSegmentGraphics(); } /** @@ -182,6 +211,9 @@ public class RenderingContext { this.pixels = parent.pixels; this.graphics = parent.graphics; this.developerTools = parent.developerTools; + this.debugLogBuffer = parent.debugLogBuffer; + this.lightingManager = parent.lightingManager; + this.segmentGraphics = null; } /** @@ -194,6 +226,58 @@ public class RenderingContext { currentObjectUnderMouseCursor = null; } + /** + * Creates Graphics2D contexts for each render segment, pre-clipped to Y bounds. + * + * @return array of Graphics2D objects, one per segment + */ + private Graphics2D[] createSegmentGraphics() { + final Graphics2D[] contexts = new Graphics2D[NUM_RENDER_SEGMENTS]; + final int segmentHeight = height / NUM_RENDER_SEGMENTS; + + for (int i = 0; i < NUM_RENDER_SEGMENTS; i++) { + final int minY = i * segmentHeight; + final int maxY = (i == NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight; + + final Graphics2D g = bufferedImage.createGraphics(); + g.setClip(0, minY, width, maxY - minY); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + contexts[i] = g; + } + + return contexts; + } + + /** + * Returns the Graphics2D context for a specific render segment. + * Each segment's Graphics2D is pre-clipped to its Y bounds. + * + * @param segmentIndex the segment index (0 to NUM_RENDER_SEGMENTS-1) + * @return the Graphics2D for that segment + * @throws NullPointerException if called on a segment view (not the main context) + */ + public Graphics2D getSegmentGraphics(final int segmentIndex) { + return segmentGraphics[segmentIndex]; + } + + /** + * Disposes all Graphics2D resources associated with this context. + * Should be called when the context is no longer needed (e.g., on resize). + */ + public void dispose() { + if (segmentGraphics != null) { + for (final Graphics2D g : segmentGraphics) { + if (g != null) { + g.dispose(); + } + } + } + if (graphics != null) { + graphics.dispose(); + } + } + /** * Executes a graphics operation in a thread-safe manner. * This must be used for all Graphics2D operations (text, lines, etc.) diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java index 7f26423..f01b2a8 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java @@ -25,24 +25,28 @@ import java.util.function.Consumer; public class SegmentRenderingContext extends RenderingContext { private final RenderingContext parent; + private final int segmentIndex; private MouseInteractionController segmentMouseHit; /** * Creates a segment view of a parent rendering context. * - * @param parent the parent rendering context to delegate to - * @param renderMinY minimum Y coordinate (inclusive) for this segment - * @param renderMaxY maximum Y coordinate (exclusive) for this segment + * @param parent the parent rendering context to delegate to + * @param renderMinY minimum Y coordinate (inclusive) for this segment + * @param renderMaxY maximum Y coordinate (exclusive) for this segment + * @param segmentIndex the index of this segment (0 to NUM_RENDER_SEGMENTS-1) */ public SegmentRenderingContext(final RenderingContext parent, - final int renderMinY, final int renderMaxY) { + final int renderMinY, final int renderMaxY, + final int segmentIndex) { super(parent, renderMinY, renderMaxY); this.parent = parent; + this.segmentIndex = segmentIndex; } @Override public void executeWithGraphics(final Consumer operation) { - parent.executeWithGraphics(operation); + operation.accept(parent.getSegmentGraphics(segmentIndex)); } @Override diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java index 0e35b3e..3377919 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java @@ -9,6 +9,7 @@ import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack; import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController; import eu.svjatoslav.sixth.e3d.renderer.raster.Color; import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection; +import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager; import java.awt.*; import java.awt.event.ComponentAdapter; @@ -77,7 +78,6 @@ import java.util.concurrent.Executors; public class ViewPanel extends Canvas { private static final long serialVersionUID = 1683277888885045387L; private static final int NUM_BUFFERS = 2; - private static final int NUM_RENDER_SEGMENTS = 8; /** The input manager handling mouse and keyboard events. */ private final InputManager inputManager = new InputManager(this); @@ -90,7 +90,7 @@ public class ViewPanel extends Canvas { /** The set of frame listeners notified before each frame. */ private final Set frameListeners = ConcurrentHashMap.newKeySet(); /** The executor service for parallel rendering. */ - private final ExecutorService renderExecutor = Executors.newFixedThreadPool(NUM_RENDER_SEGMENTS); + private final ExecutorService renderExecutor = Executors.newFixedThreadPool(RenderingContext.NUM_RENDER_SEGMENTS); /** The background color of the view. */ public Color backgroundColor = Color.BLACK; @@ -101,6 +101,14 @@ public class ViewPanel extends Canvas { /** The developer tools panel popup, or null if not currently shown. */ private DeveloperToolsPanel developerToolsPanel = null; + /** + * Global lighting manager for the scene. + * Contains all light sources and ambient light settings. Shaded polygons + * access this via the RenderingContext during paint(). Add lights here + * to illuminate the world. + */ + private final LightingManager lightingManager = new LightingManager(); + /** * Stores milliseconds when the last frame was updated. This is needed to calculate the time delta between frames. * Time delta is used to calculate smooth animation. @@ -152,6 +160,9 @@ public class ViewPanel extends Canvas { initializeCanvas(); + // Set default ambient light for the scene + lightingManager.setAmbientLight(new Color(50, 50, 50)); + addComponentListener(new ComponentAdapter() { @Override public void componentResized(final ComponentEvent e) { @@ -278,6 +289,16 @@ public class ViewPanel extends Canvas { return debugLogBuffer; } + /** + * Returns the global lighting manager for the scene. + * Add light sources here to illuminate the world. + * + * @return the lighting manager + */ + public LightingManager getLightingManager() { + return lightingManager; + } + /** * Shows the developer tools panel, toggling it if already open. * Called when F12 is pressed. @@ -362,36 +383,27 @@ public class ViewPanel extends Canvas { } renderFrameCount++; - debugLogBuffer.log("[VIEWPANEL] renderFrame #" + renderFrameCount - + " START, shapes=" + rootShapeCollection.getShapes().size() - + ", frameNumber=" + renderingContext.frameNumber - + ", bufferSize=" + renderingContext.width + "x" + renderingContext.height); try { // === Render ONCE to offscreen buffer === // The offscreen bufferedImage is unaffected by BufferStrategy contentsRestored(), // so we only need to render once, then retry the blit if needed. clearCanvasAllSegments(); - debugLogBuffer.log("[VIEWPANEL] Phase 1 done: canvas cleared"); - rootShapeCollection.transformShapes(this, renderingContext); - debugLogBuffer.log("[VIEWPANEL] Phase 2 done: shapes transformed, queued=" + rootShapeCollection.getQueuedShapeCount()); - rootShapeCollection.sortShapes(); - debugLogBuffer.log("[VIEWPANEL] Phase 3 done: shapes sorted"); // Phase 4: Paint segments in parallel final int height = renderingContext.height; - final int segmentHeight = height / NUM_RENDER_SEGMENTS; - final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[NUM_RENDER_SEGMENTS]; - final CountDownLatch latch = new CountDownLatch(NUM_RENDER_SEGMENTS); + final int segmentHeight = height / RenderingContext.NUM_RENDER_SEGMENTS; + final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[RenderingContext.NUM_RENDER_SEGMENTS]; + final CountDownLatch latch = new CountDownLatch(RenderingContext.NUM_RENDER_SEGMENTS); - for (int i = 0; i < NUM_RENDER_SEGMENTS; i++) { + for (int i = 0; i < RenderingContext.NUM_RENDER_SEGMENTS; i++) { final int segmentIndex = i; final int minY = i * segmentHeight; - final int maxY = (i == NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight; + final int maxY = (i == RenderingContext.NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight; - segmentContexts[i] = new SegmentRenderingContext(renderingContext, minY, maxY); + segmentContexts[i] = new SegmentRenderingContext(renderingContext, minY, maxY, segmentIndex); // Skip odd segments when renderAlternateSegments is enabled for overdraw debugging if (developerTools.renderAlternateSegments && (i % 2 == 1)) { @@ -415,29 +427,21 @@ public class ViewPanel extends Canvas { Thread.currentThread().interrupt(); return; } - debugLogBuffer.log("[VIEWPANEL] Phase 4 done: segments painted"); // Phase 5: Combine mouse results combineMouseResults(segmentContexts); - debugLogBuffer.log("[VIEWPANEL] Phase 5 done: mouse results combined"); // === Blit loop — only re-blit, never re-render === // contentsRestored() can trigger when the OS recreates the back buffer // (common during window creation). Since our offscreen bufferedImage still // contains the correct frame data, we only need to re-blit, not re-render. - int blitCount = 0; do { Graphics2D g = null; try { g = (Graphics2D) bufferStrategy.getDrawGraphics(); if (g != null) { - debugLogBuffer.log("[VIEWPANEL] Blit attempt #" + (blitCount + 1) + - ", image=" + renderingContext.bufferedImage.getWidth() + "x" + renderingContext.bufferedImage.getHeight() + - ", type=" + renderingContext.bufferedImage.getType() + - ", g=" + g.getClass().getSimpleName()); // Use image observer to ensure proper image loading g.drawImage(renderingContext.bufferedImage, 0, 0, this); - blitCount++; } } catch (final Exception e) { debugLogBuffer.log("[VIEWPANEL] Blit exception: " + e.getMessage()); @@ -446,17 +450,14 @@ public class ViewPanel extends Canvas { if (g != null) g.dispose(); } } while (bufferStrategy.contentsRestored()); - debugLogBuffer.log("[VIEWPANEL] Phase 6 done: blit complete (x" + blitCount + "), contentsRestored=" + bufferStrategy.contentsRestored() + ", contentsLost=" + bufferStrategy.contentsLost()); if (bufferStrategy.contentsLost()) { debugLogBuffer.log("[VIEWPANEL] Buffer contents LOST, reinitializing"); bufferStrategyInitialized = false; bufferStrategy = null; } else { - debugLogBuffer.log("[VIEWPANEL] Calling bufferStrategy.show()"); bufferStrategy.show(); java.awt.Toolkit.getDefaultToolkit().sync(); - debugLogBuffer.log("[VIEWPANEL] show() completed"); } } catch (final Exception e) { debugLogBuffer.log("[VIEWPANEL] renderFrame exception: " + e.getMessage()); @@ -468,17 +469,16 @@ public class ViewPanel extends Canvas { private void clearCanvasAllSegments() { final int rgb = (backgroundColor.r << 16) | (backgroundColor.g << 8) | backgroundColor.b; - debugLogBuffer.log("[VIEWPANEL] Clearing canvas with color: 0x" + Integer.toHexString(rgb) + " (black=0x0)"); final int width = renderingContext.width; final int height = renderingContext.height; final int[] pixels = renderingContext.pixels; if (developerTools.renderAlternateSegments) { // Clear only even segments (0, 2, 4, 6), leave odd segments black - final int segmentHeight = height / NUM_RENDER_SEGMENTS; - for (int seg = 0; seg < NUM_RENDER_SEGMENTS; seg += 2) { + final int segmentHeight = height / RenderingContext.NUM_RENDER_SEGMENTS; + for (int seg = 0; seg < RenderingContext.NUM_RENDER_SEGMENTS; seg += 2) { final int minY = seg * segmentHeight; - final int maxY = (seg == NUM_RENDER_SEGMENTS - 1) ? height : (seg + 1) * segmentHeight; + final int maxY = (seg == RenderingContext.NUM_RENDER_SEGMENTS - 1) ? height : (seg + 1) * segmentHeight; Arrays.fill(pixels, minY * width, maxY * width, rgb); } } else { @@ -624,7 +624,10 @@ public class ViewPanel extends Canvas { int panelHeight = getHeight(); if (panelWidth <= 0 || panelHeight <= 0) { - renderingContext = null; + if (renderingContext != null) { + renderingContext.dispose(); + renderingContext = null; + } return; } @@ -632,8 +635,13 @@ public class ViewPanel extends Canvas { if ((renderingContext == null) || (renderingContext.width != panelWidth) || (renderingContext.height != panelHeight)) { + if (renderingContext != null) { + renderingContext.dispose(); + } renderingContext = new RenderingContext(panelWidth, panelHeight); renderingContext.developerTools = developerTools; + renderingContext.debugLogBuffer = debugLogBuffer; + renderingContext.lightingManager = lightingManager; } renderingContext.prepareForNewFrameRendering(); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java index dc038aa..2d11012 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java @@ -10,7 +10,6 @@ import eu.svjatoslav.sixth.e3d.gui.Camera; import eu.svjatoslav.sixth.e3d.gui.FrameListener; import eu.svjatoslav.sixth.e3d.gui.ViewPanel; import eu.svjatoslav.sixth.e3d.math.Quaternion; -import eu.svjatoslav.sixth.e3d.math.Rotation; import java.awt.*; import java.awt.event.*; @@ -278,7 +277,7 @@ public class InputManager implements Math.min( Math.PI / 2 - 0.001, cameraPitch)); final Camera camera = viewPanel.getCamera(); - camera.getTransform().getRotation().setQuaternion(Quaternion.fromAngles(cameraYaw, cameraPitch)); + camera.getTransform().getRotation().set(Quaternion.fromAngles(cameraYaw, cameraPitch)); mouseDelta.zero(); return true; diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/DiamondSquare.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/DiamondSquare.java new file mode 100644 index 0000000..1801d49 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/DiamondSquare.java @@ -0,0 +1,171 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +package eu.svjatoslav.sixth.e3d.math; + +import java.util.Random; + +/** + * Diamond-square algorithm for procedural noise generation. + *

+ * Generates realistic fractal noise suitable for terrain, textures, + * and other procedural content. The algorithm produces a 2D map + * where each value falls within the specified [min, max] range. + *

+ * Grid size must be 2^n + 1 (e.g., 3, 5, 9, 17, 33, 65, 129, 257). + * + * @see Diamond-square algorithm + */ +public final class DiamondSquare { + + private static final double DEFAULT_ROUGHNESS = 0.6; + + private DiamondSquare() { + } + + /** + * Generates a fractal noise map using the diamond-square algorithm. + * + * @param gridSize the size of the grid (must be 2^n + 1) + * @param min the minimum value in the output + * @param max the maximum value in the output + * @param seed random seed for reproducible results + * @return a 2D array of values in range [min, max] + * @throws IllegalArgumentException if gridSize is not 2^n + 1 + */ + public static double[][] generateMap(int gridSize, double min, double max, long seed) { + return generateMap(gridSize, min, max, DEFAULT_ROUGHNESS, seed); + } + + /** + * Generates a fractal noise map using the diamond-square algorithm with custom roughness. + * + * @param gridSize the size of the grid (must be 2^n + 1) + * @param min the minimum value in the output + * @param max the maximum value in the output + * @param roughness the roughness factor (0.0 to 1.0), higher values produce more variation + * @param seed random seed for reproducible results + * @return a 2D array of values in range [min, max] + * @throws IllegalArgumentException if gridSize is not 2^n + 1 + */ + public static double[][] generateMap(int gridSize, double min, double max, double roughness, long seed) { + if (!isValidGridSize(gridSize)) { + throw new IllegalArgumentException("Grid size must be 2^n + 1 (e.g., 65, 129, 257)"); + } + + Random random = new Random(seed); + double[][] map = new double[gridSize][gridSize]; + + map[0][0] = random.nextDouble(); + map[0][gridSize - 1] = random.nextDouble(); + map[gridSize - 1][0] = random.nextDouble(); + map[gridSize - 1][gridSize - 1] = random.nextDouble(); + + int stepSize = gridSize - 1; + double currentScale = roughness; + + while (stepSize > 1) { + int halfStep = stepSize / 2; + + for (int y = 0; y < gridSize - 1; y += stepSize) { + for (int x = 0; x < gridSize - 1; x += stepSize) { + double avg = (map[y][x] + + map[y][x + stepSize] + + map[y + stepSize][x] + + map[y + stepSize][x + stepSize]) / 4.0; + map[y + halfStep][x + halfStep] = + avg + (random.nextDouble() - 0.5) * currentScale; + } + } + + for (int y = 0; y < gridSize; y += stepSize) { + for (int x = 0; x < gridSize; x += stepSize) { + if (x + halfStep < gridSize) { + double avg = map[y][x]; + if (x - halfStep >= 0) { + avg += map[y][x - halfStep]; + } + if (x + stepSize < gridSize) { + avg += map[y][x + stepSize]; + } + if (y + halfStep < gridSize) { + avg += map[y + halfStep][x + halfStep]; + } else if (y - halfStep >= 0) { + avg += map[y - halfStep][x + halfStep]; + } + map[y][x + halfStep] = + avg / 4.0 + (random.nextDouble() - 0.5) * currentScale; + } + + if (y + halfStep < gridSize) { + double avg = map[y][x]; + if (y - halfStep >= 0) { + avg += map[y - halfStep][x]; + } + if (y + stepSize < gridSize) { + avg += map[y + stepSize][x]; + } + if (x + halfStep < gridSize) { + avg += map[y + halfStep][x + halfStep]; + } else if (x - halfStep >= 0) { + avg += map[y + halfStep][x - halfStep]; + } + map[y + halfStep][x] = + avg / 4.0 + (random.nextDouble() - 0.5) * currentScale; + } + } + } + + stepSize = halfStep; + currentScale *= roughness; + } + + normalize(map, min, max); + return map; + } + + private static void normalize(double[][] map, double min, double max) { + double actualMin = Double.MAX_VALUE; + double actualMax = Double.MIN_VALUE; + + for (double[] row : map) { + for (double value : row) { + if (value < actualMin) actualMin = value; + if (value > actualMax) actualMax = value; + } + } + + double range = actualMax - actualMin; + double targetRange = max - min; + + if (range == 0) { + for (int y = 0; y < map.length; y++) { + for (int x = 0; x < map[y].length; x++) { + map[y][x] = min; + } + } + return; + } + + for (int y = 0; y < map.length; y++) { + for (int x = 0; x < map[y].length; x++) { + map[y][x] = min + (map[y][x] - actualMin) / range * targetRange; + } + } + } + + /** + * Checks if the grid size is valid for the diamond-square algorithm. + * Valid sizes are 2^n + 1 (e.g., 3, 5, 9, 17, 33, 65, 129, 257). + * + * @param size the grid size to validate + * @return true if the size is valid + */ + public static boolean isValidGridSize(int size) { + if (size < 3) return false; + int value = size - 1; + return (value & (value - 1)) == 0; + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Quaternion.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Quaternion.java index 86aef8a..42bab87 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Quaternion.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Quaternion.java @@ -15,16 +15,55 @@ import static java.lang.Math.sin; *

Quaternions provide a compact representation of rotations that avoids * gimbal lock and enables smooth interpolation (slerp).

* + *

Usage example:

+ *
{@code
+ * // Create a rotation from yaw and pitch angles
+ * Quaternion rotation = Quaternion.fromAngles(0.5, -0.3);
+ *
+ * // Apply rotation to a point
+ * Point3D point = new Point3D(1, 0, 0);
+ * rotation.rotate(point);
+ *
+ * // Combine rotations
+ * Quaternion combined = rotation.multiply(otherRotation);
+ * }
+ * * @see Matrix3x3 - * @see Rotation + * @see Transform */ public class Quaternion { + /** + * The scalar (real) component of the quaternion. + */ public double w; + + /** + * The i component (x-axis rotation factor). + */ public double x; + + /** + * The j component (y-axis rotation factor). + */ public double y; + + /** + * The k component (z-axis rotation factor). + */ public double z; + /** + * Creates an identity quaternion representing no rotation. + * Equivalent to Quaternion(1, 0, 0, 0). + */ + public Quaternion() { + this.w = 1; + this.x = 0; + this.y = 0; + this.z = 0; + } + /** * Creates a quaternion with the specified components. * @@ -40,6 +79,27 @@ public class Quaternion { this.z = z; } + /** + * Creates a copy of this quaternion. + * + * @return a new quaternion with the same component values + */ + public Quaternion clone() { + return new Quaternion(w, x, y, z); + } + + /** + * Copies the values from another quaternion into this one. + * + * @param other the quaternion to copy from + */ + public void set(final Quaternion other) { + this.w = other.w; + this.x = other.x; + this.y = other.y; + this.z = other.z; + } + /** * Returns the identity quaternion representing no rotation. * @@ -110,6 +170,18 @@ public class Quaternion { return this; } + /** + * Returns the inverse (conjugate) of this unit quaternion. + * + *

For a unit quaternion, the inverse equals the conjugate: (w, -x, -y, -z). + * This represents the opposite rotation.

+ * + * @return a new quaternion representing the inverse rotation + */ + public Quaternion invert() { + return new Quaternion(w, -x, -y, -z); + } + /** * Converts this quaternion to a 3x3 rotation matrix. * @@ -133,4 +205,14 @@ public class Quaternion { return m; } + /** + * Converts this quaternion to a 3x3 rotation matrix. + * Alias for {@link #toMatrix3x3()} for API convenience. + * + * @return a new matrix representing this rotation + */ + public Matrix3x3 toMatrix() { + return toMatrix3x3(); + } + } \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java deleted file mode 100644 index 8e529d0..0000000 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Sixth 3D engine. Author: Svjatoslav Agejenko. - * This project is released under Creative Commons Zero (CC0) license. - */ -package eu.svjatoslav.sixth.e3d.math; - -import eu.svjatoslav.sixth.e3d.geometry.Point3D; - -/** - * Represents a rotation in 3D space using a quaternion. - * - *

Quaternions provide smooth interpolation and avoid gimbal lock - * compared to Euler angles.

- * - * @see Transform - * @see Quaternion - */ -public class Rotation implements Cloneable { - - private Quaternion quaternion; - - /** - * Creates a rotation with no rotation (identity). - */ - public Rotation() { - quaternion = Quaternion.identity(); - } - - /** - * Creates a copy of this rotation with the same orientation. - * - * @return a new rotation with the same quaternion values - */ - @Override - public Rotation clone() { - final Rotation r = new Rotation(); - r.quaternion = new Quaternion(quaternion.w, quaternion.x, quaternion.y, quaternion.z); - return r; - } - - /** - * Rotates a point around the origin using this rotation. - * - * @param point3d the point to rotate (modified in place) - */ - public void rotate(final Point3D point3d) { - toMatrix().transform(point3d, point3d); - } - - /** - * Sets the rotation from a quaternion. - * - * @param q the quaternion to set - */ - public void setQuaternion(final Quaternion q) { - quaternion = new Quaternion(q.w, q.x, q.y, q.z); - } - - /** - * Returns the internal quaternion. - * - * @return the quaternion (not a copy) - */ - public Quaternion getQuaternion() { - return quaternion; - } - - /** - * Converts this rotation to a 3x3 transformation matrix. - * - * @return a matrix representing this rotation - */ - public Matrix3x3 toMatrix() { - return quaternion.toMatrix3x3(); - } - -} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java index 047308a..9a75ece 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java @@ -11,7 +11,8 @@ import eu.svjatoslav.sixth.e3d.geometry.Point3D; * *

Transformations are applied in order: rotation first, then translation.

* - * @see Rotation + * @see Quaternion + * @see Point3D */ public class Transform implements Cloneable { @@ -23,14 +24,14 @@ public class Transform implements Cloneable { /** * The rotation applied before translation. */ - private final Rotation rotation; + private final Quaternion rotation; /** * Creates a transform with no translation or rotation (identity transform). */ public Transform() { translation = new Point3D(); - rotation = new Rotation(); + rotation = new Quaternion(); } /** @@ -40,7 +41,7 @@ public class Transform implements Cloneable { */ public Transform(final Point3D translation) { this.translation = translation; - rotation = new Rotation(); + rotation = new Quaternion(); } /** @@ -53,7 +54,7 @@ public class Transform implements Cloneable { */ public static Transform fromAngles(final Point3D translation, final double angleXZ, final double angleYZ) { final Transform t = new Transform(translation); - t.rotation.setQuaternion(Quaternion.fromAngles(angleXZ, angleYZ)); + t.rotation.set(Quaternion.fromAngles(angleXZ, angleYZ)); return t; } @@ -61,11 +62,11 @@ public class Transform implements Cloneable { * Creates a transform with the specified translation and rotation. * * @param translation the translation - * @param rotation the rotation + * @param rotation the rotation (will be cloned) */ - public Transform(final Point3D translation, final Rotation rotation) { + public Transform(final Point3D translation, final Quaternion rotation) { this.translation = translation; - this.rotation = rotation; + this.rotation = rotation.clone(); } /** @@ -81,9 +82,9 @@ public class Transform implements Cloneable { /** * Returns the rotation component of this transform. * - * @return the rotation (mutable reference) + * @return the rotation quaternion (mutable reference) */ - public Rotation getRotation() { + public Quaternion getRotation() { return rotation; } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java index 1da9fdd..20cea76 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java @@ -58,13 +58,13 @@ public class Vertex { /** * Texture coordinate for UV mapping (optional). */ - public Point2D textureCoordinate; + public Point2D textureCoordinate; // TODO: is this proper term ? /** * The frame number when this vertex was last transformed (for caching). */ - private int lastTransformedFrame = -1; // Start at -1 so first frame (frameNumber=1) will transform + private int lastTransformedFrame = -1; // Start at -1 so the first frame (frameNumber=1) will transform /** * Creates a vertex at the origin (0, 0, 0) with no texture coordinate. diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java index d585576..900c508 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java @@ -34,7 +34,7 @@ public class CameraView { bottomLeft = new Point3D(0, 0, SIZE).rotate(-viewAngle, viewAngle); bottomRight = new Point3D(0, 0, SIZE).rotate(viewAngle, viewAngle); - final Matrix3x3 m = camera.getTransform().getRotation().toMatrix(); + final Matrix3x3 m = camera.getTransform().getRotation().invert().toMatrix3x3(); final Point3D temp = new Point3D(); temp.clone(topLeft); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java index d751a84..47edea9 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java @@ -79,7 +79,7 @@ public class RaytracingCamera extends TexturedRectangle { bottomLeft.rotate(cameraCenter, -viewAngle, viewAngle); bottomRight.rotate(cameraCenter, viewAngle, viewAngle); - final Matrix3x3 m = camera.getTransform().getRotation().toMatrix(); + final Matrix3x3 m = camera.getTransform().getRotation().invert().toMatrix3x3(); final Point3D temp = new Point3D(); temp.clone(topLeft); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java index c075580..02b4c0c 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java @@ -106,8 +106,7 @@ public class ShapeCollection { final Camera camera = viewPanel.getCamera(); - cameraRotationTransform.getRotation().setQuaternion( - camera.getTransform().getRotation().getQuaternion()); + cameraRotationTransform.getRotation().set(camera.getTransform().getRotation()); transformStack.addTransform(cameraRotationTransform); final Point3D cameraLocation = camera.getTransform().getTranslation(); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java index d62c034..164ae54 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java @@ -39,7 +39,6 @@ public class SolidPolygon extends AbstractCoordinateShape { private final Point3D cachedCenter = new Point3D(); private Color color; private boolean shadingEnabled = false; - private LightingManager lightingManager; private boolean backfaceCulling = false; /** @@ -241,15 +240,6 @@ public class SolidPolygon extends AbstractCoordinateShape { this.color = color; } - /** - * Returns the lighting manager used for shading calculations. - * - * @return the lighting manager, or null if shading is not enabled - */ - public LightingManager getLightingManager() { - return lightingManager; - } - /** * Checks if shading is enabled for this polygon. * @@ -261,13 +251,13 @@ public class SolidPolygon extends AbstractCoordinateShape { /** * Enables or disables shading for this polygon. + * When enabled, the polygon uses the global lighting manager from the + * rendering context to calculate flat shading based on light sources. * - * @param shadingEnabled true to enable shading, false to disable - * @param lightingManager the lighting manager to use for shading calculations + * @param shadingEnabled true to enable shading, false to disable */ - public void setShadingEnabled(final boolean shadingEnabled, final LightingManager lightingManager) { + public void setShadingEnabled(final boolean shadingEnabled) { this.shadingEnabled = shadingEnabled; - this.lightingManager = lightingManager; } /** @@ -370,10 +360,10 @@ public class SolidPolygon extends AbstractCoordinateShape { Color paintColor = color; - if (shadingEnabled && lightingManager != null) { + if (shadingEnabled && renderBuffer.lightingManager != null) { calculateCenter(cachedCenter); calculateNormal(cachedNormal); - paintColor = lightingManager.calculateLighting(cachedCenter, cachedNormal, color); + paintColor = renderBuffer.lightingManager.calculateLighting(cachedCenter, cachedNormal, color); } drawPolygon(renderBuffer, onScreenPoint1, onScreenPoint2, diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java index d3455a0..f456590 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java @@ -12,7 +12,6 @@ import eu.svjatoslav.sixth.e3d.math.Transform; import eu.svjatoslav.sixth.e3d.math.TransformStack; import eu.svjatoslav.sixth.e3d.renderer.raster.Color; import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator; -import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager; import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape; import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line; import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; @@ -66,13 +65,45 @@ import java.util.List; * @see eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer the level-of-detail polygon slicer */ public class AbstractCompositeShape extends AbstractShape { + /** + * The original sub-shapes added to this composite, each wrapped with group + * identifier and visibility state. Shapes are stored in insertion order and + * remain in this collection even when hidden. + */ private final List originalSubShapes = new ArrayList<>(); + + /** + * Tracks the distance and angle between the camera and this shape to compute + * an appropriate slice factor for level-of-detail adjustments. + */ private final ViewSpaceTracker viewSpaceTracker; + + /** + * The current slice factor used for tessellating textured polygons into smaller + * triangles for perspective-correct rendering. Higher values produce more triangles + * for distant objects; lower values for nearby objects. Updated dynamically based + * on view-space analysis. + */ double currentSliceFactor = 5; + + /** + * The processed list of sub-shapes ready for rendering. Contains non-textured + * shapes directly, and sliced triangles for textured polygons. Regenerated when + * {@link #slicingOutdated} is true. + */ private List renderedSubShapes = new ArrayList<>(); + + /** + * Flag indicating whether the rendered sub-shapes need to be regenerated. + * Set to true when sub-shapes are added, removed, or when group visibility changes. + */ private boolean slicingOutdated = true; + + /** + * The position and orientation transform for this composite shape. + * Applied to all sub-shapes during the rendering transform pass. + */ private Transform transform; - private LightingManager lightingManager; /** * Creates a composite shape at the world origin with no rotation. @@ -119,15 +150,12 @@ public class AbstractCompositeShape extends AbstractShape { * @param groupId the group identifier, or {@code null} for ungrouped shapes */ public void addShape(final AbstractShape shape, final String groupId) { - final SubShape subShape = new SubShape(shape); - subShape.setGroup(groupId); - subShape.setVisible(true); - originalSubShapes.add(subShape); + originalSubShapes.add(new SubShape(shape, groupId, true)); slicingOutdated = true; } /** - * This method should be overridden by anyone wanting to customize shape + * This method should be overridden by anyone wanting to customize the shape * before it is rendered. * * @param transformPipe the current transform stack @@ -174,8 +202,7 @@ public class AbstractCompositeShape extends AbstractShape { * @see #removeGroup(String) */ public void hideGroup(final String groupIdentifier) { - for (int i = 0; i < originalSubShapes.size(); i++) { - final SubShape subShape = originalSubShapes.get(i); + for (final SubShape subShape : originalSubShapes) { if (subShape.matchesGroup(groupIdentifier)) { subShape.setVisible(false); slicingOutdated = true; @@ -183,19 +210,27 @@ public class AbstractCompositeShape extends AbstractShape { } } - private boolean isReslicingNeeded(double proposedNewSliceFactor, double currentSliceFactor) { + /** + * Determines whether textured polygons need to be re-sliced based on slice factor change. + *

+ * Re-slicing is needed if the slicing state is marked outdated, or if the ratio between + * the larger and smaller slice factor exceeds 1.5x. This threshold prevents frequent + * re-slicing for minor view changes while ensuring significant LOD changes trigger updates. + * + * @param proposedNewSliceFactor the slice factor computed from current view distance + * @param currentSliceFactor the slice factor currently in use + * @return {@code true} if re-slicing should be performed + */ + private boolean isReslicingNeeded(final double proposedNewSliceFactor, final double currentSliceFactor) { if (slicingOutdated) return true; // reslice if there is significant difference between proposed and current slice factor - if (proposedNewSliceFactor > currentSliceFactor) { - final double tmp = proposedNewSliceFactor; - proposedNewSliceFactor = currentSliceFactor; - currentSliceFactor = tmp; - } + final double larger = Math.max(proposedNewSliceFactor, currentSliceFactor); + final double smaller = Math.min(proposedNewSliceFactor, currentSliceFactor); - return (currentSliceFactor / proposedNewSliceFactor) > 1.5d; + return (larger / smaller) > 1.5d; } /** @@ -233,13 +268,18 @@ public class AbstractCompositeShape extends AbstractShape { return result; } - private void resliceIfNeeded() { + /** + * Checks if re-slicing is needed and performs it if so. + * + * @param context the rendering context for logging + */ + private void resliceIfNeeded(final RenderingContext context) { final double proposedSliceFactor = viewSpaceTracker.proposeSliceFactor(); if (isReslicingNeeded(proposedSliceFactor, currentSliceFactor)) { currentSliceFactor = proposedSliceFactor; - reslice(); + reslice(context); } } @@ -295,39 +335,21 @@ public class AbstractCompositeShape extends AbstractShape { this.transform = transform; } - /** - * Sets the lighting manager for this composite shape and enables shading on all SolidPolygon sub-shapes. - * - * @param lightingManager the lighting manager to use for shading calculations - */ - public void setLightingManager(final LightingManager lightingManager) { - this.lightingManager = lightingManager; - applyShadingToPolygons(); - } - /** * Enables or disables shading for all SolidPolygon sub-shapes. + * When enabled, polygons use the global lighting manager from the rendering + * context to calculate flat shading based on light sources. * - * @param shadingEnabled true to enable shading, false to disable + * @param shadingEnabled {@code true} to enable shading, {@code false} to disable */ public void setShadingEnabled(final boolean shadingEnabled) { for (final SubShape subShape : getOriginalSubShapes()) { final AbstractShape shape = subShape.getShape(); if (shape instanceof SolidPolygon) { - ((SolidPolygon) shape).setShadingEnabled(shadingEnabled, lightingManager); + ((SolidPolygon) shape).setShadingEnabled(shadingEnabled); } - } - } - private void applyShadingToPolygons() { - if (lightingManager == null) - return; - - for (final SubShape subShape : getOriginalSubShapes()) { - final AbstractShape shape = subShape.getShape(); - if (shape instanceof SolidPolygon) { - ((SolidPolygon) shape).setShadingEnabled(true, lightingManager); - } + // TODO: if shape is abstract composite, it seems that it would be good to enabled sharding recursively there too } } @@ -363,41 +385,61 @@ public class AbstractCompositeShape extends AbstractShape { } } - private void reslice() { + /** + * Re-slices all textured polygons and rebuilds the rendered sub-shapes list. + * Logs the operation to the debug log buffer if available. + * + * @param context the rendering context for logging, may be {@code null} + */ + private void reslice(final RenderingContext context) { slicingOutdated = false; final List result = new ArrayList<>(); final Slicer slicer = new Slicer(currentSliceFactor); + int texturedPolygonCount = 0; + int otherShapeCount = 0; + for (int i = 0; i < originalSubShapes.size(); i++) { final SubShape subShape = originalSubShapes.get(i); if (subShape.isVisible()) { - if (subShape.getShape() instanceof TexturedPolygon) + if (subShape.getShape() instanceof TexturedPolygon) { slicer.slice((TexturedPolygon) subShape.getShape()); - else + texturedPolygonCount++; + } else { result.add(subShape.getShape()); + otherShapeCount++; + } } } result.addAll(slicer.getResult()); renderedSubShapes = result; + + // Log to developer tools console if available + if (context != null && context.debugLogBuffer != null) { + context.debugLogBuffer.log("reslice: " + getClass().getSimpleName() + + " sliceFactor=" + String.format("%.2f", currentSliceFactor) + + " texturedPolygons=" + texturedPolygonCount + + " otherShapes=" + otherShapeCount + + " resultingTexturedPolygons=" + slicer.getResult().size()); + } } @Override public void transform(final TransformStack transformPipe, final RenderAggregator aggregator, final RenderingContext context) { - // add current composite shape transform to the end of the transform - // pipeline + // Add the current composite shape transform to the end of the transform + // pipeline. transformPipe.addTransform(transform); viewSpaceTracker.analyze(transformPipe, context); beforeTransformHook(transformPipe, context); - // hack, to get somewhat perspective correct textures - resliceIfNeeded(); + resliceIfNeeded(context); // transform rendered subshapes for (final AbstractShape shape : renderedSubShapes) diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java index 6530b2d..b139a2b 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java @@ -4,6 +4,8 @@ */ package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base; +import java.util.Objects; + import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape; /** @@ -20,17 +22,44 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape; */ public class SubShape { + /** + * The wrapped shape that belongs to the parent composite shape. + * This is the actual renderable geometry (line, polygon, etc.). + */ private final AbstractShape shape; + + /** + * Whether this sub-shape should be rendered. + * Hidden shapes remain in the composite but are excluded from rendering. + */ private boolean visible = true; + + /** + * The group identifier for batch visibility operations. + * {@code null} indicates this shape is not part of any named group. + */ private String groupIdentifier; /** - * Creates a sub-shape wrapper around the given shape. + * Creates a sub-shape wrapper around the given shape with default visibility (visible). * * @param shape the shape to wrap */ - public SubShape(AbstractShape shape) { + public SubShape(final AbstractShape shape) { + this(shape, null, true); + } + + /** + * Creates a sub-shape with all properties specified. + * + * @param shape the shape to wrap + * @param groupIdentifier the group identifier, or {@code null} for ungrouped + * @param visible whether the shape is initially visible + */ + public SubShape(final AbstractShape shape, final String groupIdentifier, final boolean visible) { this.shape = shape; + this.groupIdentifier = groupIdentifier; + this.visible = visible; } /** @@ -49,10 +78,7 @@ public class SubShape { * @return {@code true} if this sub-shape belongs to the specified group */ public boolean matchesGroup(final String groupIdentifier) { - if (this.groupIdentifier == null) - return groupIdentifier == null; - - return this.groupIdentifier.equals(groupIdentifier); + return Objects.equals(this.groupIdentifier, groupIdentifier); } /** diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java index 7558eb8..689d0c3 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java @@ -75,7 +75,7 @@ public class Texture { * Index 0 is 1/2 the primary, index 1 is 1/4, and so on. * Entries are lazily populated on first access. */ - TextureBitmap[] downSampled = new TextureBitmap[8]; + TextureBitmap[] downSampled = new TextureBitmap[8]; // TODO: consider renaming it to mipmap to use standard terminology /** * Creates a new texture with the specified dimensions and upscale capacity. diff --git a/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java b/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java index 831ba29..db4567a 100644 --- a/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java +++ b/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java @@ -11,24 +11,49 @@ import static org.junit.Assert.assertEquals; public class QuaternionTest { @Test - public void testFromAnglesMatchesRotation() { - final Rotation rotation = new Rotation(); - rotation.setQuaternion(Quaternion.fromAngles(0.5, 0.3)); - final Matrix3x3 rotationMatrix = rotation.toMatrix(); - + public void testFromAnglesProducesValidMatrix() { final Quaternion quaternion = Quaternion.fromAngles(0.5, 0.3); - final Matrix3x3 quaternionMatrix = quaternion.toMatrix3x3(); + final Matrix3x3 matrix = quaternion.toMatrix(); + + // Verify matrix is a valid rotation (determinant ≈ 1) + final double det = matrix.m00 * (matrix.m11 * matrix.m22 - matrix.m12 * matrix.m21) + - matrix.m01 * (matrix.m10 * matrix.m22 - matrix.m12 * matrix.m20) + + matrix.m02 * (matrix.m10 * matrix.m21 - matrix.m11 * matrix.m20); + assertEquals(1.0, det, 0.0001); + } + + @Test + public void testToMatrixAliasesToMatrix3x3() { + final Quaternion quaternion = Quaternion.fromAngles(0.7, -0.4); + final Matrix3x3 m1 = quaternion.toMatrix(); + final Matrix3x3 m2 = quaternion.toMatrix3x3(); final double epsilon = 0.0001; - assertEquals(rotationMatrix.m00, quaternionMatrix.m00, epsilon); - assertEquals(rotationMatrix.m01, quaternionMatrix.m01, epsilon); - assertEquals(rotationMatrix.m02, quaternionMatrix.m02, epsilon); - assertEquals(rotationMatrix.m10, quaternionMatrix.m10, epsilon); - assertEquals(rotationMatrix.m11, quaternionMatrix.m11, epsilon); - assertEquals(rotationMatrix.m12, quaternionMatrix.m12, epsilon); - assertEquals(rotationMatrix.m20, quaternionMatrix.m20, epsilon); - assertEquals(rotationMatrix.m21, quaternionMatrix.m21, epsilon); - assertEquals(rotationMatrix.m22, quaternionMatrix.m22, epsilon); + assertEquals(m1.m00, m2.m00, epsilon); + assertEquals(m1.m01, m2.m01, epsilon); + assertEquals(m1.m02, m2.m02, epsilon); + assertEquals(m1.m10, m2.m10, epsilon); + assertEquals(m1.m11, m2.m11, epsilon); + assertEquals(m1.m12, m2.m12, epsilon); + assertEquals(m1.m20, m2.m20, epsilon); + assertEquals(m1.m21, m2.m21, epsilon); + assertEquals(m1.m22, m2.m22, epsilon); + } + + @Test + public void testCloneProducesIndependentCopy() { + final Quaternion original = Quaternion.fromAngles(0.5, 0.3); + final Quaternion clone = original.clone(); + + assertEquals(original.w, clone.w, 0.0001); + assertEquals(original.x, clone.x, 0.0001); + assertEquals(original.y, clone.y, 0.0001); + assertEquals(original.z, clone.z, 0.0001); + + // Modify original, verify clone is unaffected + final double originalW = original.w; + original.w = 0; + assertEquals(originalW, clone.w, 0.0001); } } \ No newline at end of file diff --git a/src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java b/src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java deleted file mode 100644 index 79cb7c4..0000000 --- a/src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Sixth 3D engine. Author: Svjatoslav Agejenko. - * This project is released under Creative Commons Zero (CC0) license. - */ -package eu.svjatoslav.sixth.e3d.math; - -import eu.svjatoslav.sixth.e3d.geometry.Point3D; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class RotationMatrixTest { - - @Test - public void testToMatrixMatchesRotate() { - final Rotation rotation = new Rotation(); - rotation.setQuaternion(Quaternion.fromAngles(0.5, 0.3)); - final Point3D original = new Point3D(1, 2, 3); - - final Point3D viaRotate = new Point3D(original); - rotation.rotate(viaRotate); - - final Point3D viaMatrix = new Point3D(); - rotation.toMatrix().transform(original, viaMatrix); - - final double epsilon = 0.0001; - assertEquals(viaRotate.x, viaMatrix.x, epsilon); - assertEquals(viaRotate.y, viaMatrix.y, epsilon); - assertEquals(viaRotate.z, viaMatrix.z, epsilon); - } - -} \ No newline at end of file -- 2.20.1