[
  {
    "path": ".github/pull_request_template.md",
    "content": "## Sorry, but...\n\nI'm sorry you put the time and effort into creating this pull request, but I am\nnot currently accepting contributions to Psychopath, as explained in the\nproject's readme file.\n\nYou are more than welcome to fork Psychopath and play with it, or even develop\nit independently into something of your own.  But this repo is, at least for\nnow, a one-man project.\n"
  },
  {
    "path": ".gitignore",
    "content": "target\n*.rs.bk\nclippy.toml\n\n# Python Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n.zedstate\n.vscode\ntest_renders\nperf.data*\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nmembers = [\n    \"sub_crates/bvh_order\",\n    \"sub_crates/color\",\n    \"sub_crates/compact\",\n    \"sub_crates/halton\",\n    \"sub_crates/math3d\",\n    \"sub_crates/spectral_upsampling\",\n]\n\n[package]\nname = \"psychopath\"\nversion = \"0.1.0\"\nauthors = [\"Nathan Vegdahl <cessen@cessen.com>\"]\nedition = \"2018\"\nlicense = \"GPL v3\"\n\n[profile.release]\ndebug = true\n\n[dependencies]\n# Crates.io dependencies\nbase64 = \"0.9\"\nclap = \"2.30\"\ncopy_in_place = \"0.2.0\"\ncrossbeam = \"0.3\"\nhalf = \"1.0\"\nlazy_static = \"1.0\"\nnom = \"5\"\nnum_cpus = \"1.8\"\nopenexr = \"0.7\"\nkioku = \"0.3\"\nsobol_burley = \"0.3\"\npng_encode_mini = \"0.1.2\"\nrustc-serialize = \"0.3\"\nscoped_threadpool = \"0.1\"\ntime = \"0.1\"\nglam = \"0.15\"\nfastapprox = \"0.3\"\n\n# Local crate dependencies\n[dependencies.bvh_order]\npath = \"sub_crates/bvh_order\"\n\n[dependencies.color]\npath = \"sub_crates/color\"\n\n[dependencies.compact]\npath = \"sub_crates/compact\"\n[dependencies.halton]\n\npath = \"sub_crates/halton\"\n\n[dependencies.math3d]\npath = \"sub_crates/math3d\"\n\n[dependencies.spectral_upsampling]\npath = \"sub_crates/spectral_upsampling\"\n"
  },
  {
    "path": "LICENSE.md",
    "content": "## Psychopath\n\nWith the exception of files under `psychoblend/` and `sub_crates/`, this project is licensed under the GPLv3 as follows:\n\nCopyright (c) 2020 Nathan Vegdahl\n\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\n\n\n## Psychoblend\n\nFor the license of the files under `psychoblend/`, see `psychoblend/LICENSE.md`.\n\n\n\n## Sub-crates\n\nFor the license of the files under `sub_crates/`, see the license files in each of its respective subdirectories.\n"
  },
  {
    "path": "README.md",
    "content": "# Overview\n\nPsychopath is a path tracing 3d renderer.  You can read about its development\nat [psychopath.io](http://psychopath.io).\n\nThis project is mostly just for me to have fun, learn, and play with ideas in\n3d rendering.  I do have vague hopes that it will eventually be useful for real\nthings, but that's not a hard goal.\n\nUnlike many for-fun 3d rendering projects, Psychopath is being designed with\nproduction rendering in mind.  I think that architecting a renderer to\nefficiently handle very large data sets, complex shading, motion blur, color\nmanagement, etc. presents a much richer and more challenging problem space to\nexplore than just writing a basic path tracer.\n\n\n## Building\n\nPsychopath is written in [Rust](https://www.rust-lang.org), and is pretty\nstraightforward to build except for its OpenEXR dependency.\n\nIf you have OpenEXR 2.2 installed on your system such that pkg-config can find\nit, then as long as you have Rust (including Cargo) and a C++ compiler\ninstalled, you should be able to build Psychopath with this command at the\nrepository root:\n\n```\ncargo build --release\n```\n\nHowever, if you are on an OS that doesn't have pkg-config (e.g. OSX, Windows),\nor you prefer to do a custom build of OpenEXR, then you will need to download\nand build OpenEXR yourself and specify the necessary environment variables as\ndocumented in the [OpenEXR-rs readme](https://github.com/cessen/openexr-rs/blob/master/README.md).\n\nOnce those environment variables are set, then you should be able to build using\nthe same simple cargo command above.\n\n\n# PsychoBlend\n\nIncluded in the repository is an add-on for [Blender](http://www.blender.org)\ncalled \"PsychoBlend\" that lets you use Psychopath for rendering in Blender.\nHowever, most Blender features are not yet supported because Psychopath itself\ndoesn't support them yet.\n\n## Features Supported\n- Polygon meshes.\n- Point, area, and sun lamps (exported as sphere, rectangle, and distant disc lights, respectively)\n- Simple materials assigned per-object.\n- Focal blur / DoF\n- Camera, transform, and deformation motion blur\n- Exports dupligroups with full hierarchical instancing\n- Limited auto-detection of instanced meshes\n\n\n# License\n\nSee LICENSE.md for details.  But the gist is:\n\n* The overall project is licensed under GPLv3.\n* PsychoBlend is licensed under GPLv2, for compatibility with Blender.\n* Most crates under the `sub_crates` directory are dual-licensed under MIT and Apache 2.0 (but with some exceptions--see each crate for its respective licenses).\n\nThe intent of this scheme is to keep Psychopath itself copyleft, while allowing smaller reusable components to be licensed more liberally.\n\n\n# Contributing\n\nThis is a personal, experimental, for-fun project, and I am specifically\nnot looking for contributions of any kind.  All PRs will be rejected\nwithout review.\n\nHowever, feel free to fork this into an entirely new project, or examine\nthe code for ideas for a project of your own.\n"
  },
  {
    "path": "example_scenes/cornell_box.psy",
    "content": "Scene $Scene_fr1 {\n    Output {\n        Path [\"test_renders/cornell_box.png\"]\n    }\n    RenderSettings {\n        Resolution [512 512]\n        SamplesPerPixel [16]\n        Seed [1]\n    }\n    Camera {\n        Fov [39.449188]\n        FocalDistance [10.620000]\n        ApertureRadius [0.000000]\n        Transform [1.000000 -0.000000 0.000000 0.000000 -0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 -0.000000 0.000000 -2.779998 -8.000000 2.730010 1.000000]\n    }\n    World {\n        BackgroundShader {\n            Type [Color]\n            Color [rec709, 0.000000 0.000000 0.000000]\n        }\n    }\n    Shaders {\n        SurfaceShader $Green {\n            Type [Lambert]\n            Color [rec709, 0.117000 0.412500 0.115000]\n        }\n        SurfaceShader $Red {\n            Type [Lambert]\n            Color [rec709, 0.611000 0.055500 0.062000]\n        }\n        SurfaceShader $White {\n            Type [Lambert]\n            Color [rec709, 0.729500 0.735500 0.729000]\n        }\n    }\n    Objects {\n        RectangleLight $__Area {\n            Color [rec709, 84.300003 53.800003 18.500000]\n            Dimensions [1.350000 1.100000]\n        }\n        MeshSurface $__Plane.010_ {\n            SurfaceShaderBind [$White]\n            Vertices [-2.649998 2.959996 3.299997 -4.229996 2.469997 3.299997 -3.139998 4.559995 3.299997 -4.719996 4.059995 3.299997 -4.719996 4.059996 0.000000 -3.139998 4.559995 0.000000 -4.229996 2.469997 0.000000 -2.649998 2.959997 0.000000 ]\n            FaceVertCounts [4 4 4 4 4 ]\n            FaceVertIndices [0 1 3 2 1 0 7 6 3 1 6 4 2 3 4 5 0 2 5 7 ]\n        }\n        MeshSurface $__Plane.008_ {\n            SurfaceShaderBind [$White]\n            Vertices [-1.299999 0.649999 1.649998 -0.820000 2.249998 1.649999 -2.899997 1.139998 1.649999 -2.399998 2.719997 1.649999 -1.299999 0.649999 0.000000 -0.820000 2.249998 0.000000 -2.899997 1.139998 0.000000 -2.399998 2.719997 0.000000 ]\n            FaceVertCounts [4 4 4 4 4 ]\n            FaceVertIndices [0 2 3 1 3 2 6 7 1 3 7 5 0 1 5 4 2 0 4 6 ]\n        }\n        MeshSurface $__Plane.006_ {\n            SurfaceShaderBind [$Red]\n            Vertices [-5.495996 5.591994 0.000000 -5.527995 -0.000001 -0.000000 -5.559996 5.591993 5.487995 -5.559995 -0.000001 5.487995 ]\n            FaceVertCounts [4 ]\n            FaceVertIndices [0 1 3 2 ]\n        }\n        MeshSurface $__Plane.004_ {\n            SurfaceShaderBind [$Green]\n            Vertices [-0.000001 5.591995 0.000000 0.000000 0.000000 0.000000 -0.000001 5.591994 5.487995 0.000000 -0.000000 5.487995 ]\n            FaceVertCounts [4 ]\n            FaceVertIndices [1 0 2 3 ]\n        }\n        MeshSurface $__Plane.002_ {\n            SurfaceShaderBind [$White]\n            Vertices [-5.495996 5.591994 0.000000 -0.000001 5.591995 0.000000 -5.559996 5.591993 5.487995 -0.000001 5.591994 5.487995 ]\n            FaceVertCounts [4 ]\n            FaceVertIndices [0 1 3 2 ]\n        }\n        MeshSurface $__Plane.001_ {\n            SurfaceShaderBind [$White]\n            Vertices [-5.559996 5.591993 5.487995 -0.000001 5.591994 5.487995 -5.559995 -0.000001 5.487995 0.000000 -0.000000 5.487995 -3.429997 3.319996 5.487995 -2.129998 3.319996 5.487995 -3.429997 2.269997 5.487995 -2.129998 2.269997 5.487995 ]\n            FaceVertCounts [4 4 4 4 ]\n            FaceVertIndices [1 5 4 0 0 4 6 2 2 6 7 3 7 5 1 3 ]\n        }\n        MeshSurface $__Plane_ {\n            SurfaceShaderBind [$White]\n            Vertices [-5.495996 5.591994 0.000000 -0.000001 5.591995 0.000000 -5.527995 -0.000001 -0.000000 0.000000 0.000000 0.000000 ]\n            FaceVertCounts [4 ]\n            FaceVertIndices [0 1 3 2 ]\n        }\n    }\n    Assembly {\n        Instance {\n            Data [$__Area]\n            Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 2.779475 -2.794788 -5.498045 1.000000]\n        }\n        Instance {\n            Data [$__Plane.010_]\n            Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000]\n        }\n        Instance {\n            Data [$__Plane.008_]\n            Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000]\n        }\n        Instance {\n            Data [$__Plane.006_]\n            Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000]\n        }\n        Instance {\n            Data [$__Plane.004_]\n            Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000]\n        }\n        Instance {\n            Data [$__Plane.002_]\n            Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000]\n        }\n        Instance {\n            Data [$__Plane.001_]\n            Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000]\n        }\n        Instance {\n            Data [$__Plane_]\n            Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000]\n        }\n    }\n}\n"
  },
  {
    "path": "example_scenes/cube.psy",
    "content": "Scene $Scene_fr1 {\n    Output {\n        Path [\"test_renders/cube.png\"]\n    }\n    RenderSettings {\n        Resolution [960 540]\n        SamplesPerPixel [16]\n        Seed [1]\n    }\n    Camera {\n        Fov [49.134342]\n        FocalDistance [9.559999]\n        ApertureRadius [0.250000]\n        Transform [0.685881 0.727634 -0.010817 0.000000 -0.317370 0.312469 0.895343 0.000000 -0.654862 0.610666 -0.445245 0.000000 7.481132 -6.507640 5.343665 1.000000]\n    }\n    World {\n        BackgroundShader {\n            Type [Color]\n            Color [rec709, 0.050876 0.050876 0.050876]\n        }\n    }\n    Shaders {\n        SurfaceShader $Material {\n            Type [Lambert]\n            Color [rec709, 0.800000 0.800000 0.800000]\n        }\n    }\n    Objects {\n        MeshSurface $__Plane_ {\n            SurfaceShaderBind [$Material]\n            Vertices [-1.000000 -1.000000 0.000000 1.000000 -1.000000 0.000000 -1.000000 1.000000 0.000000 1.000000 1.000000 0.000000]\n            FaceVertCounts [4 ]\n            FaceVertIndices [0 1 3 2 ]\n        }\n        MeshSurface $__Cube_ {\n            SurfaceShaderBind [$Material]\n            Vertices [1.000000 1.000000 -1.000000 1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 1.000000 -1.000000 1.000000 0.999999 1.000000 0.999999 -1.000001 1.000000 -1.000000 -1.000000 1.000000 -1.000000 1.000000 1.000000 ]\n            FaceVertCounts [4 4 4 4 4 4 ]\n            FaceVertIndices [0 1 2 3 4 7 6 5 0 4 5 1 1 5 6 2 2 6 7 3 4 0 3 7 ]\n        }\n        SphereLight $__Lamp {\n            Color [rec709, 50.000000 50.000000 50.000000]\n            Radius [0.100000]\n        }\n    }\n    Assembly {\n        Instance {\n            Data [$__Plane_]\n            Transform [0.078868 -0.000000 0.000000 -0.000000 -0.000000 0.078868 -0.000000 0.000000 0.000000 -0.000000 0.078868 -0.000000 -0.000000 0.000000 -0.000000 1.000000]\n        }\n        Instance {\n            Data [$__Cube_]\n            Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -1.000000 1.000000]\n        }\n        Instance {\n            Data [$__Lamp]\n            Transform [0.019856 -0.060763 0.000000 -0.000000 0.015191 0.079422 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.026851 -0.125233 -4.432303 1.000000]\n        }\n    }\n}\n"
  },
  {
    "path": "licenses/Apache-2.0.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "licenses/GPL-2.0.txt",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n"
  },
  {
    "path": "licenses/GPL-3.0.txt",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "licenses/MIT.txt",
    "content": "Copyright (c) 2020 Nathan Vegdahl\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "psychoblend/LICENSE.md",
    "content": "Copyright (c) 2020 Nathan Vegdahl\n\nThis program is free software; you can redistribute it and/or\nmodify it under the terms of the GNU General Public License\nas published by the Free Software Foundation; either version 2\nof the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program; if not, write to the Free Software\nFoundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n"
  },
  {
    "path": "psychoblend/__init__.py",
    "content": "bl_info = {\n    \"name\": \"PsychoBlend\",\n    \"version\": (0, 1),\n    \"author\": \"Nathan Vegdahl\",\n    \"blender\": (2, 70, 0),\n    \"description\": \"Psychopath renderer integration\",\n    \"location\": \"\",\n    \"wiki_url\": \"https://github.com/cessen/psychopath/wiki\",\n    \"tracker_url\": \"https://github.com/cessen/psychopath/issues\",\n    \"category\": \"Render\"}\n\n\nif \"bpy\" in locals():\n    import imp\n    imp.reload(ui)\n    imp.reload(psy_export)\n    imp.reload(render)\nelse:\n    from . import ui, psy_export, render\n\nimport bpy\nfrom bpy.types import (AddonPreferences,\n                       PropertyGroup,\n                       Operator,\n                       )\nfrom bpy.props import (StringProperty,\n                       BoolProperty,\n                       IntProperty,\n                       FloatProperty,\n                       FloatVectorProperty,\n                       EnumProperty,\n                       PointerProperty,\n                       )\n\n\n# Custom Scene settings\nclass RenderPsychopathSettingsScene(PropertyGroup):\n    spp = IntProperty(\n        name=\"Samples Per Pixel\", description=\"Total number of samples to take per pixel\",\n        min=1, max=65536, default=16\n        )\n\n    max_samples_per_bucket = IntProperty(\n        name=\"Max Samples Per Bucket\", description=\"How many samples to simultaneously calculate per thread; indirectly determines bucket size\",\n        min=1, max=2**28, soft_max=2**16, default=4096\n        )\n\n    dicing_rate = FloatProperty(\n        name=\"Dicing Rate\", description=\"The target microgeometry width in pixels\",\n        min=0.0001, max=100.0, soft_min=0.125, soft_max=1.0, default=0.25\n        )\n\n    motion_blur_segments = IntProperty(\n        name=\"Motion Segments\", description=\"The number of segments to use in motion blur.  Zero means no motion blur.  Will be rounded down to the nearest power of two.\",\n        min=0, max=256, default=0\n        )\n\n    shutter_start = FloatProperty(\n        name=\"Shutter Open\", description=\"The time during the frame that the shutter opens, for motion blur\",\n        min=-1.0, max=1.0, soft_min=0.0, soft_max=1.0, default=0.0\n        )\n\n    shutter_end = FloatProperty(\n        name=\"Shutter Close\", description=\"The time during the frame that the shutter closes, for motion blur\",\n        min=-1.0, max=1.0, soft_min=0.0, soft_max=1.0, default=0.5\n        )\n\n    export_path = StringProperty(\n        name=\"Export Path\", description=\"The path to where the .psy files should be exported when rendering.  If left blank, /tmp or the equivalent is used.\",\n        subtype='FILE_PATH'\n        )\n\n# Custom Camera properties\nclass PsychopathCamera(bpy.types.PropertyGroup):\n    aperture_radius = FloatProperty(\n        name=\"Aperture Radius\", description=\"Size of the camera's aperture, for DoF\",\n        min=0.0, max=10000.0, soft_min=0.0, soft_max=2.0, default=0.0\n        )\n\n# Psychopath material\nclass PsychopathLight(bpy.types.PropertyGroup):\n    color_type = EnumProperty(\n        name=\"Color Type\", description=\"\",\n        items=[\n            ('Rec709', 'Rec709', \"\"),\n            ('Blackbody', 'Blackbody', \"\"),\n            ('ColorTemperature', 'ColorTemperature', \"Same as Blackbody, except with brightness kept more even.\"),\n        ],\n        default=\"Rec709\"\n        )\n\n    color_blackbody_temp = FloatProperty(\n        name=\"Temperature\", description=\"Blackbody temperature in kelvin\",\n        min=0.0, soft_min=800.0, soft_max=6500.0, default=1200.0\n        )\n\n# Custom Mesh properties\nclass PsychopathMesh(bpy.types.PropertyGroup):\n    is_subdivision_surface = BoolProperty(\n        name=\"Is Subdivision Surface\", description=\"Whether this is a sibdivision surface or just a normal mesh\",\n        default=False\n        )\n\n# Psychopath material\nclass PsychopathMaterial(bpy.types.PropertyGroup):\n    surface_shader_type = EnumProperty(\n        name=\"Surface Shader Type\", description=\"\",\n        items=[('Emit', 'Emit', \"\"), ('Lambert', 'Lambert', \"\"), ('GGX', 'GGX', \"\")],\n        default=\"Lambert\"\n        )\n\n    color_type = EnumProperty(\n        name=\"Color Type\", description=\"\",\n        items=[\n            ('Rec709', 'Rec709', \"\"),\n            ('Blackbody', 'Blackbody', \"\"),\n            ('ColorTemperature', 'ColorTemperature', \"Same as Blackbody, except with brightness kept more even.\"),\n        ],\n        default=\"Rec709\"\n        )\n\n    color = FloatVectorProperty(\n        name=\"Color\", description=\"\",\n        subtype='COLOR',\n        min=0.0, soft_min=0.0, soft_max = 1.0,\n        default=[0.8,0.8,0.8]\n        )\n\n    color_blackbody_temp = FloatProperty(\n        name=\"Temperature\", description=\"Blackbody temperature in kelvin\",\n        min=0.0, soft_min=800.0, soft_max=6500.0, default=1200.0\n        )\n\n    roughness = FloatProperty(\n        name=\"Roughness\", description=\"\",\n        min=-1.0, max=1.0, soft_min=0.0, soft_max=1.0, default=0.1\n        )\n\n    tail_shape = FloatProperty(\n        name=\"Tail Shape\", description=\"\",\n        min=0.0, max=8.0, soft_min=1.0, soft_max=3.0, default=2.0\n        )\n\n    fresnel = FloatProperty(\n        name=\"Fresnel\", description=\"\",\n        min=0.0, max=1.0, soft_min=0.0, soft_max=1.0, default=0.9\n        )\n\n\n# Addon Preferences\nclass PsychopathPreferences(AddonPreferences):\n    bl_idname = __name__\n\n    filepath_psychopath = StringProperty(\n                name=\"Psychopath Location\",\n                description=\"Path to renderer executable\",\n                subtype='DIR_PATH',\n                )\n\n    def draw(self, context):\n        layout = self.layout\n        layout.prop(self, \"filepath_psychopath\")\n\n\n##### REGISTER #####\ndef register():\n    bpy.utils.register_class(PsychopathPreferences)\n    bpy.utils.register_class(RenderPsychopathSettingsScene)\n    bpy.utils.register_class(PsychopathCamera)\n    bpy.utils.register_class(PsychopathLight)\n    bpy.utils.register_class(PsychopathMesh)\n    bpy.utils.register_class(PsychopathMaterial)\n    bpy.types.Scene.psychopath = PointerProperty(type=RenderPsychopathSettingsScene)\n    bpy.types.Camera.psychopath = PointerProperty(type=PsychopathCamera)\n    bpy.types.Lamp.psychopath = PointerProperty(type=PsychopathLight)\n    bpy.types.Mesh.psychopath = PointerProperty(type=PsychopathMesh)\n    bpy.types.Material.psychopath = PointerProperty(type=PsychopathMaterial)\n    render.register()\n    ui.register()\n\n\ndef unregister():\n    bpy.utils.unregister_class(PsychopathPreferences)\n    bpy.utils.unregister_class(RenderPsychopathSettingsScene)\n    bpy.utils.unregister_class(PsychopathCamera)\n    bpy.utils.unregister_class(PsychopathLight)\n    bpy.utils.unregister_class(PsychopathMesh)\n    bpy.utils.unregister_class(PsychopathMaterial)\n    del bpy.types.Scene.psychopath\n    del bpy.types.Camera.psychopath\n    del bpy.types.Lamp.psychopath\n    del bpy.types.Mesh.psychopath\n    del bpy.types.Material.psychopath\n    render.unregister()\n    ui.unregister()\n"
  },
  {
    "path": "psychoblend/assembly.py",
    "content": "import bpy\n\nfrom .util import escape_name, mat2str, needs_def_mb, needs_xform_mb, ExportCancelled\n\nclass Assembly:\n    def __init__(self, render_engine, objects, visible_layers, group_prefix=\"\", translation_offset=(0,0,0)):\n        self.name = group_prefix\n        self.translation_offset = translation_offset\n        self.render_engine = render_engine\n        \n        self.materials = []\n        self.objects = []\n        self.instances = []\n\n        self.material_names = set()\n        self.mesh_names = set()\n        self.assembly_names = set()\n\n        # Collect all the objects, materials, instances, etc.\n        for ob in objects:\n            # Check if render is cancelled\n            if render_engine.test_break():\n                raise ExportCancelled()\n\n            # Check if the object is visible for rendering\n            vis_layer = False\n            for i in range(len(ob.layers)):\n                vis_layer = vis_layer or (ob.layers[i] and visible_layers[i])\n            if ob.hide_render or not vis_layer:\n                continue\n\n            # Store object data\n            name = None\n\n            if ob.type == 'EMPTY':\n                if ob.dupli_type == 'GROUP':\n                    name = group_prefix + \"__\" + escape_name(ob.dupli_group.name)\n                    if name not in self.assembly_names:\n                        self.assembly_names.add(name)\n                        self.objects += [Assembly(self.render_engine, ob.dupli_group.objects, ob.dupli_group.layers, name, ob.dupli_group.dupli_offset*-1)]\n            elif ob.type == 'MESH':\n                name = self.get_mesh(ob, group_prefix)\n            elif ob.type == 'LAMP' and ob.data.type == 'POINT':\n                name = self.get_sphere_lamp(ob, group_prefix)\n            elif ob.type == 'LAMP' and ob.data.type == 'AREA':\n                name = self.get_rect_lamp(ob, group_prefix)\n            \n            # Store instance\n            if name != None:\n                self.instances += [Instance(render_engine, ob, name)]\n\n    def export(self, render_engine, w):\n        if self.name == \"\":\n            w.write(\"Assembly {\\n\")\n        else:\n            w.write(\"Assembly $%s {\\n\" % self.name)\n        w.indent()\n\n        for mat in self.materials:\n            # Check if render is cancelled\n            if render_engine.test_break():\n                raise ExportCancelled()\n            mat.export(render_engine, w)\n\n        for ob in self.objects:\n            # Check if render is cancelled\n            if render_engine.test_break():\n                raise ExportCancelled()\n            ob.export(render_engine, w)\n\n        for inst in self.instances:\n            # Check if render is cancelled\n            if render_engine.test_break():\n                raise ExportCancelled()\n            inst.export(render_engine, w)\n\n        w.unindent()\n        w.write(\"}\\n\")\n    \n    #----------------\n\n    def take_sample(self, render_engine, scene, time):\n        for mat in self.materials:\n            # Check if render is cancelled\n            if render_engine.test_break():\n                raise ExportCancelled()\n            mat.take_sample(render_engine, scene, time)\n\n        for ob in self.objects:\n            # Check if render is cancelled\n            if render_engine.test_break():\n                raise ExportCancelled()\n            ob.take_sample(render_engine, scene, time)\n\n        for inst in self.instances:\n            # Check if render is cancelled\n            if render_engine.test_break():\n                raise ExportCancelled()\n            inst.take_sample(render_engine, time, self.translation_offset)\n    \n    def cleanup(self):\n        for mat in self.materials:\n            mat.cleanup()\n        for ob in self.objects:\n            ob.cleanup()\n\n    def get_mesh(self, ob, group_prefix):\n        # Figure out if we need to export or not and figure out what name to\n        # export with.\n        has_modifiers = len(ob.modifiers) > 0\n        deform_mb = needs_def_mb(ob)\n        if has_modifiers or deform_mb:\n            mesh_name = group_prefix + escape_name(\"__\" + ob.name + \"__\" + ob.data.name + \"_\")\n        else:\n            mesh_name = group_prefix + escape_name(\"__\" + ob.data.name + \"_\")\n        has_faces = len(ob.data.polygons) > 0\n        should_export_mesh = has_faces and (mesh_name not in self.mesh_names)\n        \n        # Get mesh\n        if should_export_mesh:\n            self.mesh_names.add(mesh_name)\n            self.objects += [Mesh(self.render_engine, ob, mesh_name)]\n\n            # Get materials\n            for ms in ob.material_slots:\n                if ms != None:\n                    if ms.material.name not in self.material_names:\n                        self.material_names.add(ms.material.name)\n                        self.materials += [Material(self.render_engine, ms.material)]\n\n            return mesh_name\n        else:\n            return None\n\n\n    def get_sphere_lamp(self, ob, group_prefix):\n        name = group_prefix + \"__\" + escape_name(ob.name)\n        self.objects += [SphereLamp(self.render_engine, ob, name)]\n        return name\n\n    def get_rect_lamp(self, ob, group_prefix):\n        name = group_prefix + \"__\" + escape_name(ob.name)\n        self.objects += [RectLamp(self.render_engine, ob, name)]\n        return name\n\n\n#=========================================================================\n\n\nclass Mesh:\n    \"\"\" Holds data for a mesh to be exported.\n    \"\"\"\n    def __init__(self, render_engine, ob, name):\n        self.ob = ob\n        self.name = name\n        self.needs_mb = needs_def_mb(self.ob)\n        self.time_meshes = []\n\n    def take_sample(self, render_engine, scene, time):\n        if len(self.time_meshes) == 0 or self.needs_mb:\n            render_engine.update_stats(\"\", \"Psychopath: Collecting '{}' at time {}\".format(self.ob.name, time))\n            self.time_meshes += [self.ob.to_mesh(scene, True, 'RENDER')]\n    \n    def cleanup(self):\n        for mesh in self.time_meshes:\n            bpy.data.meshes.remove(mesh)\n\n    def export(self, render_engine, w):\n        render_engine.update_stats(\"\", \"Psychopath: Exporting %s\" % self.ob.name)\n\n        if self.ob.data.psychopath.is_subdivision_surface == False:\n            # Exporting normal mesh\n            w.write(\"MeshSurface $%s {\\n\" % self.name)\n            w.indent()\n        else:\n            # Exporting subdivision surface cage\n            w.write(\"SubdivisionSurface $%s {\\n\" % self.name)\n            w.indent()\n\n        # Write vertices and (if it's smooth shaded) normals\n        for ti in range(len(self.time_meshes)):\n            w.write(\"Vertices [\")\n            w.write(\" \".join([(\"%f\" % i) for vert in self.time_meshes[ti].vertices for i in vert.co]), False)\n            w.write(\"]\\n\", False)\n            if self.time_meshes[0].polygons[0].use_smooth and self.ob.data.psychopath.is_subdivision_surface == False:\n                w.write(\"Normals [\")\n                w.write(\" \".join([(\"%f\" % i) for vert in self.time_meshes[ti].vertices for i in vert.normal]), False)\n                w.write(\"]\\n\", False)\n\n        # Write face vertex counts\n        w.write(\"FaceVertCounts [\")\n        w.write(\" \".join([(\"%d\" % len(p.vertices)) for p in self.time_meshes[0].polygons]), False)\n        w.write(\"]\\n\", False)\n\n        # Write face vertex indices\n        w.write(\"FaceVertIndices [\")\n        w.write(\" \".join([(\"%d\"%v) for p in self.time_meshes[0].polygons for v in p.vertices]), False)\n        w.write(\"]\\n\", False)\n\n        # MeshSurface/SubdivisionSurface section end\n        w.unindent()\n        w.write(\"}\\n\")\n\n\nclass SphereLamp:\n    \"\"\" Holds data for a sphere light to be exported.\n    \"\"\"\n    def __init__(self, render_engine, ob, name):\n        self.ob = ob\n        self.name = name\n        self.time_col = []\n        self.time_rad = []\n\n    def take_sample(self, render_engine, scene, time):\n        render_engine.update_stats(\"\", \"Psychopath: Collecting '{}' at time {}\".format(self.ob.name, time))\n\n        if self.ob.data.psychopath.color_type == 'Rec709':\n            self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)]\n        elif self.ob.data.psychopath.color_type == 'Blackbody':\n            self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)]\n        elif self.ob.data.psychopath.color_type == 'ColorTemperature':\n            self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)]\n\n        self.time_rad += [self.ob.data.shadow_soft_size]\n\n    def cleanup(self):\n        pass\n    \n    def export(self, render_engine, w):\n        render_engine.update_stats(\"\", \"Psychopath: Exporting %s\" % self.ob.name)\n\n        w.write(\"SphereLight $%s {\\n\" % self.name)\n        w.indent()\n        for col in self.time_col:\n            if col[0] == 'Rec709':\n                w.write(\"Color [rec709, %f %f %f]\\n\" % (col[1][0], col[1][1], col[1][2]))\n            elif col[0] == 'Blackbody':\n                w.write(\"Color [blackbody, %f %f]\\n\" % (col[1], col[2]))\n            elif col[0] == 'ColorTemperature':\n                w.write(\"Color [color_temperature, %f %f]\\n\" % (col[1], col[2]))\n        for rad in self.time_rad:\n            w.write(\"Radius [%f]\\n\" % rad)\n\n        w.unindent()\n        w.write(\"}\\n\")\n\n\nclass RectLamp:\n    \"\"\" Holds data for a rectangular light to be exported.\n    \"\"\"\n    def __init__(self, render_engine, ob, name):\n        self.ob = ob\n        self.name = name\n        self.time_col = []\n        self.time_dim = []\n\n    def take_sample(self, render_engine, scene, time):\n        render_engine.update_stats(\"\", \"Psychopath: Collecting '{}' at time {}\".format(self.ob.name, time))\n\n        if self.ob.data.psychopath.color_type == 'Rec709':\n            self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)]\n        elif self.ob.data.psychopath.color_type == 'Blackbody':\n            self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)]\n        elif self.ob.data.psychopath.color_type == 'ColorTemperature':\n            self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)]\n\n        if self.ob.data.shape == 'RECTANGLE':\n            self.time_dim += [(self.ob.data.size, self.ob.data.size_y)]\n        else:\n            self.time_dim += [(self.ob.data.size, self.ob.data.size)]\n    \n    def cleanup(self):\n        pass\n    \n    def export(self, render_engine, w):\n        render_engine.update_stats(\"\", \"Psychopath: Exporting %s\" % self.ob.name)\n\n        w.write(\"RectangleLight $%s {\\n\" % self.name)\n        w.indent()\n        for col in self.time_col:\n            if col[0] == 'Rec709':\n                w.write(\"Color [rec709, %f %f %f]\\n\" % (col[1][0], col[1][1], col[1][2]))\n            elif col[0] == 'Blackbody':\n                w.write(\"Color [blackbody, %f %f]\\n\" % (col[1], col[2]))\n            elif col[0] == 'ColorTemperature':\n                w.write(\"Color [color_temperature, %f %f]\\n\" % (col[1], col[2]))\n        for dim in self.time_dim:\n            w.write(\"Dimensions [%f %f]\\n\" % dim)\n\n        w.unindent()\n        w.write(\"}\\n\")\n\n\nclass Instance:\n    def __init__(self, render_engine, ob, data_name):\n        self.ob = ob\n        self.data_name = data_name\n        self.needs_mb = needs_xform_mb(self.ob)\n        self.time_xforms = []\n\n    def take_sample(self, render_engine, time, translation_offset):\n        if len(self.time_xforms) == 0 or self.needs_mb:\n            render_engine.update_stats(\"\", \"Psychopath: Collecting '{}' xforms at time {}\".format(self.ob.name, time))\n            mat = self.ob.matrix_world.copy()\n            mat[0][3] += translation_offset[0]\n            mat[1][3] += translation_offset[1]\n            mat[2][3] += translation_offset[2]\n            self.time_xforms += [mat]\n\n    def export(self, render_engine, w):\n        render_engine.update_stats(\"\", \"Psychopath: Exporting %s\" % self.ob.name)\n\n        w.write(\"Instance {\\n\")\n        w.indent()\n        w.write(\"Data [$%s]\\n\" % self.data_name)\n        for mat in self.time_xforms:\n            w.write(\"Transform [%s]\\n\" % mat2str(mat.inverted()))\n        for ms in self.ob.material_slots:\n            if ms != None:\n                w.write(\"SurfaceShaderBind [$%s]\\n\" % escape_name(ms.material.name))\n                break\n        w.unindent()\n        w.write(\"}\\n\")\n\n\nclass Material:\n    def __init__(self, render_engine, material):\n        self.mat = material\n\n    def take_sample(self, render_engine, time, translation_offset):\n        # TODO: motion blur of material settings\n        pass\n\n    def export(self, render_engine, w):\n        render_engine.update_stats(\"\", \"Psychopath: Exporting %s\" % self.mat.name)\n\n        w.write(\"SurfaceShader $%s {\\n\" % escape_name(self.mat.name))\n        w.indent()\n        if self.mat.psychopath.surface_shader_type == 'Emit':\n            w.write(\"Type [Emit]\\n\")\n            if self.mat.psychopath.color_type == 'Rec709':\n                col = self.mat.psychopath.color\n                w.write(\"Color [rec709, %f %f %f]\\n\" % (\n                    col[0], col[1], col[2],\n                ))\n            elif self.mat.psychopath.color_type == 'Blackbody':\n                w.write(\"Color [blackbody, %f %f]\\n\" % (\n                    self.mat.psychopath.color_blackbody_temp,\n                    1.0,\n                ))\n            elif self.mat.psychopath.color_type == 'ColorTemperature':\n                w.write(\"Color [color_temperature, %f %f]\\n\" % (\n                    self.mat.psychopath.color_blackbody_temp,\n                    1.0,\n                ))\n        elif self.mat.psychopath.surface_shader_type == 'Lambert':\n            w.write(\"Type [Lambert]\\n\")\n            if self.mat.psychopath.color_type == 'Rec709':\n                col = self.mat.psychopath.color\n                w.write(\"Color [rec709, %f %f %f]\\n\" % (\n                    col[0], col[1], col[2],\n                ))\n            elif self.mat.psychopath.color_type == 'Blackbody':\n                w.write(\"Color [blackbody, %f %f]\\n\" % (\n                    self.mat.psychopath.color_blackbody_temp,\n                    1.0,\n                ))\n            elif self.mat.psychopath.color_type == 'ColorTemperature':\n                w.write(\"Color [color_temperature, %f %f]\\n\" % (\n                    self.mat.psychopath.color_blackbody_temp,\n                    1.0,\n                ))\n        elif self.mat.psychopath.surface_shader_type == 'GGX':\n            w.write(\"Type [GGX]\\n\")\n            if self.mat.psychopath.color_type == 'Rec709':\n                col = self.mat.psychopath.color\n                w.write(\"Color [rec709, %f %f %f]\\n\" % (\n                    col[0], col[1], col[2],\n                ))\n            elif self.mat.psychopath.color_type == 'Blackbody':\n                w.write(\"Color [blackbody, %f %f]\\n\" % (\n                    self.mat.psychopath.color_blackbody_temp,\n                    1.0,\n                ))\n            elif self.mat.psychopath.color_type == 'ColorTemperature':\n                w.write(\"Color [color_temperature, %f %f]\\n\" % (\n                    self.mat.psychopath.color_blackbody_temp,\n                    1.0,\n                ))\n            w.write(\"Roughness [%f]\\n\" % self.mat.psychopath.roughness)\n            w.write(\"Fresnel [%f]\\n\" % self.mat.psychopath.fresnel)\n        else:\n            raise \"Unsupported surface shader type '%s'\" % self.mat.psychopath.surface_shader_type\n        w.unindent()\n        w.write(\"}\\n\")\n\n    def cleanup(self):\n        pass\n"
  },
  {
    "path": "psychoblend/psy_export.py",
    "content": "import bpy\n\nfrom math import log\n\nfrom .assembly import Assembly\nfrom .util import escape_name, mat2str, ExportCancelled\nfrom .world import World\n\n\nclass IndentedWriter:\n    def __init__(self, file_handle):\n        self.f = file_handle\n        self.indent_level = 0\n        self.indent_size = 4\n\n    def indent(self):\n        self.indent_level += self.indent_size\n\n    def unindent(self):\n        self.indent_level -= self.indent_size\n        if self.indent_level < 0:\n            self.indent_level = 0\n\n    def write(self, text, do_indent=True):\n        if do_indent:\n            self.f.write(bytes(' '*self.indent_level + text, \"utf-8\"))\n        else:\n            self.f.write(bytes(text, \"utf-8\"))\n\n\nclass PsychoExporter:\n    def __init__(self, f, render_engine, scene):\n        self.w = IndentedWriter(f)\n        self.render_engine = render_engine\n        self.scene = scene\n\n        self.mesh_names = {}\n        self.group_names = {}\n\n        # Motion blur segments are rounded down to a power of two\n        if scene.psychopath.motion_blur_segments > 0:\n            self.time_samples = (2**int(log(scene.psychopath.motion_blur_segments, 2))) + 1\n        else:\n            self.time_samples = 1\n\n        # pre-calculate useful values for exporting motion blur\n        self.shutter_start = scene.psychopath.shutter_start\n        self.shutter_diff = (scene.psychopath.shutter_end - scene.psychopath.shutter_start) / max(1, (self.time_samples-1))\n\n        self.fr = scene.frame_current\n\n\n    def set_frame(self, frame, fraction):\n        if fraction >= 0:\n            self.scene.frame_set(frame, fraction)\n        else:\n            self.scene.frame_set(frame-1, 1.0+fraction)\n\n    def export_psy(self):\n        try:\n            self._export_psy()\n        except ExportCancelled:\n            # Cleanup\n            self.scene.frame_set(self.fr)\n            return False\n        else:\n            # Cleanup\n            self.scene.frame_set(self.fr)\n            return True\n\n    def _export_psy(self):\n        # Info\n        self.w.write(\"# Exported from Blender 2.7x\\n\")\n\n        # Scene begin\n        self.w.write(\"\\n\\nScene $%s_fr%d {\\n\" % (escape_name(self.scene.name), self.fr))\n        self.w.indent()\n\n        #######################\n        # Output section begin\n        self.w.write(\"Output {\\n\")\n        self.w.indent()\n\n        self.w.write('Path [\"\"]\\n')\n\n        # Output section end\n        self.w.unindent()\n        self.w.write(\"}\\n\")\n\n        ###############################\n        # RenderSettings section begin\n        self.w.write(\"RenderSettings {\\n\")\n        self.w.indent()\n\n        res_x = int(self.scene.render.resolution_x * (self.scene.render.resolution_percentage / 100))\n        res_y = int(self.scene.render.resolution_y * (self.scene.render.resolution_percentage / 100))\n        self.w.write('Resolution [%d %d]\\n' % (res_x, res_y))\n        self.w.write(\"SamplesPerPixel [%d]\\n\" % self.scene.psychopath.spp)\n        self.w.write(\"DicingRate [%f]\\n\" % self.scene.psychopath.dicing_rate)\n        self.w.write('Seed [%d]\\n' % self.fr)\n\n        # RenderSettings section end\n        self.w.unindent()\n        self.w.write(\"}\\n\")\n\n        ###############################\n        # Export world and object data\n        world = None\n        root_assembly = None\n        try:\n            # Prep for data collection\n            world = World(self.render_engine, self.scene, self.scene.layers, float(res_x) / float(res_y))\n            root_assembly = Assembly(self.render_engine, self.scene.objects, self.scene.layers)\n\n            # Collect data for each time sample\n            for i in range(self.time_samples):\n                time = self.fr + self.shutter_start + (self.shutter_diff*i)\n                self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i))\n                world.take_sample(self.render_engine, self.scene, time)\n                root_assembly.take_sample(self.render_engine, self.scene, time)\n\n            # Export collected data\n            world.export(self.render_engine, self.w)\n            root_assembly.export(self.render_engine, self.w)\n        finally:\n            if world != None:\n                world.cleanup()\n            if root_assembly != None:\n                root_assembly.cleanup()\n\n        # Scene end\n        self.w.unindent()\n        self.w.write(\"}\\n\")\n"
  },
  {
    "path": "psychoblend/render.py",
    "content": "import bpy\nimport time\nimport os\nimport subprocess\nimport base64\nimport struct\nfrom . import psy_export\n\nclass PsychopathRender(bpy.types.RenderEngine):\n    bl_idname = 'PSYCHOPATH_RENDER'\n    bl_label = \"Psychopath\"\n    DELAY = 1.0\n\n    @staticmethod\n    def _locate_binary():\n        addon_prefs = bpy.context.user_preferences.addons[__package__].preferences\n\n        # Use the system preference if its set.\n        psy_binary = addon_prefs.filepath_psychopath\n        if psy_binary:\n            if os.path.exists(psy_binary):\n                return psy_binary\n            else:\n                print(\"User Preference to psychopath %r NOT FOUND, checking $PATH\" % psy_binary)\n\n        # search the path all os's\n        psy_binary_default = \"psychopath\"\n\n        os_path_ls = os.getenv(\"PATH\").split(':') + [\"\"]\n\n        for dir_name in os_path_ls:\n            psy_binary = os.path.join(dir_name, psy_binary_default)\n            if os.path.exists(psy_binary):\n                return psy_binary\n        return \"\"\n\n    def _start_psychopath(self, scene, psy_filepath, use_stdin, crop):\n        psy_binary = PsychopathRender._locate_binary()\n        if not psy_binary:\n            print(\"Psychopath: could not execute psychopath, possibly Psychopath isn't installed\")\n            return False\n\n        # Figure out command line options\n        args = []\n        if crop != None:\n            args += [\"--crop\", str(crop[0]), str(self.size_y - crop[3]), str(crop[2] - 1), str(self.size_y - crop[1] - 1)]\n        if use_stdin:\n            args += [\"--spb\", str(scene.psychopath.max_samples_per_bucket), \"--serialized_output\", \"--use_stdin\"]\n        else:\n            args += [\"--spb\", str(scene.psychopath.max_samples_per_bucket), \"--serialized_output\", \"-i\", psy_filepath]\n\n        # Start Rendering!\n        try:\n            self._process = subprocess.Popen([psy_binary] + args, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE)\n        except OSError:\n            # TODO, report api\n            print(\"Psychopath: could not execute '%s'\" % psy_binary)\n            import traceback\n            traceback.print_exc()\n            print (\"***-DONE-***\")\n            return False\n\n        return True\n\n    def _draw_bucket(self, crop, bucket_info, pixels_encoded):\n        if crop != None:\n            x = bucket_info[0] - crop[0]\n            y = self.size_y - bucket_info[3] - crop[1]\n        else:\n            x = bucket_info[0]\n            y = self.size_y - bucket_info[3]\n        width = bucket_info[2] - bucket_info[0]\n        height = bucket_info[3] - bucket_info[1]\n\n        # Decode pixel data\n        pixels = [p for p in struct.iter_unpack(\"ffff\", base64.b64decode(pixels_encoded))]\n        pixels_flipped = []\n        for i in range(height):\n            n = height - i - 1\n            pixels_flipped += pixels[n*width:(n+1)*width]\n\n        # Write pixel data to render image\n        result = self.begin_result(x, y, width, height)\n        lay = result.layers[0].passes[\"Combined\"]\n        lay.rect = pixels_flipped\n        self.end_result(result)\n\n    def render(self, scene):\n        self._process = None\n        try:\n            self._render(scene)\n        except:\n            if self._process != None:\n                self._process.terminate()\n            raise\n\n    def _render(self, scene):\n        # has to be called to update the frame on exporting animations\n        scene.frame_set(scene.frame_current)\n\n        export_path = scene.psychopath.export_path.strip()\n        use_stdin = False\n\n        r = scene.render\n        # compute resolution\n        self.size_x = int(r.resolution_x * r.resolution_percentage / 100)\n        self.size_y = int(r.resolution_y * r.resolution_percentage / 100)\n\n        # Calculate border cropping, if any.\n        if scene.render.use_border == True:\n            minx = r.resolution_x * scene.render.border_min_x * (r.resolution_percentage / 100)\n            miny = r.resolution_y * scene.render.border_min_y * (r.resolution_percentage / 100)\n            maxx = r.resolution_x * scene.render.border_max_x * (r.resolution_percentage / 100)\n            maxy = r.resolution_y * scene.render.border_max_y * (r.resolution_percentage / 100)\n            crop = (int(minx), int(miny), int(maxx), int(maxy))\n        else:\n            crop = None\n\n        # Are we using an output file or standard in/out?\n        if export_path != \"\":\n            export_path += \"_%d.psy\" % scene.frame_current\n        else:\n            # We'll write directly to Psychopath's stdin\n            use_stdin = True\n\n        if use_stdin:\n            # Start rendering\n            if not self._start_psychopath(scene, export_path, use_stdin, crop):\n                self.update_stats(\"\", \"Psychopath: Not found\")\n                return\n\n            self.update_stats(\"\", \"Psychopath: Collecting...\")\n            # Export to Psychopath's stdin\n            if not psy_export.PsychoExporter(self._process.stdin, self, scene).export_psy():\n                # Render cancelled in the middle of exporting,\n                # so just return.\n                self._process.terminate()\n                return\n            self._process.stdin.write(bytes(\"__PSY_EOF__\", \"utf-8\"))\n            self._process.stdin.flush()\n        else:\n            # Export to file\n            self.update_stats(\"\", \"Psychopath: Exporting data from Blender\")\n            with open(export_path, 'w+b') as f:\n                if not psy_export.PsychoExporter(f, self, scene).export_psy():\n                    # Render cancelled in the middle of exporting,\n                    # so just return.\n                    return\n\n            # Start rendering\n            self.update_stats(\"\", \"Psychopath: Rendering from %s\" % export_path)\n            if not self._start_psychopath(scene, export_path, use_stdin, crop):\n                self.update_stats(\"\", \"Psychopath: Not found\")\n                return\n\n        self.update_stats(\"\", \"Psychopath: Building\")\n\n        # If we can, make the render process's stdout non-blocking.  The\n        # benefit of this is that canceling the render won't block waiting\n        # for the next piece of input.\n        try:\n            import fcntl\n            fd = self._process.stdout.fileno()\n            fl = fcntl.fcntl(fd, fcntl.F_GETFL)\n            fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)\n        except:\n            print(\"NOTE: Can't make Psychopath's stdout non-blocking, so canceling renders may take a moment to respond.\")\n\n        # Process output from rendering process\n        reached_first_bucket = False\n        output = b\"\"\n        render_process_finished = False\n        all_output_consumed = False\n        while not (render_process_finished and all_output_consumed):\n            if self._process.poll() != None:\n                render_process_finished = True\n\n            # Check for render cancel\n            if self.test_break():\n                self._process.terminate()\n                break\n\n            # Get render output from stdin\n            tmp = self._process.stdout.read1(2**16)\n            if len(tmp) == 0:\n                time.sleep(0.0001) # Don't spin on the CPU\n                if render_process_finished:\n                    all_output_consumed = True\n                continue\n            output += tmp\n            outputs = output.split(b'DIV\\n')\n\n            # Skip render process output until we hit the first bucket.\n            # (The stuff before it is just informational printouts.)\n            if not reached_first_bucket:\n                if len(outputs) > 1:\n                    reached_first_bucket = True\n                    outputs = outputs[1:]\n                else:\n                    continue\n\n            self.update_stats(\"\", \"Psychopath: Rendering\")\n\n            # Clear output buffer, since it's all in 'outputs' now.\n            output = b\"\"\n\n            # Process buckets\n            for bucket in outputs:\n                if len(bucket) == 0:\n                    continue\n\n                if bucket[-11:] == b'BUCKET_END\\n':\n                    # Parse bucket text\n                    contents = bucket.split(b'\\n')\n                    percentage = contents[0]\n                    bucket_info = [int(i) for i in contents[1].split(b' ')]\n                    pixels = contents[2]\n\n                    # Draw the bucket\n                    self._draw_bucket(crop, bucket_info, pixels)\n\n                    # Update render progress bar\n                    try:\n                        progress = float(percentage[:-1])\n                    except ValueError:\n                        pass\n                    finally:\n                        self.update_progress(progress/100)\n                else:\n                    output += bucket\n\ndef register():\n    bpy.utils.register_class(PsychopathRender)\n\ndef unregister():\n    bpy.utils.unregister_class(PsychopathRender)\n"
  },
  {
    "path": "psychoblend/ui.py",
    "content": "import bpy\n\n# Use some of the existing buttons.\nfrom bl_ui import properties_render\nproperties_render.RENDER_PT_render.COMPAT_ENGINES.add('PSYCHOPATH_RENDER')\nproperties_render.RENDER_PT_dimensions.COMPAT_ENGINES.add('PSYCHOPATH_RENDER')\nproperties_render.RENDER_PT_output.COMPAT_ENGINES.add('PSYCHOPATH_RENDER')\ndel properties_render\n\nfrom bl_ui import properties_data_camera\nproperties_data_camera.DATA_PT_lens.COMPAT_ENGINES.add('PSYCHOPATH_RENDER')\nproperties_data_camera.DATA_PT_camera.COMPAT_ENGINES.add('PSYCHOPATH_RENDER')\nproperties_data_camera.DATA_PT_camera_display.COMPAT_ENGINES.add('PSYCHOPATH_RENDER')\nproperties_data_camera.DATA_PT_custom_props_camera.COMPAT_ENGINES.add('PSYCHOPATH_RENDER')\ndel properties_data_camera\n\nclass PsychopathPanel():\n    COMPAT_ENGINES = {'PSYCHOPATH_RENDER'}\n\n    @classmethod\n    def poll(cls, context):\n        rd = context.scene.render\n        return (rd.use_game_engine is False) and (rd.engine in cls.COMPAT_ENGINES)\n\n\nclass RENDER_PT_psychopath_render_settings(PsychopathPanel, bpy.types.Panel):\n    bl_label = \"Render Settings\"\n    bl_space_type = 'PROPERTIES'\n    bl_region_type = 'WINDOW'\n    bl_context = \"render\"\n\n    def draw(self, context):\n        scene = context.scene\n        layout = self.layout\n\n        col = layout.column()\n\n        col.label(text=\"Sampling\")\n        col.prop(scene.psychopath, \"spp\")\n\n        col.label(text=\"Dicing\")\n        col.prop(scene.psychopath, \"dicing_rate\")\n\n        col.label(text=\"Motion Blur\")\n        col.prop(scene.psychopath, \"motion_blur_segments\")\n        col.prop(scene.psychopath, \"shutter_start\")\n        col.prop(scene.psychopath, \"shutter_end\")\n\n        col.label(text=\"Performance\")\n        col.prop(scene.psychopath, \"max_samples_per_bucket\")\n\n\nclass RENDER_PT_psychopath_export_settings(PsychopathPanel, bpy.types.Panel):\n    bl_label = \"Export Settings\"\n    bl_space_type = 'PROPERTIES'\n    bl_region_type = 'WINDOW'\n    bl_context = \"render\"\n\n    def draw(self, context):\n        scene = context.scene\n        layout = self.layout\n\n        col = layout.column()\n        col.prop(scene.psychopath, \"export_path\")\n\n\nclass WORLD_PT_psychopath_background(PsychopathPanel, bpy.types.Panel):\n    bl_label = \"Background\"\n    bl_space_type = 'PROPERTIES'\n    bl_region_type = 'WINDOW'\n    bl_context = \"world\"\n\n    @classmethod\n    def poll(cls, context):\n        return context.world and PsychopathPanel.poll(context)\n\n    def draw(self, context):\n        layout = self.layout\n\n        world = context.world\n        layout.prop(world, \"horizon_color\", text=\"Color\")\n\n\nclass DATA_PT_psychopath_camera_dof(PsychopathPanel, bpy.types.Panel):\n    bl_label = \"Depth of Field\"\n    bl_space_type = 'PROPERTIES'\n    bl_region_type = 'WINDOW'\n    bl_context = \"data\"\n\n    @classmethod\n    def poll(cls, context):\n        engine = context.scene.render.engine\n        return context.camera and PsychopathPanel.poll(context)\n\n    def draw(self, context):\n        ob = context.active_object\n        layout = self.layout\n\n        col = layout.column()\n\n        col.prop(ob.data, \"dof_object\")\n        col.prop(ob.data, \"dof_distance\")\n        col.prop(ob.data.psychopath, \"aperture_radius\")\n\n\nclass DATA_PT_psychopath_lamp(PsychopathPanel, bpy.types.Panel):\n    bl_label = \"Lamp\"\n    bl_space_type = 'PROPERTIES'\n    bl_region_type = 'WINDOW'\n    bl_context = \"data\"\n\n    @classmethod\n    def poll(cls, context):\n        engine = context.scene.render.engine\n        return context.lamp and PsychopathPanel.poll(context)\n\n    def draw(self, context):\n        ob = context.active_object\n        layout = self.layout\n\n        col = layout.column()\n\n        row = col.row()\n        row.prop(ob.data, \"type\", expand=True)\n\n        if ob.data.type != 'HEMI' and ob.data.type != 'AREA':\n            col.prop(ob.data, \"shadow_soft_size\")\n\n        col.prop(ob.data.psychopath, \"color_type\")\n\n        if ob.data.psychopath.color_type == 'Rec709':\n            col.prop(ob.data, \"color\")\n        elif ob.data.psychopath.color_type == 'Blackbody' or ob.data.psychopath.color_type == 'ColorTemperature':\n            col.prop(ob.data.psychopath, \"color_blackbody_temp\")\n\n        col.prop(ob.data, \"energy\")\n\n\nclass DATA_PT_psychopath_area_lamp(PsychopathPanel, bpy.types.Panel):\n    bl_label = \"Area Shape\"\n    bl_space_type = 'PROPERTIES'\n    bl_region_type = 'WINDOW'\n    bl_context = \"data\"\n\n    @classmethod\n    def poll(cls, context):\n        lamp = context.lamp\n        engine = context.scene.render.engine\n        return (lamp and lamp.type == 'AREA') and (engine in cls.COMPAT_ENGINES)\n\n    def draw(self, context):\n        layout = self.layout\n\n        lamp = context.lamp\n\n        col = layout.column()\n        col.row().prop(lamp, \"shape\", expand=True)\n        sub = col.row(align=True)\n\n        if lamp.shape == 'SQUARE':\n            sub.prop(lamp, \"size\")\n        elif lamp.shape == 'RECTANGLE':\n            sub.prop(lamp, \"size\", text=\"Size X\")\n            sub.prop(lamp, \"size_y\", text=\"Size Y\")\n\n\nclass DATA_PT_psychopath_mesh(PsychopathPanel, bpy.types.Panel):\n    bl_label = \"Psychopath Mesh Properties\"\n    bl_space_type = 'PROPERTIES'\n    bl_region_type = 'WINDOW'\n    bl_context = \"data\"\n\n    @classmethod\n    def poll(cls, context):\n        engine = context.scene.render.engine\n        return context.mesh and (engine in cls.COMPAT_ENGINES)\n\n    def draw(self, context):\n        layout = self.layout\n\n        mesh = context.mesh\n\n        layout.row().prop(mesh.psychopath, \"is_subdivision_surface\")\n\n\nclass MATERIAL_PT_psychopath_context_material(PsychopathPanel, bpy.types.Panel):\n    bl_label = \"\"\n    bl_space_type = \"PROPERTIES\"\n    bl_region_type = \"WINDOW\"\n    bl_context = \"material\"\n    bl_options = {'HIDE_HEADER'}\n\n    @classmethod\n    def poll(cls, context):\n        return (context.material or context.object) and PsychopathPanel.poll(context)\n\n    def draw(self, context):\n        layout = self.layout\n\n        mat = context.material\n        ob = context.object\n        slot = context.material_slot\n        space = context.space_data\n\n        if ob:\n            row = layout.row()\n\n            row.template_list(\"MATERIAL_UL_matslots\", \"\", ob, \"material_slots\", ob, \"active_material_index\", rows=1)\n\n            col = row.column(align=True)\n            col.operator(\"object.material_slot_add\", icon='ZOOMIN', text=\"\")\n            col.operator(\"object.material_slot_remove\", icon='ZOOMOUT', text=\"\")\n\n            col.menu(\"MATERIAL_MT_specials\", icon='DOWNARROW_HLT', text=\"\")\n\n            if ob.mode == 'EDIT':\n                row = layout.row(align=True)\n                row.operator(\"object.material_slot_assign\", text=\"Assign\")\n                row.operator(\"object.material_slot_select\", text=\"Select\")\n                row.operator(\"object.material_slot_deselect\", text=\"Deselect\")\n\n        split = layout.split(percentage=0.65)\n\n        if ob:\n            split.template_ID(ob, \"active_material\", new=\"material.new\")\n            row = split.row()\n\n            if slot:\n                row.prop(slot, \"link\", text=\"\")\n            else:\n                row.label()\n        elif mat:\n            split.template_ID(space, \"pin_id\")\n            split.separator()\n\n\nclass MATERIAL_PT_psychopath_surface(PsychopathPanel, bpy.types.Panel):\n    bl_label = \"Surface\"\n    bl_space_type = \"PROPERTIES\"\n    bl_region_type = \"WINDOW\"\n    bl_context = \"material\"\n\n    @classmethod\n    def poll(cls, context):\n        return context.material and PsychopathPanel.poll(context)\n\n    def draw(self, context):\n        layout = self.layout\n        col = layout.column()\n\n        mat = context.material\n        col.prop(mat.psychopath, \"surface_shader_type\")\n\n        col.prop(mat.psychopath, \"color_type\")\n        if mat.psychopath.color_type == 'Rec709':\n            col.prop(mat.psychopath, \"color\")\n        elif mat.psychopath.color_type == 'Blackbody' or mat.psychopath.color_type == 'ColorTemperature':\n            col.prop(mat.psychopath, \"color_blackbody_temp\")\n\n        if mat.psychopath.surface_shader_type == 'GTR':\n            layout.prop(mat.psychopath, \"roughness\")\n            layout.prop(mat.psychopath, \"tail_shape\")\n            layout.prop(mat.psychopath, \"fresnel\")\n\n        if mat.psychopath.surface_shader_type == 'GGX':\n            layout.prop(mat.psychopath, \"roughness\")\n            layout.prop(mat.psychopath, \"fresnel\")\n\n\ndef register():\n    bpy.utils.register_class(RENDER_PT_psychopath_render_settings)\n    bpy.utils.register_class(RENDER_PT_psychopath_export_settings)\n    bpy.utils.register_class(WORLD_PT_psychopath_background)\n    bpy.utils.register_class(DATA_PT_psychopath_camera_dof)\n    bpy.utils.register_class(DATA_PT_psychopath_mesh)\n    bpy.utils.register_class(DATA_PT_psychopath_lamp)\n    bpy.utils.register_class(DATA_PT_psychopath_area_lamp)\n    bpy.utils.register_class(MATERIAL_PT_psychopath_context_material)\n    bpy.utils.register_class(MATERIAL_PT_psychopath_surface)\n\ndef unregister():\n    bpy.utils.unregister_class(RENDER_PT_psychopath_render_settings)\n    bpy.utils.unregister_class(RENDER_PT_psychopath_export_settings)\n    bpy.utils.unregister_class(WORLD_PT_psychopath_background)\n    bpy.utils.unregister_class(DATA_PT_psychopath_camera_dof)\n    bpy.utils.register_class(DATA_PT_psychopath_mesh)\n    bpy.utils.unregister_class(DATA_PT_psychopath_lamp)\n    bpy.utils.unregister_class(DATA_PT_psychopath_area_lamp)\n    bpy.utils.unregister_class(MATERIAL_PT_psychopath_context_material)\n    bpy.utils.unregister_class(MATERIAL_PT_psychopath_surface)\n"
  },
  {
    "path": "psychoblend/util.py",
    "content": "class ExportCancelled(Exception):\n    \"\"\" Indicates that the render was cancelled in the middle of exporting\n        the scene file.\n    \"\"\"\n    pass\n\n\ndef mat2str(m):\n    \"\"\" Converts a matrix into a single-line string of values.\n    \"\"\"\n    s = \"\"\n    for j in range(4):\n        for i in range(4):\n            s += (\" %f\" % m[i][j])\n    return s[1:]\n\n\ndef needs_def_mb(ob):\n    \"\"\" Determines if the given object needs to be exported with\n        deformation motion blur or not.\n    \"\"\"\n    anim = ob.animation_data\n    no_anim_data = anim == None or (anim.action == None and len(anim.nla_tracks) == 0 and len(anim.drivers) == 0)\n\n    for mod in ob.modifiers:\n        if mod.type == 'SUBSURF':\n            pass\n        elif mod.type == 'MULTIRES':\n            pass\n        elif mod.type == 'MIRROR':\n            if mod.mirror_object == None:\n                pass\n            else:\n                return True\n        elif mod.type == 'BEVEL' and no_anim_data:\n            pass\n        elif mod.type == 'EDGE_SPLIT' and no_anim_data:\n            pass\n        elif mod.type == 'SOLIDIFY' and no_anim_data:\n            pass\n        elif mod.type == 'MASK' and no_anim_data:\n            pass\n        elif mod.type == 'REMESH' and no_anim_data:\n            pass\n        elif mod.type == 'TRIANGULATE' and no_anim_data:\n            pass\n        elif mod.type == 'WIREFRAME' and no_anim_data:\n            pass\n        else:\n            return True\n\n    if ob.type == 'MESH':\n        if ob.data.shape_keys == None:\n            pass\n        else:\n            return True\n\n    return False\n\ndef escape_name(name):\n    name = name.replace(\"\\\\\", \"\\\\\\\\\")\n    name = name.replace(\" \", \"\\\\ \")\n    name = name.replace(\"$\", \"\\\\$\")\n    name = name.replace(\"[\", \"\\\\[\")\n    name = name.replace(\"]\", \"\\\\]\")\n    name = name.replace(\"{\", \"\\\\{\")\n    name = name.replace(\"}\", \"\\\\}\")\n    return name\n\n\ndef needs_xform_mb(ob):\n    \"\"\" Determines if the given object needs to be exported with\n        transformation motion blur or not.\n    \"\"\"\n    if ob.animation_data != None:\n        return True\n\n    if len(ob.constraints) > 0:\n        return True\n\n    if ob.parent != None:\n        return needs_xform_mb(ob.parent)\n\n    return False"
  },
  {
    "path": "psychoblend/world.py",
    "content": "import bpy\n\nfrom math import degrees, tan, atan\nfrom mathutils import Vector, Matrix\n\nfrom .util import escape_name, mat2str, ExportCancelled\n\nclass World:\n    def __init__(self, render_engine, scene, visible_layers, aspect_ratio):\n        self.background_shader = BackgroundShader(render_engine, scene.world)\n        self.camera = Camera(render_engine, scene.camera, aspect_ratio)\n        self.lights = []\n\n        # Collect infinite-extent light sources.\n        # TODO: also get sun lamps inside group instances.\n        for ob in scene.objects:\n            if ob.type == 'LAMP' and ob.data.type == 'SUN':\n                name = escape_name(ob.name)\n                self.lights += [DistantDiskLamp(ob, name)]\n\n    def take_sample(self, render_engine, scene, time):\n        self.camera.take_sample(render_engine, scene, time)\n\n        for light in self.lights:\n            # Check if render is cancelled\n            if render_engine.test_break():\n                raise ExportCancelled()\n            light.take_sample(render_engine, scene, time)\n\n    def export(self, render_engine, w):\n        self.camera.export(render_engine, w)\n\n        w.write(\"World {\\n\")\n        w.indent()\n\n        self.background_shader.export(render_engine, w)\n\n        for light in self.lights:\n            light.export(render_engine, w)\n\n        w.unindent()\n        w.write(\"}\\n\")\n\n    def cleanup(self):\n        # For future use.  This is run by the calling code when finished,\n        # even if export did not succeed.\n        pass\n\n#================================================================\n\nclass Camera:\n    def __init__(self, render_engine, ob, aspect_ratio):\n        self.ob = ob\n        self.aspect_ratio = aspect_ratio\n\n        self.fovs = []\n        self.aperture_radii = []\n        self.focal_distances = []\n        self.xforms = []\n\n    def take_sample(self, render_engine, scene, time):\n        render_engine.update_stats(\"\", \"Psychopath: Collecting '{}' at time {}\".format(self.ob.name, time))\n\n        # Fov\n        if self.aspect_ratio >= 1.0:\n            self.fovs += [degrees(self.ob.data.angle)]\n        else:\n            self.fovs += [degrees(2.0 * atan(tan(self.ob.data.angle * 0.5) * self.aspect_ratio))]\n\n        # Aperture radius\n        self.aperture_radii += [self.ob.data.psychopath.aperture_radius]\n\n        # Dof distance\n        if self.ob.data.dof_object == None:\n            self.focal_distances += [self.ob.data.dof_distance]\n        else:\n            # TODO: implement DoF object tracking here\n            self.focal_distances += [0.0]\n            print(\"WARNING: DoF object tracking not yet implemented.\")\n\n        # Transform\n        mat = self.ob.matrix_world.copy()\n        matz = Matrix()\n        matz[2][2] = -1\n        self.xforms += [mat * matz]\n\n    def export(self, render_engine, w):\n        render_engine.update_stats(\"\", \"Psychopath: Exporting %s\" % self.ob.name)\n        w.write(\"Camera {\\n\")\n        w.indent()\n\n        for fov in self.fovs:\n            w.write(\"Fov [%f]\\n\" % fov)\n\n        for rad in self.aperture_radii:\n            w.write(\"ApertureRadius [%f]\\n\" % rad)\n\n        for dist in self.focal_distances:\n            w.write(\"FocalDistance [%f]\\n\" % dist)\n\n        for mat in self.xforms:\n            w.write(\"Transform [%s]\\n\" % mat2str(mat))\n\n        w.unindent()\n        w.write(\"}\\n\")\n\n\nclass BackgroundShader:\n    def __init__(self, render_engine, world):\n        self.world = world\n        if self.world != None:\n            self.color = (world.horizon_color[0], world.horizon_color[1], world.horizon_color[2])\n\n    def export(self, render_engine, w):\n        if self.world != None:\n            w.write(\"BackgroundShader {\\n\")\n            w.indent();\n            w.write(\"Type [Color]\\n\")\n            w.write(\"Color [rec709, %f %f %f]\\n\" % self.color)\n            w.unindent()\n            w.write(\"}\\n\")\n\n\nclass DistantDiskLamp:\n    def __init__(self, ob, name):\n        self.ob = ob\n        self.name = name\n        self.time_col = []\n        self.time_dir = []\n        self.time_rad = []\n\n    def take_sample(self, render_engine, scene, time):\n        render_engine.update_stats(\"\", \"Psychopath: Collecting '{}' at time {}\".format(self.ob.name, time))\n        self.time_dir += [tuple(self.ob.matrix_world.to_3x3() * Vector((0, 0, -1)))]\n\n        if self.ob.data.psychopath.color_type == 'Rec709':\n            self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)]\n        elif self.ob.data.psychopath.color_type == 'Blackbody':\n            self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)]\n        elif self.ob.data.psychopath.color_type == 'ColorTemperature':\n            self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)]\n\n        self.time_rad += [self.ob.data.shadow_soft_size]\n\n    def export(self, render_engine, w):\n        render_engine.update_stats(\"\", \"Psychopath: Exporting %s\" % self.ob.name)\n        w.write(\"DistantDiskLight $%s {\\n\" % self.name)\n        w.indent()\n        for direc in self.time_dir:\n            w.write(\"Direction [%f %f %f]\\n\" % (direc[0], direc[1], direc[2]))\n        for col in self.time_col:\n            if col[0] == 'Rec709':\n                w.write(\"Color [rec709, %f %f %f]\\n\" % (col[1][0], col[1][1], col[1][2]))\n            elif col[0] == 'Blackbody':\n                w.write(\"Color [blackbody, %f %f]\\n\" % (col[1], col[2]))\n            elif col[0] == 'ColorTemperature':\n                w.write(\"Color [color_temperature, %f %f]\\n\" % (col[1], col[2]))\n        for rad in self.time_rad:\n            w.write(\"Radius [%f]\\n\" % rad)\n\n        w.unindent()\n        w.write(\"}\\n\")\n"
  },
  {
    "path": "src/accel/bvh.rs",
    "content": "#![allow(dead_code)]\n\nuse mem_arena::MemArena;\n\nuse crate::{\n    algorithm::partition, bbox::BBox, boundable::Boundable, lerp::lerp_slice, ray::AccelRay,\n    timer::Timer,\n};\n\nuse super::{\n    bvh_base::{BVHBase, BVHBaseNode, BVH_MAX_DEPTH},\n    ACCEL_NODE_RAY_TESTS, ACCEL_TRAV_TIME,\n};\n\n#[derive(Copy, Clone, Debug)]\npub struct BVH<'a> {\n    root: Option<&'a BVHNode<'a>>,\n    depth: usize,\n}\n\n#[derive(Copy, Clone, Debug)]\npub enum BVHNode<'a> {\n    Internal {\n        bounds_len: u16,\n        split_axis: u8,\n        bounds_start: &'a BBox,\n        children: (&'a BVHNode<'a>, &'a BVHNode<'a>),\n    },\n\n    Leaf {\n        bounds_start: &'a BBox,\n        bounds_len: u16,\n        object_range: (usize, usize),\n    },\n}\n\nimpl<'a> BVH<'a> {\n    pub fn from_objects<'b, T, F>(\n        arena: &'a MemArena,\n        objects: &mut [T],\n        objects_per_leaf: usize,\n        bounder: F,\n    ) -> BVH<'a>\n    where\n        F: 'b + Fn(&T) -> &'b [BBox],\n    {\n        if objects.is_empty() {\n            BVH {\n                root: None,\n                depth: 0,\n            }\n        } else {\n            let base = BVHBase::from_objects(objects, objects_per_leaf, bounder);\n\n            BVH {\n                root: Some(BVH::construct_from_base(\n                    arena,\n                    &base,\n                    base.root_node_index(),\n                )),\n                depth: base.depth,\n            }\n        }\n    }\n\n    pub fn tree_depth(&self) -> usize {\n        self.depth\n    }\n\n    pub fn traverse<T, F>(&self, rays: &mut [AccelRay], objects: &[T], mut obj_ray_test: F)\n    where\n        F: FnMut(&T, &mut [AccelRay]),\n    {\n        if self.root.is_none() {\n            return;\n        }\n\n        let mut timer = Timer::new();\n        let mut trav_time: f64 = 0.0;\n        let mut node_tests: u64 = 0;\n\n        let ray_sign = [\n            rays[0].dir_inv.x() >= 0.0,\n            rays[0].dir_inv.y() >= 0.0,\n            rays[0].dir_inv.z() >= 0.0,\n        ];\n\n        // +2 of max depth for root and last child\n        let mut node_stack = [self.root.unwrap(); BVH_MAX_DEPTH + 2];\n        let mut ray_i_stack = [rays.len(); BVH_MAX_DEPTH + 2];\n        let mut stack_ptr = 1;\n\n        while stack_ptr > 0 {\n            node_tests += ray_i_stack[stack_ptr] as u64;\n            match *node_stack[stack_ptr] {\n                BVHNode::Internal {\n                    children,\n                    bounds_start,\n                    bounds_len,\n                    split_axis,\n                } => {\n                    let bounds =\n                        unsafe { std::slice::from_raw_parts(bounds_start, bounds_len as usize) };\n                    let part = partition(&mut rays[..ray_i_stack[stack_ptr]], |r| {\n                        (!r.is_done()) && lerp_slice(bounds, r.time).intersect_accel_ray(r)\n                    });\n                    if part > 0 {\n                        ray_i_stack[stack_ptr] = part;\n                        ray_i_stack[stack_ptr + 1] = part;\n                        if ray_sign[split_axis as usize] {\n                            node_stack[stack_ptr] = children.1;\n                            node_stack[stack_ptr + 1] = children.0;\n                        } else {\n                            node_stack[stack_ptr] = children.0;\n                            node_stack[stack_ptr + 1] = children.1;\n                        }\n                        stack_ptr += 1;\n                    } else {\n                        stack_ptr -= 1;\n                    }\n                }\n\n                BVHNode::Leaf {\n                    object_range,\n                    bounds_start,\n                    bounds_len,\n                } => {\n                    let bounds =\n                        unsafe { std::slice::from_raw_parts(bounds_start, bounds_len as usize) };\n                    let part = partition(&mut rays[..ray_i_stack[stack_ptr]], |r| {\n                        (!r.is_done()) && lerp_slice(bounds, r.time).intersect_accel_ray(r)\n                    });\n\n                    trav_time += timer.tick() as f64;\n\n                    if part > 0 {\n                        for obj in &objects[object_range.0..object_range.1] {\n                            obj_ray_test(obj, &mut rays[..part]);\n                        }\n                    }\n\n                    timer.tick();\n\n                    stack_ptr -= 1;\n                }\n            }\n        }\n\n        trav_time += timer.tick() as f64;\n        ACCEL_TRAV_TIME.with(|att| {\n            let v = att.get();\n            att.set(v + trav_time);\n        });\n        ACCEL_NODE_RAY_TESTS.with(|anv| {\n            let v = anv.get();\n            anv.set(v + node_tests);\n        });\n    }\n\n    #[allow(clippy::mut_from_ref)]\n    fn construct_from_base(\n        arena: &'a MemArena,\n        base: &BVHBase,\n        node_index: usize,\n    ) -> &'a mut BVHNode<'a> {\n        match base.nodes[node_index] {\n            BVHBaseNode::Internal {\n                bounds_range,\n                children_indices,\n                split_axis,\n            } => {\n                let node = unsafe { arena.alloc_uninitialized_with_alignment::<BVHNode>(32) };\n\n                let bounds = arena\n                    .copy_slice_with_alignment(&base.bounds[bounds_range.0..bounds_range.1], 32);\n                let child1 = BVH::construct_from_base(arena, base, children_indices.0);\n                let child2 = BVH::construct_from_base(arena, base, children_indices.1);\n\n                *node = BVHNode::Internal {\n                    bounds_len: bounds.len() as u16,\n                    split_axis: split_axis,\n                    bounds_start: &bounds[0],\n                    children: (child1, child2),\n                };\n\n                node\n            }\n\n            BVHBaseNode::Leaf {\n                bounds_range,\n                object_range,\n            } => {\n                let node = unsafe { arena.alloc_uninitialized::<BVHNode>() };\n                let bounds = arena.copy_slice(&base.bounds[bounds_range.0..bounds_range.1]);\n\n                *node = BVHNode::Leaf {\n                    bounds_start: &bounds[0],\n                    bounds_len: bounds.len() as u16,\n                    object_range: object_range,\n                };\n\n                node\n            }\n        }\n    }\n}\n\nlazy_static! {\n    static ref DEGENERATE_BOUNDS: [BBox; 1] = [BBox::new()];\n}\n\nimpl<'a> Boundable for BVH<'a> {\n    fn bounds(&self) -> &[BBox] {\n        match self.root {\n            None => &DEGENERATE_BOUNDS[..],\n            Some(root) => match *root {\n                BVHNode::Internal {\n                    bounds_start,\n                    bounds_len,\n                    ..\n                }\n                | BVHNode::Leaf {\n                    bounds_start,\n                    bounds_len,\n                    ..\n                } => unsafe { std::slice::from_raw_parts(bounds_start, bounds_len as usize) },\n            },\n        }\n    }\n}\n"
  },
  {
    "path": "src/accel/bvh4.rs",
    "content": "//! This BVH4 implementation is based on the ideas from the paper\n//! \"Efficient Ray Tracing Kernels for Modern CPU Architectures\"\n//! by Fuetterling et al.\n\n#![allow(dead_code)]\n\nuse std::mem::{transmute, MaybeUninit};\n\nuse glam::BVec4A;\n\nuse kioku::Arena;\n\nuse crate::{\n    bbox::BBox,\n    bbox4::BBox4,\n    boundable::Boundable,\n    lerp::lerp_slice,\n    math::Vector,\n    ray::{RayBatch, RayStack},\n};\n\nuse super::{\n    bvh_base::{BVHBase, BVHBaseNode, BVH_MAX_DEPTH},\n    ACCEL_NODE_RAY_TESTS,\n};\n\nuse bvh_order::{calc_traversal_code, SplitAxes, TRAVERSAL_TABLE};\n\npub fn ray_code(dir: Vector) -> usize {\n    let ray_sign_is_neg = [dir.x() < 0.0, dir.y() < 0.0, dir.z() < 0.0];\n    ray_sign_is_neg[0] as usize\n        + ((ray_sign_is_neg[1] as usize) << 1)\n        + ((ray_sign_is_neg[2] as usize) << 2)\n}\n\n#[derive(Copy, Clone, Debug)]\npub struct BVH4<'a> {\n    root: Option<&'a BVH4Node<'a>>,\n    depth: usize,\n    node_count: usize,\n    _bounds: Option<&'a [BBox]>,\n}\n\n#[derive(Copy, Clone, Debug)]\npub enum BVH4Node<'a> {\n    Internal {\n        bounds: &'a [BBox4],\n        children: &'a [BVH4Node<'a>],\n        traversal_code: u8,\n    },\n\n    Leaf {\n        object_range: (usize, usize),\n    },\n}\n\nimpl<'a> BVH4<'a> {\n    pub fn from_objects<'b, T, F>(\n        arena: &'a Arena,\n        objects: &mut [T],\n        objects_per_leaf: usize,\n        bounder: F,\n    ) -> BVH4<'a>\n    where\n        F: 'b + Fn(&T) -> &'b [BBox],\n    {\n        if objects.is_empty() {\n            BVH4 {\n                root: None,\n                depth: 0,\n                node_count: 0,\n                _bounds: None,\n            }\n        } else {\n            let base = BVHBase::from_objects(objects, objects_per_leaf, bounder);\n\n            let fill_node = arena.alloc_align_uninit::<BVH4Node>(32);\n            let node_count = BVH4::construct_from_base(\n                arena,\n                &base,\n                &base.nodes[base.root_node_index()],\n                fill_node,\n            );\n\n            BVH4 {\n                root: Some(unsafe { transmute(fill_node) }),\n                depth: (base.depth / 2) + 1,\n                node_count: node_count,\n                _bounds: {\n                    let range = base.nodes[base.root_node_index()].bounds_range();\n                    Some(arena.copy_slice(&base.bounds[range.0..range.1]))\n                },\n            }\n        }\n    }\n\n    pub fn tree_depth(&self) -> usize {\n        self.depth\n    }\n\n    pub fn traverse<F>(&self, rays: &mut RayBatch, ray_stack: &mut RayStack, mut obj_ray_test: F)\n    where\n        F: FnMut(std::ops::Range<usize>, &mut RayBatch, &mut RayStack),\n    {\n        if self.root.is_none() {\n            return;\n        }\n\n        let mut node_tests: u64 = 0;\n\n        let traversal_table =\n            &TRAVERSAL_TABLE[ray_code(rays.dir_inv_local(ray_stack.next_task_ray_idx(0)))];\n\n        // +2 of max depth for root and last child\n        let mut node_stack = [self.root.unwrap(); (BVH_MAX_DEPTH * 3) + 2];\n        let mut stack_ptr = 1;\n\n        while stack_ptr > 0 {\n            match *node_stack[stack_ptr] {\n                BVH4Node::Internal {\n                    bounds,\n                    children,\n                    traversal_code,\n                } => {\n                    node_tests += ray_stack.ray_count_in_next_task() as u64;\n                    let mut all_hits = BVec4A::default();\n\n                    // Ray testing\n                    ray_stack.pop_do_next_task_and_push_rays(children.len(), |ray_idx| {\n                        if rays.is_done(ray_idx) {\n                            BVec4A::default()\n                        } else {\n                            let hits = if bounds.len() == 1 {\n                                bounds[0].intersect_ray(\n                                    rays.orig_local(ray_idx),\n                                    rays.dir_inv_local(ray_idx),\n                                    rays.max_t(ray_idx),\n                                )\n                            } else {\n                                lerp_slice(bounds, rays.time(ray_idx)).intersect_ray(\n                                    rays.orig_local(ray_idx),\n                                    rays.dir_inv_local(ray_idx),\n                                    rays.max_t(ray_idx),\n                                )\n                            };\n                            all_hits |= hits;\n                            hits\n                        }\n                    });\n\n                    // If there were any intersections, create tasks.\n                    if all_hits.any() {\n                        let order_code = traversal_table[traversal_code as usize];\n                        let mut lane_count = 0;\n                        let mut i = children.len() as u8;\n                        while i > 0 {\n                            i -= 1;\n                            let child_i = ((order_code >> (i * 2)) & 3) as usize;\n                            if ray_stack.push_lane_to_task(child_i) {\n                                node_stack[stack_ptr + lane_count] = &children[child_i];\n                                lane_count += 1;\n                            }\n                        }\n\n                        stack_ptr += lane_count - 1;\n                    } else {\n                        stack_ptr -= 1;\n                    }\n                }\n\n                BVH4Node::Leaf { object_range } => {\n                    // Do the ray tests.\n                    obj_ray_test(object_range.0..object_range.1, rays, ray_stack);\n\n                    stack_ptr -= 1;\n                }\n            }\n        }\n\n        ACCEL_NODE_RAY_TESTS.with(|anv| {\n            let v = anv.get();\n            anv.set(v + node_tests);\n        });\n    }\n\n    fn construct_from_base(\n        arena: &'a Arena,\n        base: &BVHBase,\n        node: &BVHBaseNode,\n        fill_node: &mut MaybeUninit<BVH4Node<'a>>,\n    ) -> usize {\n        let mut node_count = 0;\n\n        match *node {\n            // Create internal node\n            BVHBaseNode::Internal {\n                children_indices,\n                split_axis,\n                ..\n            } => {\n                let child_l = &base.nodes[children_indices.0];\n                let child_r = &base.nodes[children_indices.1];\n\n                // Prepare convenient access to the stuff we need.\n                let child_count: usize;\n                let children; // [Optional, Optional, Optional, Optional]\n                let split_info: SplitAxes;\n                match *child_l {\n                    BVHBaseNode::Internal {\n                        children_indices: i_l,\n                        split_axis: s_l,\n                        ..\n                    } => {\n                        match *child_r {\n                            BVHBaseNode::Internal {\n                                children_indices: i_r,\n                                split_axis: s_r,\n                                ..\n                            } => {\n                                // Four nodes\n                                child_count = 4;\n                                children = [\n                                    Some(&base.nodes[i_l.0]),\n                                    Some(&base.nodes[i_l.1]),\n                                    Some(&base.nodes[i_r.0]),\n                                    Some(&base.nodes[i_r.1]),\n                                ];\n                                split_info = SplitAxes::Full((split_axis, s_l, s_r));\n                            }\n                            BVHBaseNode::Leaf { .. } => {\n                                // Three nodes with left split\n                                child_count = 3;\n                                children = [\n                                    Some(&base.nodes[i_l.0]),\n                                    Some(&base.nodes[i_l.1]),\n                                    Some(child_r),\n                                    None,\n                                ];\n                                split_info = SplitAxes::Left((split_axis, s_l));\n                            }\n                        }\n                    }\n                    BVHBaseNode::Leaf { .. } => {\n                        match *child_r {\n                            BVHBaseNode::Internal {\n                                children_indices: i_r,\n                                split_axis: s_r,\n                                ..\n                            } => {\n                                // Three nodes with right split\n                                child_count = 3;\n                                children = [\n                                    Some(child_l),\n                                    Some(&base.nodes[i_r.0]),\n                                    Some(&base.nodes[i_r.1]),\n                                    None,\n                                ];\n                                split_info = SplitAxes::Right((split_axis, s_r));\n                            }\n                            BVHBaseNode::Leaf { .. } => {\n                                // Two nodes\n                                child_count = 2;\n                                children = [Some(child_l), Some(child_r), None, None];\n                                split_info = SplitAxes::TopOnly(split_axis);\n                            }\n                        }\n                    }\n                }\n\n                node_count += child_count;\n\n                // Construct bounds\n                let bounds = {\n                    let bounds_len = children\n                        .iter()\n                        .map(|c| {\n                            if let Some(n) = *c {\n                                let len = n.bounds_range().1 - n.bounds_range().0;\n                                debug_assert!(len >= 1);\n                                len\n                            } else {\n                                0\n                            }\n                        })\n                        .max()\n                        .unwrap();\n                    debug_assert!(bounds_len >= 1);\n                    let bounds = arena.alloc_array_align_uninit(bounds_len, 32);\n                    if bounds_len < 2 {\n                        let b1 =\n                            children[0].map_or(BBox::new(), |c| base.bounds[c.bounds_range().0]);\n                        let b2 =\n                            children[1].map_or(BBox::new(), |c| base.bounds[c.bounds_range().0]);\n                        let b3 =\n                            children[2].map_or(BBox::new(), |c| base.bounds[c.bounds_range().0]);\n                        let b4 =\n                            children[3].map_or(BBox::new(), |c| base.bounds[c.bounds_range().0]);\n                        unsafe {\n                            *bounds[0].as_mut_ptr() = BBox4::from_bboxes(b1, b2, b3, b4);\n                        }\n                    } else {\n                        for (i, b) in bounds.iter_mut().enumerate() {\n                            let time = i as f32 / (bounds_len - 1) as f32;\n\n                            let b1 = children[0].map_or(BBox::new(), |c| {\n                                let (x, y) = c.bounds_range();\n                                lerp_slice(&base.bounds[x..y], time)\n                            });\n                            let b2 = children[1].map_or(BBox::new(), |c| {\n                                let (x, y) = c.bounds_range();\n                                lerp_slice(&base.bounds[x..y], time)\n                            });\n                            let b3 = children[2].map_or(BBox::new(), |c| {\n                                let (x, y) = c.bounds_range();\n                                lerp_slice(&base.bounds[x..y], time)\n                            });\n                            let b4 = children[3].map_or(BBox::new(), |c| {\n                                let (x, y) = c.bounds_range();\n                                lerp_slice(&base.bounds[x..y], time)\n                            });\n                            unsafe {\n                                *b.as_mut_ptr() = BBox4::from_bboxes(b1, b2, b3, b4);\n                            }\n                        }\n                    }\n                    bounds\n                };\n\n                // Construct child nodes\n                let child_nodes = arena.alloc_array_align_uninit::<BVH4Node>(child_count, 32);\n                for (i, c) in children[0..child_count].iter().enumerate() {\n                    node_count +=\n                        BVH4::construct_from_base(arena, base, c.unwrap(), &mut child_nodes[i]);\n                }\n\n                // Build this node\n                unsafe {\n                    *fill_node.as_mut_ptr() = BVH4Node::Internal {\n                        bounds: transmute(bounds),\n                        children: transmute(child_nodes),\n                        traversal_code: calc_traversal_code(split_info),\n                    };\n                }\n            }\n\n            // Create internal node\n            BVHBaseNode::Leaf { object_range, .. } => {\n                unsafe {\n                    *fill_node.as_mut_ptr() = BVH4Node::Leaf {\n                        object_range: object_range,\n                    };\n                }\n                node_count += 1;\n            }\n        }\n\n        return node_count;\n    }\n}\n\nimpl<'a> Boundable for BVH4<'a> {\n    fn bounds<'b>(&'b self) -> &'b [BBox] {\n        self._bounds.unwrap_or(&[])\n    }\n}\n"
  },
  {
    "path": "src/accel/bvh_base.rs",
    "content": "#![allow(dead_code)]\n\nuse crate::{algorithm::merge_slices_append, bbox::BBox, lerp::lerp_slice, math::log2_64};\n\nuse super::objects_split::{median_split, sah_split};\n\npub const BVH_MAX_DEPTH: usize = 42;\n\n// Amount bigger the union of all time samples can be\n// and still use the union rather than preserve the\n// individual time samples.\nconst USE_UNION_FACTOR: f32 = 1.4;\n\n/// An intermediary structure for creating a BVH.\n#[derive(Debug)]\npub struct BVHBase {\n    pub nodes: Vec<BVHBaseNode>,\n    pub bounds: Vec<BBox>,\n    pub depth: usize,\n    bounds_cache: Vec<BBox>,\n}\n\n#[derive(Copy, Clone, Debug)]\npub enum BVHBaseNode {\n    Internal {\n        bounds_range: (usize, usize),\n        children_indices: (usize, usize),\n        split_axis: u8,\n    },\n\n    Leaf {\n        bounds_range: (usize, usize),\n        object_range: (usize, usize),\n    },\n}\n\nimpl BVHBaseNode {\n    pub fn bounds_range(&self) -> (usize, usize) {\n        match *self {\n            BVHBaseNode::Internal { bounds_range, .. } | BVHBaseNode::Leaf { bounds_range, .. } => {\n                bounds_range\n            }\n        }\n    }\n}\n\nimpl BVHBase {\n    fn new() -> BVHBase {\n        BVHBase {\n            nodes: Vec::new(),\n            bounds: Vec::new(),\n            depth: 0,\n            bounds_cache: Vec::new(),\n        }\n    }\n\n    pub fn from_objects<'b, T, F>(objects: &mut [T], objects_per_leaf: usize, bounder: F) -> BVHBase\n    where\n        F: 'b + Fn(&T) -> &'b [BBox],\n    {\n        let mut bvh = BVHBase::new();\n        bvh.recursive_build(0, 0, objects_per_leaf, objects, &bounder);\n        bvh\n    }\n\n    pub fn root_node_index(&self) -> usize {\n        0\n    }\n\n    fn acc_bounds<'a, T, F>(&mut self, objects: &mut [T], bounder: &F)\n    where\n        F: 'a + Fn(&T) -> &'a [BBox],\n    {\n        // TODO: do all of this without the temporary cache\n        let max_len = objects.iter().map(|obj| bounder(obj).len()).max().unwrap();\n\n        self.bounds_cache.clear();\n        self.bounds_cache.resize(max_len, BBox::new());\n\n        for obj in objects.iter() {\n            let bounds = bounder(obj);\n            debug_assert!(!bounds.is_empty());\n            if bounds.len() == max_len {\n                for i in 0..bounds.len() {\n                    self.bounds_cache[i] |= bounds[i];\n                }\n            } else {\n                let s = (max_len - 1) as f32;\n                for (i, bbc) in self.bounds_cache.iter_mut().enumerate() {\n                    *bbc |= lerp_slice(bounds, i as f32 / s);\n                }\n            }\n        }\n    }\n\n    fn recursive_build<'a, T, F>(\n        &mut self,\n        offset: usize,\n        depth: usize,\n        objects_per_leaf: usize,\n        objects: &mut [T],\n        bounder: &F,\n    ) -> (usize, (usize, usize))\n    where\n        F: 'a + Fn(&T) -> &'a [BBox],\n    {\n        let me = self.nodes.len();\n\n        if objects.is_empty() {\n            return (0, (0, 0));\n        } else if objects.len() <= objects_per_leaf {\n            // Leaf node\n            let bi = self.bounds.len();\n            // Get bounds\n            {\n                // We make sure that it's worth having multiple time samples, and if not\n                // we reduce to the union of the time samples.\n                self.acc_bounds(objects, bounder);\n                let union_bounds = self\n                    .bounds_cache\n                    .iter()\n                    .fold(BBox::new(), |b1, b2| (b1 | *b2));\n                let average_area = self\n                    .bounds_cache\n                    .iter()\n                    .fold(0.0, |area, bb| area + bb.surface_area())\n                    / self.bounds_cache.len() as f32;\n                if union_bounds.surface_area() <= (average_area * USE_UNION_FACTOR) {\n                    self.bounds.push(union_bounds);\n                } else {\n                    self.bounds.extend(&self.bounds_cache);\n                }\n            }\n\n            // Create node\n            self.nodes.push(BVHBaseNode::Leaf {\n                bounds_range: (bi, self.bounds.len()),\n                object_range: (offset, offset + objects.len()),\n            });\n\n            if self.depth < depth {\n                self.depth = depth;\n            }\n\n            return (me, (bi, self.bounds.len()));\n        } else {\n            // Not a leaf node\n            self.nodes.push(BVHBaseNode::Internal {\n                bounds_range: (0, 0),\n                children_indices: (0, 0),\n                split_axis: 0,\n            });\n\n            // Partition objects.\n            // If we're too near the max depth, we do balanced building to\n            // avoid exceeding max depth.\n            // Otherwise we do SAH splitting to build better trees.\n            let (split_index, split_axis) =\n                if (log2_64(objects.len() as u64) as usize) < (BVH_MAX_DEPTH - depth) {\n                    // SAH splitting, when we have room to play\n                    sah_split(objects, &bounder)\n                } else {\n                    // Balanced splitting, when we don't have room to play\n                    median_split(objects, &bounder)\n                };\n\n            // Create child nodes\n            let (c1_index, c1_bounds) = self.recursive_build(\n                offset,\n                depth + 1,\n                objects_per_leaf,\n                &mut objects[..split_index],\n                bounder,\n            );\n            let (c2_index, c2_bounds) = self.recursive_build(\n                offset + split_index,\n                depth + 1,\n                objects_per_leaf,\n                &mut objects[split_index..],\n                bounder,\n            );\n\n            // Determine bounds\n            // TODO: do merging without the temporary vec.\n            let bi = self.bounds.len();\n            {\n                let mut merged = Vec::new();\n                merge_slices_append(\n                    &self.bounds[c1_bounds.0..c1_bounds.1],\n                    &self.bounds[c2_bounds.0..c2_bounds.1],\n                    &mut merged,\n                    |b1, b2| *b1 | *b2,\n                );\n                // We make sure that it's worth having multiple time samples, and if not\n                // we reduce to the union of the time samples.\n                let union_bounds = merged.iter().fold(BBox::new(), |b1, b2| (b1 | *b2));\n                let average_area = merged.iter().fold(0.0, |area, bb| area + bb.surface_area())\n                    / merged.len() as f32;\n                if union_bounds.surface_area() <= (average_area * USE_UNION_FACTOR) {\n                    self.bounds.push(union_bounds);\n                } else {\n                    self.bounds.extend(merged.drain(0..));\n                }\n            }\n\n            // Set node\n            self.nodes[me] = BVHBaseNode::Internal {\n                bounds_range: (bi, self.bounds.len()),\n                children_indices: (c1_index, c2_index),\n                split_axis: split_axis as u8,\n            };\n\n            return (me, (bi, self.bounds.len()));\n        }\n    }\n}\n"
  },
  {
    "path": "src/accel/light_array.rs",
    "content": "use kioku::Arena;\n\nuse crate::{\n    bbox::BBox,\n    math::{Normal, Point, Vector},\n    shading::surface_closure::SurfaceClosure,\n};\n\nuse super::LightAccel;\n\n#[derive(Debug, Copy, Clone)]\npub struct LightArray<'a> {\n    indices: &'a [usize],\n    aprx_energy: f32,\n}\n\nimpl<'a> LightArray<'a> {\n    #[allow(dead_code)]\n    pub fn from_objects<'b, T, F>(\n        arena: &'a Arena,\n        objects: &mut [T],\n        info_getter: F,\n    ) -> LightArray<'a>\n    where\n        F: 'b + Fn(&T) -> (&'b [BBox], f32),\n    {\n        let mut indices = Vec::new();\n        let mut aprx_energy = 0.0;\n        for (i, thing) in objects.iter().enumerate() {\n            let (_, power) = info_getter(thing);\n            if power > 0.0 {\n                indices.push(i);\n                aprx_energy += power;\n            }\n        }\n\n        LightArray {\n            indices: arena.copy_slice(&indices),\n            aprx_energy: aprx_energy,\n        }\n    }\n}\n\nimpl<'a> LightAccel for LightArray<'a> {\n    fn select(\n        &self,\n        inc: Vector,\n        pos: Point,\n        nor: Normal,\n        nor_g: Normal,\n        sc: &SurfaceClosure,\n        time: f32,\n        n: f32,\n    ) -> Option<(usize, f32, f32)> {\n        let _ = (inc, pos, nor, nor_g, sc, time); // Not using these, silence warnings\n\n        assert!(n >= 0.0 && n <= 1.0);\n\n        if self.indices.is_empty() {\n            return None;\n        }\n\n        let n2 = n * self.indices.len() as f32;\n        let i = if n == 1.0 {\n            *self.indices.last().unwrap()\n        } else {\n            self.indices[n2 as usize]\n        };\n\n        let whittled_n = n2 - i as f32;\n        let pdf = 1.0 / self.indices.len() as f32;\n\n        Some((i, pdf, whittled_n))\n    }\n\n    fn approximate_energy(&self) -> f32 {\n        self.aprx_energy\n    }\n}\n"
  },
  {
    "path": "src/accel/light_tree.rs",
    "content": "use std::mem::{transmute, MaybeUninit};\n\nuse kioku::Arena;\n\nuse crate::{\n    algorithm::merge_slices_append,\n    bbox::BBox,\n    lerp::lerp_slice,\n    math::{Normal, Point, Vector},\n    shading::surface_closure::SurfaceClosure,\n};\n\nuse super::{objects_split::sah_split, LightAccel};\n\nconst ARITY_LOG2: usize = 3; // Determines how much to collapse the binary tree,\n                             // implicitly defining the light tree's arity.  1 = no collapsing, leave as binary\n                             // tree.\nconst ARITY: usize = 1 << ARITY_LOG2; // Arity of the final tree\n\n#[derive(Copy, Clone, Debug)]\npub struct LightTree<'a> {\n    root: Option<&'a Node<'a>>,\n    depth: usize,\n}\n\n#[derive(Copy, Clone, Debug)]\nenum Node<'a> {\n    Inner {\n        children: &'a [Node<'a>],\n        bounds: &'a [BBox],\n        energy: f32,\n    },\n    Leaf {\n        light_index: usize,\n        bounds: &'a [BBox],\n        energy: f32,\n    },\n}\n\nimpl<'a> Node<'a> {\n    fn bounds(&self) -> &'a [BBox] {\n        match *self {\n            Node::Inner { bounds, .. } | Node::Leaf { bounds, .. } => bounds,\n        }\n    }\n\n    fn energy(&self) -> f32 {\n        match *self {\n            Node::Inner { energy, .. } | Node::Leaf { energy, .. } => energy,\n        }\n    }\n\n    fn light_index(&self) -> usize {\n        match *self {\n            Node::Inner { .. } => panic!(),\n            Node::Leaf { light_index, .. } => light_index,\n        }\n    }\n}\n\nimpl<'a> LightTree<'a> {\n    pub fn from_objects<'b, T, F>(\n        arena: &'a Arena,\n        objects: &mut [T],\n        info_getter: F,\n    ) -> LightTree<'a>\n    where\n        F: 'b + Fn(&T) -> (&'b [BBox], f32),\n    {\n        if objects.is_empty() {\n            LightTree {\n                root: None,\n                depth: 0,\n            }\n        } else {\n            let mut builder = LightTreeBuilder::new();\n            builder.recursive_build(0, 0, objects, &info_getter);\n\n            let root = arena.alloc_uninit::<Node>();\n            LightTree::construct_from_builder(arena, &builder, builder.root_node_index(), root);\n\n            LightTree {\n                root: Some(unsafe { transmute(root) }),\n                depth: builder.depth,\n            }\n        }\n    }\n\n    fn construct_from_builder(\n        arena: &'a Arena,\n        base: &LightTreeBuilder,\n        node_index: usize,\n        node_mem: &mut MaybeUninit<Node<'a>>,\n    ) {\n        if base.nodes[node_index].is_leaf {\n            // Leaf\n            let bounds_range = base.nodes[node_index].bounds_range;\n            let bounds = arena.copy_slice(&base.bounds[bounds_range.0..bounds_range.1]);\n\n            unsafe {\n                *node_mem.as_mut_ptr() = Node::Leaf {\n                    light_index: base.nodes[node_index].child_index,\n                    bounds: bounds,\n                    energy: base.nodes[node_index].energy,\n                };\n            }\n        } else {\n            // Inner\n            let bounds_range = base.nodes[node_index].bounds_range;\n            let bounds = arena.copy_slice(&base.bounds[bounds_range.0..bounds_range.1]);\n\n            let child_count = base.node_child_count(node_index);\n            let children = arena.alloc_array_uninit::<Node>(child_count);\n            for i in 0..child_count {\n                LightTree::construct_from_builder(\n                    arena,\n                    base,\n                    base.node_nth_child_index(node_index, i),\n                    &mut children[i],\n                );\n            }\n\n            unsafe {\n                *node_mem.as_mut_ptr() = Node::Inner {\n                    children: transmute(children),\n                    bounds: bounds,\n                    energy: base.nodes[node_index].energy,\n                };\n            }\n        }\n    }\n}\n\nimpl<'a> LightAccel for LightTree<'a> {\n    fn select(\n        &self,\n        inc: Vector,\n        pos: Point,\n        nor: Normal,\n        nor_g: Normal,\n        sc: &SurfaceClosure,\n        time: f32,\n        n: f32,\n    ) -> Option<(usize, f32, f32)> {\n        // Calculates the selection probability for a node\n        let node_prob = |node_ref: &Node| {\n            let bbox = lerp_slice(node_ref.bounds(), time);\n            let d = bbox.center() - pos;\n            let r2 = bbox.diagonal2() * 0.25;\n            let inv_surface_area = 1.0 / r2;\n\n            // Get the approximate amount of light contribution from the\n            // composite light source.\n            let approx_contrib = sc.estimate_eval_over_sphere_light(inc, d, r2, nor, nor_g);\n            node_ref.energy() * inv_surface_area * approx_contrib\n        };\n\n        // Traverse down the tree, keeping track of the relative probabilities\n        let mut node = self.root?;\n        let mut tot_prob = 1.0;\n        let mut n = n;\n        while let Node::Inner { children, .. } = *node {\n            // Calculate the relative probabilities of the children\n            let ps = {\n                let mut ps = [0.0; ARITY];\n                let mut total = 0.0;\n                for (i, child) in children.iter().enumerate() {\n                    let p = node_prob(child);\n                    ps[i] = p;\n                    total += p;\n                }\n                if total <= 0.0 {\n                    let p = 1.0 / children.len() as f32;\n                    for prob in &mut ps[..] {\n                        *prob = p;\n                    }\n                } else {\n                    for prob in &mut ps[..] {\n                        *prob /= total;\n                    }\n                }\n                ps\n            };\n\n            // Pick child and update probabilities\n            let mut base = 0.0;\n            for (i, &p) in ps.iter().enumerate() {\n                if (n <= base + p) || (i == children.len() - 1) {\n                    tot_prob *= p;\n                    node = &children[i];\n                    n = (n - base) / p;\n                    break;\n                } else {\n                    base += p;\n                }\n            }\n        }\n\n        // Found our light!\n        Some((node.light_index(), tot_prob, n))\n    }\n\n    fn approximate_energy(&self) -> f32 {\n        if let Some(node) = self.root {\n            node.energy()\n        } else {\n            0.0\n        }\n    }\n}\n\nstruct LightTreeBuilder {\n    nodes: Vec<BuilderNode>,\n    bounds: Vec<BBox>,\n    depth: usize,\n}\n\n#[derive(Copy, Clone, Debug)]\nstruct BuilderNode {\n    is_leaf: bool,\n    bounds_range: (usize, usize),\n    energy: f32,\n    child_index: usize,\n}\n\nimpl LightTreeBuilder {\n    fn new() -> LightTreeBuilder {\n        LightTreeBuilder {\n            nodes: Vec::new(),\n            bounds: Vec::new(),\n            depth: 0,\n        }\n    }\n\n    pub fn root_node_index(&self) -> usize {\n        0\n    }\n\n    // Returns the number of children of this node, assuming a collapse\n    // number of `ARITY_LOG2`.\n    pub fn node_child_count(&self, node_index: usize) -> usize {\n        self.node_child_count_recurse(ARITY_LOG2, node_index)\n    }\n\n    // Returns the index of the nth child, assuming a collapse\n    // number of `ARITY_LOG2`.\n    pub fn node_nth_child_index(&self, node_index: usize, child_n: usize) -> usize {\n        self.node_nth_child_index_recurse(ARITY_LOG2, node_index, child_n)\n            .0\n    }\n\n    // Returns the number of children of this node, assuming a collapse\n    // number of `level_collapse`.\n    pub fn node_child_count_recurse(&self, level_collapse: usize, node_index: usize) -> usize {\n        if level_collapse > 0 {\n            if self.nodes[node_index].is_leaf {\n                1\n            } else {\n                let a = self.node_child_count_recurse(level_collapse - 1, node_index + 1);\n                let b = self.node_child_count_recurse(\n                    level_collapse - 1,\n                    self.nodes[node_index].child_index,\n                );\n\n                a + b\n            }\n        } else {\n            1\n        }\n    }\n\n    // Returns the index of the nth child, assuming a collapse\n    // number of `level_collapse`.\n    pub fn node_nth_child_index_recurse(\n        &self,\n        level_collapse: usize,\n        node_index: usize,\n        child_n: usize,\n    ) -> (usize, usize) {\n        if level_collapse > 0 && !self.nodes[node_index].is_leaf {\n            let (index, rem) =\n                self.node_nth_child_index_recurse(level_collapse - 1, node_index + 1, child_n);\n            if rem == 0 {\n                return (index, 0);\n            }\n            return self.node_nth_child_index_recurse(\n                level_collapse - 1,\n                self.nodes[node_index].child_index,\n                rem - 1,\n            );\n        } else {\n            return (node_index, child_n);\n        }\n    }\n\n    fn recursive_build<'a, T, F>(\n        &mut self,\n        offset: usize,\n        depth: usize,\n        objects: &mut [T],\n        info_getter: &F,\n    ) -> (usize, (usize, usize))\n    where\n        F: 'a + Fn(&T) -> (&'a [BBox], f32),\n    {\n        let me_index = self.nodes.len();\n\n        if objects.is_empty() {\n            return (0, (0, 0));\n        } else if objects.len() == 1 {\n            // Leaf node\n            let bi = self.bounds.len();\n            let (obj_bounds, energy) = info_getter(&objects[0]);\n            self.bounds.extend(obj_bounds);\n            self.nodes.push(BuilderNode {\n                is_leaf: true,\n                bounds_range: (bi, self.bounds.len()),\n                energy: energy,\n                child_index: offset,\n            });\n\n            if self.depth < depth {\n                self.depth = depth;\n            }\n\n            return (me_index, (bi, self.bounds.len()));\n        } else {\n            // Not a leaf node\n            self.nodes.push(BuilderNode {\n                is_leaf: false,\n                bounds_range: (0, 0),\n                energy: 0.0,\n                child_index: 0,\n            });\n\n            // Partition objects.\n            let (split_index, _) = sah_split(objects, &|obj_ref| info_getter(obj_ref).0);\n\n            // Create child nodes\n            let (_, c1_bounds) =\n                self.recursive_build(offset, depth + 1, &mut objects[..split_index], info_getter);\n            let (c2_index, c2_bounds) = self.recursive_build(\n                offset + split_index,\n                depth + 1,\n                &mut objects[split_index..],\n                info_getter,\n            );\n\n            // Determine bounds\n            // TODO: do merging without the temporary vec.\n            let bi = self.bounds.len();\n            let mut merged = Vec::new();\n            merge_slices_append(\n                &self.bounds[c1_bounds.0..c1_bounds.1],\n                &self.bounds[c2_bounds.0..c2_bounds.1],\n                &mut merged,\n                |b1, b2| *b1 | *b2,\n            );\n            self.bounds.extend(merged.drain(0..));\n\n            // Set node\n            let energy = self.nodes[me_index + 1].energy + self.nodes[c2_index].energy;\n            self.nodes[me_index] = BuilderNode {\n                is_leaf: false,\n                bounds_range: (bi, self.bounds.len()),\n                energy: energy,\n                child_index: c2_index,\n            };\n\n            return (me_index, (bi, self.bounds.len()));\n        }\n    }\n}\n"
  },
  {
    "path": "src/accel/mod.rs",
    "content": "// mod bvh;\nmod bvh4;\nmod bvh_base;\nmod light_array;\nmod light_tree;\nmod objects_split;\n\nuse std::cell::Cell;\n\nuse crate::{\n    math::{Normal, Point, Vector},\n    shading::surface_closure::SurfaceClosure,\n};\n\npub use self::{\n    // bvh::{BVHNode, BVH},\n    bvh4::{ray_code, BVH4Node, BVH4},\n    light_array::LightArray,\n    light_tree::LightTree,\n};\n\n// Track BVH traversal time\nthread_local! {\n    pub static ACCEL_NODE_RAY_TESTS: Cell<u64> = Cell::new(0);\n}\n\npub trait LightAccel {\n    /// Returns (index_of_light, selection_pdf, whittled_n)\n    fn select(\n        &self,\n        inc: Vector,\n        pos: Point,\n        nor: Normal,\n        nor_g: Normal,\n        sc: &SurfaceClosure,\n        time: f32,\n        n: f32,\n    ) -> Option<(usize, f32, f32)>;\n\n    fn approximate_energy(&self) -> f32;\n}\n"
  },
  {
    "path": "src/accel/objects_split.rs",
    "content": "#![allow(dead_code)]\n\nuse std::cmp::Ordering;\n\nuse crate::{\n    algorithm::{partition, quick_select},\n    bbox::BBox,\n    lerp::lerp_slice,\n    math::{dot, Vector},\n    sampling::uniform_sample_hemisphere,\n};\n\nconst SAH_BIN_COUNT: usize = 13; // Prime numbers work best, for some reason\nconst SPLIT_PLANE_COUNT: usize = 5;\n\n/// Takes a slice of boundable objects and partitions them based on the Surface\n/// Area Heuristic, but using arbitrarily oriented planes.\n///\n/// Returns the index of the partition boundary and the axis that it split on\n/// (0 = x, 1 = y, 2 = z).\npub fn free_sah_split<'a, T, F>(seed: u32, objects: &mut [T], bounder: &F) -> (usize, usize)\nwhere\n    F: Fn(&T) -> &'a [BBox],\n{\n    // Generate the planes for splitting\n    let planes = {\n        let mut planes = [Vector::new(0.0, 0.0, 0.0); SPLIT_PLANE_COUNT];\n        let offset = seed * SPLIT_PLANE_COUNT as u32;\n        for i in 0..SPLIT_PLANE_COUNT {\n            let u = halton::sample(0, offset + i as u32);\n            let v = halton::sample(1, offset + i as u32);\n            planes[i] = uniform_sample_hemisphere(u, v).normalized();\n        }\n        planes\n    };\n\n    // Get the extents of the objects with respect to the split planes\n    let extents = {\n        let mut extents = [(std::f32::INFINITY, std::f32::NEG_INFINITY); SPLIT_PLANE_COUNT];\n        for obj in &objects[..] {\n            let centroid = lerp_slice(bounder(obj), 0.5).center().into_vector();\n            for i in 0..SPLIT_PLANE_COUNT {\n                let dist = dot(centroid, planes[i]);\n                extents[i].0 = extents[i].0.min(dist);\n                extents[i].1 = extents[i].1.max(dist);\n            }\n        }\n        extents\n    };\n\n    // Pre-calc SAH div distances\n    let sah_divs = {\n        let mut sah_divs = [[0.0f32; SAH_BIN_COUNT - 1]; SPLIT_PLANE_COUNT];\n        for pi in 0..SPLIT_PLANE_COUNT {\n            let extent = extents[pi].1 - extents[pi].0;\n            for div in 0..(SAH_BIN_COUNT - 1) {\n                let part = extent * ((div + 1) as f32 / SAH_BIN_COUNT as f32);\n                sah_divs[pi][div] = extents[pi].0 + part;\n            }\n        }\n        sah_divs\n    };\n\n    // Build SAH bins\n    let sah_bins = {\n        let mut sah_bins =\n            [[(BBox::new(), BBox::new(), 0, 0); SAH_BIN_COUNT - 1]; SPLIT_PLANE_COUNT];\n        for obj in objects.iter() {\n            let tb = lerp_slice(bounder(obj), 0.5);\n            let centroid = tb.center().into_vector();\n\n            for pi in 0..SPLIT_PLANE_COUNT {\n                for div in 0..(SAH_BIN_COUNT - 1) {\n                    let dist = dot(centroid, planes[pi]);\n                    if dist <= sah_divs[pi][div] {\n                        sah_bins[pi][div].0 |= tb;\n                        sah_bins[pi][div].2 += 1;\n                    } else {\n                        sah_bins[pi][div].1 |= tb;\n                        sah_bins[pi][div].3 += 1;\n                    }\n                }\n            }\n        }\n        sah_bins\n    };\n\n    // Find best split axis and div point\n    let (split_plane_i, div_n) = {\n        let mut split_plane_i = 0;\n        let mut div_n = 0.0;\n        let mut smallest_cost = std::f32::INFINITY;\n\n        for pi in 0..SPLIT_PLANE_COUNT {\n            for div in 0..(SAH_BIN_COUNT - 1) {\n                let left_cost = sah_bins[pi][div].0.surface_area() * sah_bins[pi][div].2 as f32;\n                let right_cost = sah_bins[pi][div].1.surface_area() * sah_bins[pi][div].3 as f32;\n                let tot_cost = left_cost + right_cost;\n                if tot_cost < smallest_cost {\n                    split_plane_i = pi;\n                    div_n = sah_divs[pi][div];\n                    smallest_cost = tot_cost;\n                }\n            }\n        }\n\n        (split_plane_i, div_n)\n    };\n\n    // Calculate the approximate axis-aligned split, along with flipping the split plane as\n    // appropriate.\n    let (plane, approx_axis, div) = {\n        // Find axis with largest value\n        let mut largest_axis = 0;\n        let mut n = 0.0;\n        for d in 0..3 {\n            let m = planes[split_plane_i].get_n(d).abs();\n            if n < m {\n                largest_axis = d;\n                n = m;\n            }\n        }\n\n        // If it's negative, flip\n        if planes[split_plane_i].get_n(largest_axis).is_sign_positive() {\n            (planes[split_plane_i], largest_axis, div_n)\n        } else {\n            (planes[split_plane_i] * -1.0, largest_axis, div_n * -1.0)\n        }\n    };\n\n    // Partition\n    let mut split_i = partition(&mut objects[..], |obj| {\n        let centroid = lerp_slice(bounder(obj), 0.5).center().into_vector();\n        let dist = dot(centroid, plane);\n        dist < div\n    });\n\n    if split_i < 1 {\n        split_i = 1;\n    } else if split_i >= objects.len() {\n        split_i = objects.len() - 1;\n    }\n\n    (split_i, approx_axis)\n}\n\n/// Takes a slice of boundable objects and partitions them based on the Surface\n/// Area Heuristic.\n///\n/// Returns the index of the partition boundary and the axis that it split on\n/// (0 = x, 1 = y, 2 = z).\npub fn sah_split<'a, T, F>(objects: &mut [T], bounder: &F) -> (usize, usize)\nwhere\n    F: Fn(&T) -> &'a [BBox],\n{\n    // Get combined object centroid extents\n    let bounds = {\n        let mut bb = BBox::new();\n        for obj in &objects[..] {\n            bb |= lerp_slice(bounder(obj), 0.5).center();\n        }\n        bb\n    };\n\n    // Pre-calc SAH div points\n    let sah_divs = {\n        let mut sah_divs = [[0.0f32; SAH_BIN_COUNT - 1]; 3];\n        for d in 0..sah_divs.len() {\n            let extent = bounds.max.get_n(d) - bounds.min.get_n(d);\n            for div in 0..(SAH_BIN_COUNT - 1) {\n                let part = extent * ((div + 1) as f32 / SAH_BIN_COUNT as f32);\n                sah_divs[d][div] = bounds.min.get_n(d) + part;\n            }\n        }\n        sah_divs\n    };\n\n    // Build SAH bins\n    let sah_bins = {\n        let mut sah_bins = [[(BBox::new(), BBox::new(), 0, 0); SAH_BIN_COUNT - 1]; 3];\n        for obj in objects.iter() {\n            let tb = lerp_slice(bounder(obj), 0.5);\n            let centroid = (tb.min.into_vector() + tb.max.into_vector()) * 0.5;\n\n            for d in 0..3 {\n                for div in 0..(SAH_BIN_COUNT - 1) {\n                    if centroid.get_n(d) <= sah_divs[d][div] {\n                        sah_bins[d][div].0 |= tb;\n                        sah_bins[d][div].2 += 1;\n                    } else {\n                        sah_bins[d][div].1 |= tb;\n                        sah_bins[d][div].3 += 1;\n                    }\n                }\n            }\n        }\n        sah_bins\n    };\n\n    // Find best split axis and div point\n    let (split_axis, div) = {\n        let mut dim = 0;\n        let mut div_n = 0.0;\n        let mut smallest_cost = std::f32::INFINITY;\n\n        for d in 0..3 {\n            for div in 0..(SAH_BIN_COUNT - 1) {\n                let left_cost = sah_bins[d][div].0.surface_area() * sah_bins[d][div].2 as f32;\n                let right_cost = sah_bins[d][div].1.surface_area() * sah_bins[d][div].3 as f32;\n                let left_diag = sah_bins[d][div].0.diagonal();\n                let right_diag = sah_bins[d][div].1.diagonal();\n                let tot_cost = (left_cost * left_diag) + (right_cost * right_diag);\n                if tot_cost < smallest_cost {\n                    dim = d;\n                    div_n = sah_divs[d][div];\n                    smallest_cost = tot_cost;\n                }\n            }\n        }\n\n        (dim, div_n)\n    };\n\n    // Partition\n    let mut split_i = partition(&mut objects[..], |obj| {\n        let tb = lerp_slice(bounder(obj), 0.5);\n        let centroid = (tb.min.get_n(split_axis) + tb.max.get_n(split_axis)) * 0.5;\n        centroid < div\n    });\n    if split_i < 1 {\n        split_i = 1;\n    } else if split_i >= objects.len() {\n        split_i = objects.len() - 1;\n    }\n\n    (split_i, split_axis)\n}\n\n/// Takes a slice of boundable objects and partitions them based on the bounds mean heuristic.\n///\n/// Returns the index of the partition boundary and the axis that it split on\n/// (0 = x, 1 = y, 2 = z).\npub fn bounds_mean_split<'a, T, F>(objects: &mut [T], bounder: &F) -> (usize, usize)\nwhere\n    F: Fn(&T) -> &'a [BBox],\n{\n    // Get combined object bounds\n    let bounds = {\n        let mut bb = BBox::new();\n        for obj in &objects[..] {\n            bb |= lerp_slice(bounder(obj), 0.5);\n        }\n        bb\n    };\n\n    let split_axis = {\n        let mut axis = 0;\n        let mut largest = std::f32::NEG_INFINITY;\n        for i in 0..3 {\n            let extent = bounds.max.get_n(i) - bounds.min.get_n(i);\n            if extent > largest {\n                largest = extent;\n                axis = i;\n            }\n        }\n        axis\n    };\n\n    let div = (bounds.min.get_n(split_axis) + bounds.max.get_n(split_axis)) * 0.5;\n\n    // Partition\n    let mut split_i = partition(&mut objects[..], |obj| {\n        let tb = lerp_slice(bounder(obj), 0.5);\n        let centroid = (tb.min.get_n(split_axis) + tb.max.get_n(split_axis)) * 0.5;\n        centroid < div\n    });\n    if split_i < 1 {\n        split_i = 1;\n    } else if split_i >= objects.len() {\n        split_i = objects.len() - 1;\n    }\n\n    (split_i, split_axis)\n}\n\n/// Takes a slice of boundable objects and partitions them based on the median heuristic.\n///\n/// Returns the index of the partition boundary and the axis that it split on\n/// (0 = x, 1 = y, 2 = z).\npub fn median_split<'a, T, F>(objects: &mut [T], bounder: &F) -> (usize, usize)\nwhere\n    F: Fn(&T) -> &'a [BBox],\n{\n    // Get combined object bounds\n    let bounds = {\n        let mut bb = BBox::new();\n        for obj in &objects[..] {\n            bb |= lerp_slice(bounder(obj), 0.5);\n        }\n        bb\n    };\n\n    let split_axis = {\n        let mut axis = 0;\n        let mut largest = std::f32::NEG_INFINITY;\n        for i in 0..3 {\n            let extent = bounds.max.get_n(i) - bounds.min.get_n(i);\n            if extent > largest {\n                largest = extent;\n                axis = i;\n            }\n        }\n        axis\n    };\n\n    let place = {\n        let place = objects.len() / 2;\n        if place > 0 {\n            place\n        } else {\n            1\n        }\n    };\n    quick_select(objects, place, |a, b| {\n        let tb_a = lerp_slice(bounder(a), 0.5);\n        let tb_b = lerp_slice(bounder(b), 0.5);\n        let centroid_a = (tb_a.min.get_n(split_axis) + tb_a.max.get_n(split_axis)) * 0.5;\n        let centroid_b = (tb_b.min.get_n(split_axis) + tb_b.max.get_n(split_axis)) * 0.5;\n\n        if centroid_a < centroid_b {\n            Ordering::Less\n        } else if centroid_a == centroid_b {\n            Ordering::Equal\n        } else {\n            Ordering::Greater\n        }\n    });\n\n    (place, split_axis)\n}\n"
  },
  {
    "path": "src/algorithm.rs",
    "content": "#![allow(dead_code)]\n\nuse std::{\n    cmp::{self, Ordering},\n    mem::MaybeUninit,\n};\n\nuse crate::{\n    hash::hash_u64,\n    lerp::{lerp_slice, Lerp},\n};\n\n/// Selects an item from a slice based on a weighting function and a\n/// number (n) between 0.0 and 1.0.  Returns the index of the selected\n/// item and the probability that it would have been selected with a\n/// random n.\npub fn weighted_choice<T, F>(slc: &[T], n: f32, weight: F) -> (usize, f32)\nwhere\n    F: Fn(&T) -> f32,\n{\n    assert!(!slc.is_empty());\n\n    let total_weight = slc.iter().fold(0.0, |sum, v| sum + weight(v));\n    let n = n * total_weight;\n\n    let mut x = 0.0;\n    for (i, v) in slc.iter().enumerate() {\n        let w = weight(v);\n        x += w;\n        if x > n || i == slc.len() {\n            return (i, w / total_weight);\n        }\n    }\n\n    unreachable!()\n}\n\n/// Partitions a slice in-place with the given unary predicate, returning\n/// the index of the first element for which the predicate evaluates\n/// false.\n///\n/// The predicate is executed precisely once on every element in\n/// the slice, and is allowed to modify the elements.\npub fn partition<T, F>(slc: &mut [T], mut pred: F) -> usize\nwhere\n    F: FnMut(&mut T) -> bool,\n{\n    // This version uses raw pointers and pointer arithmetic to squeeze more\n    // performance out of the code.\n    unsafe {\n        let mut a = slc.as_mut_ptr();\n        let mut b = a.add(slc.len());\n        let start = a as usize;\n\n        loop {\n            loop {\n                if a == b {\n                    return ((a as usize) - start) / std::mem::size_of::<T>();\n                }\n                if !pred(&mut *a) {\n                    break;\n                }\n                a = a.offset(1);\n            }\n\n            loop {\n                b = b.offset(-1);\n                if a == b {\n                    return ((a as usize) - start) / std::mem::size_of::<T>();\n                }\n                if pred(&mut *b) {\n                    break;\n                }\n            }\n\n            std::ptr::swap(a, b);\n\n            a = a.offset(1);\n        }\n    }\n}\n\n/// Partitions a slice in-place with the given unary predicate, returning\n/// the index of the first element for which the predicate evaluates\n/// false.\n///\n/// The predicate is executed precisely once on every element in\n/// the slice, and is allowed to modify the elements.\n///\n/// The only difference between this and plain partition above, is that\n/// the predicate function is passed a bool representing which side\n/// of the array we're currently on: left or right.  False means left,\n/// True means right.\npub fn partition_with_side<T, F>(slc: &mut [T], mut pred: F) -> usize\nwhere\n    F: FnMut(&mut T, bool) -> bool,\n{\n    // This version uses raw pointers and pointer arithmetic to squeeze more\n    // performance out of the code.\n    unsafe {\n        let mut a = slc.as_mut_ptr();\n        let mut b = a.add(slc.len());\n        let start = a as usize;\n\n        loop {\n            loop {\n                if a == b {\n                    return ((a as usize) - start) / std::mem::size_of::<T>();\n                }\n                if !pred(&mut *a, false) {\n                    break;\n                }\n                a = a.offset(1);\n            }\n\n            loop {\n                b = b.offset(-1);\n                if a == b {\n                    return ((a as usize) - start) / std::mem::size_of::<T>();\n                }\n                if pred(&mut *b, true) {\n                    break;\n                }\n            }\n\n            std::ptr::swap(a, b);\n\n            a = a.offset(1);\n        }\n    }\n}\n\n/// Partitions two slices in-place in concert based on the given unary\n/// predicate, returning the index of the first element for which the\n/// predicate evaluates false.\n///\n/// Because this runs on two slices at once, they must both be the same\n/// length.\n///\n/// The predicate takes a usize (which will receive the index of the elments\n/// being tested), a mutable reference to an element of the first slice's type,\n/// and a mutable reference to an element of the last slice's type.\n///\n/// The predicate is executed precisely once on every element in\n/// the slices, and is allowed to modify the elements.\npub fn partition_pair<A, B, F>(slc1: &mut [A], slc2: &mut [B], mut pred: F) -> usize\nwhere\n    F: FnMut(usize, &mut A, &mut B) -> bool,\n{\n    assert_eq!(slc1.len(), slc2.len());\n\n    // This version uses raw pointers and pointer arithmetic to squeeze more\n    // performance out of the code.\n    unsafe {\n        let mut a1 = slc1.as_mut_ptr();\n        let mut a2 = slc2.as_mut_ptr();\n        let mut b1 = a1.add(slc1.len());\n        let mut b2 = a2.add(slc2.len());\n        let start = a1 as usize;\n\n        loop {\n            loop {\n                if a1 == b1 {\n                    return ((a1 as usize) - start) / std::mem::size_of::<A>();\n                }\n                if !pred(\n                    ((a1 as usize) - start) / std::mem::size_of::<A>(),\n                    &mut *a1,\n                    &mut *a2,\n                ) {\n                    break;\n                }\n                a1 = a1.offset(1);\n                a2 = a2.offset(1);\n            }\n\n            loop {\n                b1 = b1.offset(-1);\n                b2 = b2.offset(-1);\n                if a1 == b1 {\n                    return ((a1 as usize) - start) / std::mem::size_of::<A>();\n                }\n                if pred(\n                    ((b1 as usize) - start) / std::mem::size_of::<A>(),\n                    &mut *b1,\n                    &mut *b2,\n                ) {\n                    break;\n                }\n            }\n\n            std::ptr::swap(a1, b1);\n            std::ptr::swap(a2, b2);\n\n            a1 = a1.offset(1);\n            a2 = a2.offset(1);\n        }\n    }\n}\n\n/// Partitions the slice of items to place the nth-ordered item in the nth place,\n/// and the items less than it before and the items more than it after.\npub fn quick_select<T, F>(slc: &mut [T], n: usize, mut order: F)\nwhere\n    F: FnMut(&T, &T) -> Ordering,\n{\n    let mut left = 0;\n    let mut right = slc.len();\n    let mut seed = n as u64;\n\n    loop {\n        let i = left + (hash_u64(right as u64, seed) as usize % (right - left));\n\n        slc.swap(i, right - 1);\n        let ii = left + {\n            let (val, list) = (&mut slc[left..right]).split_last_mut().unwrap();\n            partition(list, |n| order(n, val) == Ordering::Less)\n        };\n        slc.swap(ii, right - 1);\n\n        if ii == n {\n            return;\n        } else if ii > n {\n            right = ii;\n        } else {\n            left = ii + 1;\n        }\n\n        seed += 1;\n    }\n}\n\n/// Merges two slices of things, appending the result to `vec_out`\npub fn merge_slices_append<T: Lerp + Copy, F>(\n    slice1: &[T],\n    slice2: &[T],\n    vec_out: &mut Vec<T>,\n    merge: F,\n) where\n    F: Fn(&T, &T) -> T,\n{\n    // Transform the bounding boxes\n    if slice1.is_empty() || slice2.is_empty() {\n        return;\n    } else if slice1.len() == slice2.len() {\n        for (xf1, xf2) in Iterator::zip(slice1.iter(), slice2.iter()) {\n            vec_out.push(merge(xf1, xf2));\n        }\n    } else if slice1.len() > slice2.len() {\n        let s = (slice1.len() - 1) as f32;\n        for (i, xf1) in slice1.iter().enumerate() {\n            let xf2 = lerp_slice(slice2, i as f32 / s);\n            vec_out.push(merge(xf1, &xf2));\n        }\n    } else if slice1.len() < slice2.len() {\n        let s = (slice2.len() - 1) as f32;\n        for (i, xf2) in slice2.iter().enumerate() {\n            let xf1 = lerp_slice(slice1, i as f32 / s);\n            vec_out.push(merge(&xf1, xf2));\n        }\n    }\n}\n\n/// Merges two slices of things, storing the result in `slice_out`.\n/// Panics if `slice_out` is not the right size.\npub fn merge_slices_to<T: Lerp + Copy, F>(\n    slice1: &[T],\n    slice2: &[T],\n    slice_out: &mut [MaybeUninit<T>],\n    merge: F,\n) where\n    F: Fn(&T, &T) -> T,\n{\n    assert_eq!(slice_out.len(), cmp::max(slice1.len(), slice2.len()));\n\n    // Transform the bounding boxes\n    if slice1.is_empty() || slice2.is_empty() {\n        return;\n    } else if slice1.len() == slice2.len() {\n        for (xfo, (xf1, xf2)) in Iterator::zip(\n            slice_out.iter_mut(),\n            Iterator::zip(slice1.iter(), slice2.iter()),\n        ) {\n            unsafe {\n                *xfo.as_mut_ptr() = merge(xf1, xf2);\n            }\n        }\n    } else if slice1.len() > slice2.len() {\n        let s = (slice1.len() - 1) as f32;\n        for (i, (xfo, xf1)) in Iterator::zip(slice_out.iter_mut(), slice1.iter()).enumerate() {\n            let xf2 = lerp_slice(slice2, i as f32 / s);\n            unsafe {\n                *xfo.as_mut_ptr() = merge(xf1, &xf2);\n            }\n        }\n    } else if slice1.len() < slice2.len() {\n        let s = (slice2.len() - 1) as f32;\n        for (i, (xfo, xf2)) in Iterator::zip(slice_out.iter_mut(), slice2.iter()).enumerate() {\n            let xf1 = lerp_slice(slice1, i as f32 / s);\n            unsafe {\n                *xfo.as_mut_ptr() = merge(&xf1, xf2);\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::cmp::Ordering;\n\n    fn quick_select_ints(list: &mut [i32], i: usize) {\n        quick_select(list, i, |a, b| {\n            if a < b {\n                Ordering::Less\n            } else if a == b {\n                Ordering::Equal\n            } else {\n                Ordering::Greater\n            }\n        });\n    }\n\n    #[test]\n    fn quick_select_1() {\n        let mut list = [8, 9, 7, 4, 6, 1, 0, 5, 3, 2];\n        quick_select_ints(&mut list, 5);\n        assert_eq!(list[5], 5);\n    }\n\n    #[test]\n    fn quick_select_2() {\n        let mut list = [8, 9, 7, 4, 6, 1, 0, 5, 3, 2];\n        quick_select_ints(&mut list, 3);\n        assert_eq!(list[3], 3);\n    }\n\n    #[test]\n    fn quick_select_3() {\n        let mut list = [8, 9, 7, 4, 6, 1, 0, 5, 3, 2];\n        quick_select_ints(&mut list, 0);\n        assert_eq!(list[0], 0);\n    }\n\n    #[test]\n    fn quick_select_4() {\n        let mut list = [8, 9, 7, 4, 6, 1, 0, 5, 3, 2];\n        quick_select_ints(&mut list, 9);\n        assert_eq!(list[9], 9);\n    }\n}\n"
  },
  {
    "path": "src/bbox.rs",
    "content": "#![allow(dead_code)]\n\nuse std::{\n    iter::Iterator,\n    ops::{BitOr, BitOrAssign},\n};\n\nuse crate::{\n    lerp::{lerp, lerp_slice, Lerp},\n    math::{Point, Transform, Vector},\n};\n\nconst BBOX_MAXT_ADJUST: f32 = 1.000_000_24;\n\n/// A 3D axis-aligned bounding box.\n#[derive(Debug, Copy, Clone)]\npub struct BBox {\n    pub min: Point,\n    pub max: Point,\n}\n\nimpl BBox {\n    /// Creates a degenerate BBox with +infinity min and -infinity max.\n    pub fn new() -> BBox {\n        BBox {\n            min: Point::new(std::f32::INFINITY, std::f32::INFINITY, std::f32::INFINITY),\n            max: Point::new(\n                std::f32::NEG_INFINITY,\n                std::f32::NEG_INFINITY,\n                std::f32::NEG_INFINITY,\n            ),\n        }\n    }\n\n    /// Creates a BBox with min as the minimum extent and max as the maximum\n    /// extent.\n    pub fn from_points(min: Point, max: Point) -> BBox {\n        BBox { min: min, max: max }\n    }\n\n    // Returns whether the given ray intersects with the bbox.\n    pub fn intersect_ray(&self, orig: Point, dir_inv: Vector, max_t: f32) -> bool {\n        // Calculate slab intersections\n        let t1 = (self.min.co - orig.co) * dir_inv.co;\n        let t2 = (self.max.co - orig.co) * dir_inv.co;\n\n        // Find the far and near intersection\n        let far_t = t1.max(t2).extend(std::f32::INFINITY);\n        let near_t = t1.min(t2).extend(0.0);\n        let far_hit_t = (far_t.min_element() * BBOX_MAXT_ADJUST).min(max_t);\n        let near_hit_t = near_t.max_element();\n\n        // Did we hit?\n        near_hit_t <= far_hit_t\n    }\n\n    // Creates a new BBox transformed into a different space.\n    pub fn transformed(&self, xform: Transform) -> BBox {\n        // BBox corners\n        let vs = [\n            Point::new(self.min.x(), self.min.y(), self.min.z()),\n            Point::new(self.min.x(), self.min.y(), self.max.z()),\n            Point::new(self.min.x(), self.max.y(), self.min.z()),\n            Point::new(self.min.x(), self.max.y(), self.max.z()),\n            Point::new(self.max.x(), self.min.y(), self.min.z()),\n            Point::new(self.max.x(), self.min.y(), self.max.z()),\n            Point::new(self.max.x(), self.max.y(), self.min.z()),\n            Point::new(self.max.x(), self.max.y(), self.max.z()),\n        ];\n\n        // Transform BBox corners and make new bbox\n        let mut b = BBox::new();\n        for v in &vs {\n            let v = *v * xform;\n            b.min = v.min(b.min);\n            b.max = v.max(b.max);\n        }\n\n        b\n    }\n\n    pub fn surface_area(&self) -> f32 {\n        let d = self.max - self.min;\n        ((d.x() * d.y()) + (d.y() * d.z()) + (d.z() * d.x())) * 2.0\n    }\n\n    pub fn center(&self) -> Point {\n        self.min.lerp(self.max, 0.5)\n    }\n\n    pub fn diagonal(&self) -> f32 {\n        (self.max - self.min).length()\n    }\n\n    pub fn diagonal2(&self) -> f32 {\n        (self.max - self.min).length2()\n    }\n}\n\n/// Union of two `BBox`es.\nimpl BitOr for BBox {\n    type Output = BBox;\n\n    fn bitor(self, rhs: BBox) -> BBox {\n        BBox::from_points(\n            Point {\n                co: self.min.co.min(rhs.min.co),\n            },\n            Point {\n                co: self.max.co.max(rhs.max.co),\n            },\n        )\n    }\n}\n\nimpl BitOrAssign for BBox {\n    fn bitor_assign(&mut self, rhs: BBox) {\n        *self = *self | rhs;\n    }\n}\n\n/// Expand `BBox` by a point.\nimpl BitOr<Point> for BBox {\n    type Output = BBox;\n\n    fn bitor(self, rhs: Point) -> BBox {\n        BBox::from_points(\n            Point {\n                co: self.min.co.min(rhs.co),\n            },\n            Point {\n                co: self.max.co.max(rhs.co),\n            },\n        )\n    }\n}\n\nimpl BitOrAssign<Point> for BBox {\n    fn bitor_assign(&mut self, rhs: Point) {\n        *self = *self | rhs;\n    }\n}\n\nimpl Lerp for BBox {\n    fn lerp(self, other: BBox, alpha: f32) -> BBox {\n        BBox {\n            min: lerp(self.min, other.min, alpha),\n            max: lerp(self.max, other.max, alpha),\n        }\n    }\n}\n\npub fn transform_bbox_slice_from(bbs_in: &[BBox], xforms: &[Transform], bbs_out: &mut Vec<BBox>) {\n    bbs_out.clear();\n\n    // Transform the bounding boxes\n    if xforms.is_empty() {\n        bbs_out.extend_from_slice(bbs_in);\n    } else if bbs_in.len() == xforms.len() {\n        for (bb, xf) in Iterator::zip(bbs_in.iter(), xforms.iter()) {\n            bbs_out.push(bb.transformed(xf.inverse()));\n        }\n    } else if bbs_in.len() > xforms.len() {\n        let s = (bbs_in.len() - 1) as f32;\n        for (i, bb) in bbs_in.iter().enumerate() {\n            bbs_out.push(bb.transformed(lerp_slice(xforms, i as f32 / s).inverse()));\n        }\n    } else if bbs_in.len() < xforms.len() {\n        let s = (xforms.len() - 1) as f32;\n        for (i, xf) in xforms.iter().enumerate() {\n            bbs_out.push(lerp_slice(bbs_in, i as f32 / s).transformed(xf.inverse()));\n        }\n    }\n}\n"
  },
  {
    "path": "src/bbox4.rs",
    "content": "#![allow(dead_code)]\n\nuse std;\nuse std::ops::{BitOr, BitOrAssign};\n\nuse crate::{\n    bbox::BBox,\n    lerp::{lerp, Lerp},\n    math::{Point, Vector},\n};\n\nuse glam::{BVec4A, Vec4};\n\nconst BBOX_MAXT_ADJUST: f32 = 1.000_000_24;\n\n/// A SIMD set of 4 3D axis-aligned bounding boxes.\n#[derive(Debug, Copy, Clone)]\npub struct BBox4 {\n    pub x: (Vec4, Vec4), // (min, max)\n    pub y: (Vec4, Vec4), // (min, max)\n    pub z: (Vec4, Vec4), // (min, max)\n}\n\nimpl BBox4 {\n    /// Creates a degenerate BBox with +infinity min and -infinity max.\n    pub fn new() -> BBox4 {\n        BBox4 {\n            x: (\n                Vec4::splat(std::f32::INFINITY),\n                Vec4::splat(std::f32::NEG_INFINITY),\n            ),\n            y: (\n                Vec4::splat(std::f32::INFINITY),\n                Vec4::splat(std::f32::NEG_INFINITY),\n            ),\n            z: (\n                Vec4::splat(std::f32::INFINITY),\n                Vec4::splat(std::f32::NEG_INFINITY),\n            ),\n        }\n    }\n\n    /// Creates a BBox with min as the minimum extent and max as the maximum\n    /// extent.\n    pub fn from_bboxes(b1: BBox, b2: BBox, b3: BBox, b4: BBox) -> BBox4 {\n        BBox4 {\n            x: (\n                Vec4::new(b1.min.x(), b2.min.x(), b3.min.x(), b4.min.x()),\n                Vec4::new(b1.max.x(), b2.max.x(), b3.max.x(), b4.max.x()),\n            ),\n            y: (\n                Vec4::new(b1.min.y(), b2.min.y(), b3.min.y(), b4.min.y()),\n                Vec4::new(b1.max.y(), b2.max.y(), b3.max.y(), b4.max.y()),\n            ),\n            z: (\n                Vec4::new(b1.min.z(), b2.min.z(), b3.min.z(), b4.min.z()),\n                Vec4::new(b1.max.z(), b2.max.z(), b3.max.z(), b4.max.z()),\n            ),\n        }\n    }\n\n    // Returns whether the given ray intersects with the bboxes.\n    pub fn intersect_ray(&self, orig: Point, dir_inv: Vector, max_t: f32) -> BVec4A {\n        // Get the ray data into SIMD format.\n        let ro_x = Vec4::splat(orig.co[0]);\n        let ro_y = Vec4::splat(orig.co[1]);\n        let ro_z = Vec4::splat(orig.co[2]);\n        let rdi_x = Vec4::splat(dir_inv.co[0]);\n        let rdi_y = Vec4::splat(dir_inv.co[1]);\n        let rdi_z = Vec4::splat(dir_inv.co[2]);\n        let max_t = Vec4::splat(max_t);\n\n        // Slab tests\n        let t1_x = (self.x.0 - ro_x) * rdi_x;\n        let t1_y = (self.y.0 - ro_y) * rdi_y;\n        let t1_z = (self.z.0 - ro_z) * rdi_z;\n        let t2_x = (self.x.1 - ro_x) * rdi_x;\n        let t2_y = (self.y.1 - ro_y) * rdi_y;\n        let t2_z = (self.z.1 - ro_z) * rdi_z;\n\n        // Get the far and near t hits for each axis.\n        let t_far_x = t1_x.max(t2_x);\n        let t_far_y = t1_y.max(t2_y);\n        let t_far_z = t1_z.max(t2_z);\n        let t_near_x = t1_x.min(t2_x);\n        let t_near_y = t1_y.min(t2_y);\n        let t_near_z = t1_z.min(t2_z);\n\n        // Calculate over-all far t hit.\n        let far_t = (t_far_x.min(t_far_y.min(t_far_z)) * Vec4::splat(BBOX_MAXT_ADJUST)).min(max_t);\n\n        // Calculate over-all near t hit.\n        let near_t = t_near_x.max(t_near_y).max(t_near_z.max(Vec4::splat(0.0)));\n\n        // Hit results\n        near_t.cmplt(far_t)\n    }\n}\n\n/// Union of two BBoxes.\nimpl BitOr for BBox4 {\n    type Output = BBox4;\n\n    fn bitor(self, rhs: BBox4) -> BBox4 {\n        BBox4 {\n            x: (self.x.0.min(rhs.x.0), self.x.1.max(rhs.x.1)),\n            y: (self.y.0.min(rhs.y.0), self.y.1.max(rhs.y.1)),\n            z: (self.z.0.min(rhs.z.0), self.z.1.max(rhs.z.1)),\n        }\n    }\n}\n\nimpl BitOrAssign for BBox4 {\n    fn bitor_assign(&mut self, rhs: BBox4) {\n        *self = *self | rhs;\n    }\n}\n\nimpl Lerp for BBox4 {\n    fn lerp(self, other: BBox4, alpha: f32) -> BBox4 {\n        BBox4 {\n            x: (\n                lerp(self.x.0, other.x.0, alpha),\n                lerp(self.x.1, other.x.1, alpha),\n            ),\n            y: (\n                lerp(self.y.0, other.y.0, alpha),\n                lerp(self.y.1, other.y.1, alpha),\n            ),\n            z: (\n                lerp(self.z.0, other.z.0, alpha),\n                lerp(self.z.1, other.z.1, alpha),\n            ),\n        }\n    }\n}\n"
  },
  {
    "path": "src/boundable.rs",
    "content": "#![allow(dead_code)]\n\nuse crate::bbox::BBox;\n\npub trait Boundable {\n    fn bounds(&self) -> &[BBox];\n}\n"
  },
  {
    "path": "src/camera.rs",
    "content": "#![allow(dead_code)]\n\nuse kioku::Arena;\n\nuse crate::{\n    lerp::lerp_slice,\n    math::{Point, Transform, Vector},\n    ray::Ray,\n    sampling::square_to_circle,\n};\n\n#[derive(Copy, Clone, Debug)]\npub struct Camera<'a> {\n    transforms: &'a [Transform],\n    fovs: &'a [f32],\n    tfovs: &'a [f32],\n    aperture_radii: &'a [f32],\n    focus_distances: &'a [f32],\n}\n\nimpl<'a> Camera<'a> {\n    pub fn new(\n        arena: &'a Arena,\n        transforms: &[Transform],\n        fovs: &[f32],\n        mut aperture_radii: &[f32],\n        mut focus_distances: &[f32],\n    ) -> Camera<'a> {\n        assert!(!transforms.is_empty(), \"Camera has no transform(s)!\");\n        assert!(!fovs.is_empty(), \"Camera has no fov(s)!\");\n\n        // Aperture needs focus distance and vice-versa.\n        if aperture_radii.is_empty() || focus_distances.is_empty() {\n            aperture_radii = &[0.0];\n            focus_distances = &[1.0];\n\n            if aperture_radii.is_empty() && !focus_distances.is_empty() {\n                println!(\n                    \"WARNING: camera has aperture radius but no focus distance.  Disabling \\\n                     focal blur.\"\n                );\n            } else if !aperture_radii.is_empty() && focus_distances.is_empty() {\n                println!(\n                    \"WARNING: camera has focus distance but no aperture radius.  Disabling \\\n                     focal blur.\"\n                );\n            }\n        }\n\n        // Can't have focus distance of zero.\n        if focus_distances.iter().any(|d| *d == 0.0) {\n            if aperture_radii.iter().any(|a| *a > 0.0) {\n                println!(\"WARNING: camera focal distance is zero or less.  Disabling focal blur.\");\n            }\n            aperture_radii = &[0.0];\n            focus_distances = &[1.0];\n        }\n\n        // Convert angle fov into linear fov.\n        let tfovs: Vec<f32> = fovs\n            .iter()\n            .map(|n| (n / 2.0).sin() / (n / 2.0).cos())\n            .collect();\n\n        Camera {\n            transforms: arena.copy_slice(&transforms),\n            fovs: arena.copy_slice(&fovs),\n            tfovs: arena.copy_slice(&tfovs),\n            aperture_radii: arena.copy_slice(&aperture_radii),\n            focus_distances: arena.copy_slice(&focus_distances),\n        }\n    }\n\n    pub fn generate_ray(&self, x: f32, y: f32, time: f32, wavelength: f32, u: f32, v: f32) -> Ray {\n        // Get time-interpolated camera settings\n        let transform = lerp_slice(self.transforms, time);\n        let tfov = lerp_slice(self.tfovs, time);\n        let aperture_radius = lerp_slice(self.aperture_radii, time);\n        let focus_distance = lerp_slice(self.focus_distances, time);\n\n        // Ray origin\n        let orig = {\n            let (u, v) = square_to_circle((u * 2.0) - 1.0, (v * 2.0) - 1.0);\n            Point::new(aperture_radius * u, aperture_radius * v, 0.0)\n        };\n\n        // Ray direction\n        let dir = Vector::new(\n            (x * tfov) - (orig.x() / focus_distance),\n            (y * tfov) - (orig.y() / focus_distance),\n            1.0,\n        )\n        .normalized();\n\n        Ray {\n            orig: orig * transform,\n            dir: dir * transform,\n            time: time,\n            wavelength: wavelength,\n            max_t: std::f32::INFINITY,\n        }\n    }\n}\n"
  },
  {
    "path": "src/color.rs",
    "content": "use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign};\n\npub use color::{\n    rec709_e_to_xyz, rec709_to_xyz, xyz_to_aces_ap0, xyz_to_aces_ap0_e, xyz_to_rec709,\n    xyz_to_rec709_e,\n};\nuse compact::fluv::fluv32;\nuse glam::Vec4;\nuse half::f16;\nuse spectral_upsampling::meng::{spectrum_xyz_to_p_4, EQUAL_ENERGY_REFLECTANCE};\n\nuse crate::{lerp::Lerp, math::fast_exp};\n\n// Minimum and maximum wavelength of light we care about, in nanometers\nconst WL_MIN: f32 = 380.0;\nconst WL_MAX: f32 = 700.0;\nconst WL_RANGE: f32 = WL_MAX - WL_MIN;\nconst WL_RANGE_Q: f32 = WL_RANGE / 4.0;\n\npub fn map_0_1_to_wavelength(n: f32) -> f32 {\n    n * WL_RANGE + WL_MIN\n}\n\n#[inline(always)]\nfn nth_wavelength(hero_wavelength: f32, n: usize) -> f32 {\n    let wl = hero_wavelength + (WL_RANGE_Q * n as f32);\n    if wl > WL_MAX {\n        wl - WL_RANGE\n    } else {\n        wl\n    }\n}\n\n/// Returns all wavelengths of a hero wavelength set as a Vec4\n#[inline(always)]\nfn wavelengths(hero_wavelength: f32) -> Vec4 {\n    Vec4::new(\n        nth_wavelength(hero_wavelength, 0),\n        nth_wavelength(hero_wavelength, 1),\n        nth_wavelength(hero_wavelength, 2),\n        nth_wavelength(hero_wavelength, 3),\n    )\n}\n\n//----------------------------------------------------------------\n\n#[derive(Debug, Copy, Clone)]\npub enum Color {\n    XYZ(f32, f32, f32),\n    Blackbody {\n        temperature: f32, // In kelvin\n        factor: f32,      // Brightness multiplier\n    },\n    // Same as Blackbody except with the spectrum's energy roughly\n    // normalized.\n    Temperature {\n        temperature: f32, // In kelvin\n        factor: f32,      // Brightness multiplier\n    },\n}\n\nimpl Color {\n    #[inline(always)]\n    pub fn new_xyz(xyz: (f32, f32, f32)) -> Self {\n        Color::XYZ(xyz.0, xyz.1, xyz.2)\n    }\n\n    #[inline(always)]\n    pub fn new_blackbody(temp: f32, fac: f32) -> Self {\n        Color::Blackbody {\n            temperature: temp,\n            factor: fac,\n        }\n    }\n\n    #[inline(always)]\n    pub fn new_temperature(temp: f32, fac: f32) -> Self {\n        Color::Temperature {\n            temperature: temp,\n            factor: fac,\n        }\n    }\n\n    pub fn to_spectral_sample(self, hero_wavelength: f32) -> SpectralSample {\n        let wls = wavelengths(hero_wavelength);\n        match self {\n            Color::XYZ(x, y, z) => SpectralSample {\n                e: xyz_to_spectrum_4((x, y, z), wls),\n                hero_wavelength: hero_wavelength,\n            },\n            Color::Blackbody {\n                temperature,\n                factor,\n            } => {\n                SpectralSample::from_parts(\n                    // TODO: make this SIMD\n                    Vec4::new(\n                        plancks_law(temperature, wls[0]) * factor,\n                        plancks_law(temperature, wls[1]) * factor,\n                        plancks_law(temperature, wls[2]) * factor,\n                        plancks_law(temperature, wls[3]) * factor,\n                    ),\n                    hero_wavelength,\n                )\n            }\n            Color::Temperature {\n                temperature,\n                factor,\n            } => {\n                SpectralSample::from_parts(\n                    // TODO: make this SIMD\n                    Vec4::new(\n                        plancks_law_normalized(temperature, wls[0]) * factor,\n                        plancks_law_normalized(temperature, wls[1]) * factor,\n                        plancks_law_normalized(temperature, wls[2]) * factor,\n                        plancks_law_normalized(temperature, wls[3]) * factor,\n                    ),\n                    hero_wavelength,\n                )\n            }\n        }\n    }\n\n    /// Calculates an approximate total spectral energy of the color.\n    ///\n    /// Note: this really is very _approximate_.\n    pub fn approximate_energy(self) -> f32 {\n        // TODO: better approximation for Blackbody and Temperature.\n        match self {\n            Color::XYZ(_, y, _) => y,\n\n            Color::Blackbody {\n                temperature,\n                factor,\n            } => {\n                let t2 = temperature * temperature;\n                t2 * t2 * factor\n            }\n\n            Color::Temperature { factor, .. } => factor,\n        }\n    }\n\n    /// Returns the post-compression size of this color.\n    pub fn compressed_size(&self) -> usize {\n        match self {\n            Color::XYZ(_, _, _) => 5,\n\n            Color::Blackbody { .. } => 5,\n\n            Color::Temperature { .. } => 5,\n        }\n    }\n\n    /// Writes the compressed form of this color to `out_data`.\n    ///\n    /// `out_data` must be at least `compressed_size()` bytes long, otherwise\n    /// this method will panic.\n    ///\n    /// Returns the number of bytes written.\n    pub fn write_compressed(&self, out_data: &mut [u8]) -> usize {\n        match *self {\n            Color::XYZ(x, y, z) => {\n                out_data[0] = 0; // Discriminant\n                (&mut out_data[1..5]).copy_from_slice(&fluv32::encode((x, y, z)).to_ne_bytes()[..]);\n            }\n\n            Color::Blackbody {\n                temperature,\n                factor,\n            } => {\n                out_data[0] = 1; // Discriminant\n                let tmp = (temperature.min(std::u16::MAX as f32) as u16).to_le_bytes();\n                let fac = f16::from_f32(factor).to_bits().to_le_bytes();\n                out_data[1] = tmp[0];\n                out_data[2] = tmp[1];\n                out_data[3] = fac[0];\n                out_data[4] = fac[1];\n            }\n\n            Color::Temperature {\n                temperature,\n                factor,\n            } => {\n                out_data[0] = 2; // Discriminant\n                let tmp = (temperature.min(std::u16::MAX as f32) as u16).to_le_bytes();\n                let fac = f16::from_f32(factor).to_bits().to_le_bytes();\n                out_data[1] = tmp[0];\n                out_data[2] = tmp[1];\n                out_data[3] = fac[0];\n                out_data[4] = fac[1];\n            }\n        }\n        self.compressed_size()\n    }\n\n    /// Constructs a Color from compressed color data, and also returns the\n    /// number of bytes consumed from `in_data`.\n    pub fn from_compressed(in_data: &[u8]) -> (Color, usize) {\n        match in_data[0] {\n            0 => {\n                // XYZ\n                let mut bytes = [0u8; 4];\n                (&mut bytes[..]).copy_from_slice(&in_data[1..5]);\n                let (x, y, z) = fluv32::decode(u32::from_ne_bytes(bytes));\n                (Color::XYZ(x, y, z), 5)\n            }\n\n            1 => {\n                // Blackbody\n                let mut tmp = [0u8; 2];\n                let mut fac = [0u8; 2];\n                tmp[0] = in_data[1];\n                tmp[1] = in_data[2];\n                fac[0] = in_data[3];\n                fac[1] = in_data[4];\n                let tmp = u16::from_le_bytes(tmp);\n                let fac = f16::from_bits(u16::from_le_bytes(fac));\n                (\n                    Color::Blackbody {\n                        temperature: tmp as f32,\n                        factor: fac.into(),\n                    },\n                    5,\n                )\n            }\n\n            2 => {\n                // Temperature\n                let mut tmp = [0u8; 2];\n                let mut fac = [0u8; 2];\n                tmp[0] = in_data[1];\n                tmp[1] = in_data[2];\n                fac[0] = in_data[3];\n                fac[1] = in_data[4];\n                let tmp = u16::from_le_bytes(tmp);\n                let fac = f16::from_bits(u16::from_le_bytes(fac));\n                (\n                    Color::Temperature {\n                        temperature: tmp as f32,\n                        factor: fac.into(),\n                    },\n                    5,\n                )\n            }\n\n            _ => unreachable!(),\n        }\n    }\n}\n\nimpl Mul<f32> for Color {\n    type Output = Self;\n\n    fn mul(self, rhs: f32) -> Self {\n        match self {\n            Color::XYZ(x, y, z) => Color::XYZ(x * rhs, y * rhs, z * rhs),\n\n            Color::Blackbody {\n                temperature,\n                factor,\n            } => Color::Blackbody {\n                temperature: temperature,\n                factor: factor * rhs,\n            },\n\n            Color::Temperature {\n                temperature,\n                factor,\n            } => Color::Temperature {\n                temperature: temperature,\n                factor: factor * rhs,\n            },\n        }\n    }\n}\n\nimpl MulAssign<f32> for Color {\n    fn mul_assign(&mut self, rhs: f32) {\n        *self = *self * rhs;\n    }\n}\n\nimpl Lerp for Color {\n    /// Note that this isn't a proper lerp in spectral space.  However,\n    /// for our purposes that should be fine: all we care about is that\n    /// the interpolation is smooth and \"reasonable\".\n    ///\n    /// If at some point it turns out this causes artifacts, then we\n    /// also have bigger problems: texture filtering in the shading\n    /// pipeline will have the same issues, which will be even harder\n    /// to address.  However, I strongly suspect this will not be an issue.\n    /// (Famous last words!)\n    fn lerp(self, other: Self, alpha: f32) -> Self {\n        let inv_alpha = 1.0 - alpha;\n        match (self, other) {\n            (Color::XYZ(x1, y1, z1), Color::XYZ(x2, y2, z2)) => Color::XYZ(\n                (x1 * inv_alpha) + (x2 * alpha),\n                (y1 * inv_alpha) + (y2 * alpha),\n                (z1 * inv_alpha) + (z2 * alpha),\n            ),\n\n            (\n                Color::Blackbody {\n                    temperature: tmp1,\n                    factor: fac1,\n                },\n                Color::Blackbody {\n                    temperature: tmp2,\n                    factor: fac2,\n                },\n            ) => Color::Blackbody {\n                temperature: (tmp1 * inv_alpha) + (tmp2 * alpha),\n                factor: (fac1 * inv_alpha) + (fac2 * alpha),\n            },\n\n            (\n                Color::Temperature {\n                    temperature: tmp1,\n                    factor: fac1,\n                },\n                Color::Temperature {\n                    temperature: tmp2,\n                    factor: fac2,\n                },\n            ) => Color::Temperature {\n                temperature: (tmp1 * inv_alpha) + (tmp2 * alpha),\n                factor: (fac1 * inv_alpha) + (fac2 * alpha),\n            },\n\n            _ => panic!(\"Cannot lerp colors with different representations.\"),\n        }\n    }\n}\n\nfn plancks_law(temperature: f32, wavelength: f32) -> f32 {\n    const C: f32 = 299_792_458.0; // Speed of light\n    const H: f32 = 6.626_070_15e-34; // Planck constant\n    const KB: f32 = 1.380_648_52e-23; // Boltzmann constant\n\n    // At 400 kelvin and below, the spectrum is black anyway,\n    // but the equations become numerically unstable somewhere\n    // around 100 kelvin.  So just return zero energy below 200.\n    // (Technically there is still a tiny amount of energy that\n    // we're losing this way, but it's incredibly tiny, with tons\n    // of zeros after the decimal point--way less energy than would\n    // ever, ever, ever even have the slightest chance of showing\n    // impacting a render.)\n    if temperature < 200.0 {\n        return 0.0;\n    }\n\n    // Convert the wavelength from nanometers to meters for\n    // the equations below.\n    let wavelength = wavelength * 1.0e-9;\n\n    // // As written at https://en.wikipedia.org/wiki/Planck's_law, here for\n    // // reference and clarity:\n    // let a = (2.0 * H * C * C) / (wavelength * wavelength * wavelength * wavelength * wavelength);\n    // let b = 1.0 / (((H * C) / (wavelength * KB * temperature)).exp() - 1.0);\n    // let energy = a * b;\n\n    // Optimized version of the commented code above:\n    const TMP1: f32 = (2.0f64 * H as f64 * C as f64 * C as f64) as f32;\n    const TMP2: f32 = (H as f64 * C as f64 / KB as f64) as f32;\n    let wl5 = {\n        let wl2 = wavelength * wavelength;\n        wl2 * wl2 * wavelength\n    };\n    let tmp3 = wl5 * (fast_exp(TMP2 / (wavelength * temperature)) - 1.0);\n    let energy = TMP1 / tmp3;\n\n    // Convert energy to appropriate units and return.\n    (energy * 1.0e-6).max(0.0)\n}\n\n/// Same as above, except normalized to keep roughly equal spectral\n/// energy across temperatures.  This makes it easier to use for\n/// choosing colors without making brightness explode.\nfn plancks_law_normalized(temperature: f32, wavelength: f32) -> f32 {\n    let t2 = temperature * temperature;\n    plancks_law(temperature, wavelength) * 4.0e7 / (t2 * t2)\n}\n\n//----------------------------------------------------------------\n\n#[derive(Copy, Clone, Debug)]\npub struct SpectralSample {\n    pub e: Vec4,\n    hero_wavelength: f32,\n}\n\nimpl SpectralSample {\n    pub fn new(wavelength: f32) -> SpectralSample {\n        debug_assert!(wavelength >= WL_MIN && wavelength <= WL_MAX);\n        SpectralSample {\n            e: Vec4::splat(0.0),\n            hero_wavelength: wavelength,\n        }\n    }\n\n    #[allow(dead_code)]\n    pub fn from_value(value: f32, wavelength: f32) -> SpectralSample {\n        debug_assert!(wavelength >= WL_MIN && wavelength <= WL_MAX);\n        SpectralSample {\n            e: Vec4::splat(value),\n            hero_wavelength: wavelength,\n        }\n    }\n\n    pub fn from_parts(e: Vec4, wavelength: f32) -> SpectralSample {\n        debug_assert!(wavelength >= WL_MIN && wavelength <= WL_MAX);\n        SpectralSample {\n            e: e,\n            hero_wavelength: wavelength,\n        }\n    }\n\n    /// Returns the nth wavelength\n    fn wl_n(&self, n: usize) -> f32 {\n        let wl = self.hero_wavelength + (WL_RANGE_Q * n as f32);\n        if wl > WL_MAX {\n            wl - WL_RANGE\n        } else {\n            wl\n        }\n    }\n}\n\nimpl Add for SpectralSample {\n    type Output = SpectralSample;\n    fn add(self, rhs: SpectralSample) -> Self::Output {\n        assert_eq!(self.hero_wavelength, rhs.hero_wavelength);\n        SpectralSample {\n            e: self.e + rhs.e,\n            hero_wavelength: self.hero_wavelength,\n        }\n    }\n}\n\nimpl AddAssign for SpectralSample {\n    fn add_assign(&mut self, rhs: SpectralSample) {\n        assert_eq!(self.hero_wavelength, rhs.hero_wavelength);\n        self.e = self.e + rhs.e;\n    }\n}\n\nimpl Mul for SpectralSample {\n    type Output = SpectralSample;\n    fn mul(self, rhs: SpectralSample) -> Self::Output {\n        assert_eq!(self.hero_wavelength, rhs.hero_wavelength);\n        SpectralSample {\n            e: self.e * rhs.e,\n            hero_wavelength: self.hero_wavelength,\n        }\n    }\n}\n\nimpl MulAssign for SpectralSample {\n    fn mul_assign(&mut self, rhs: SpectralSample) {\n        assert_eq!(self.hero_wavelength, rhs.hero_wavelength);\n        self.e = self.e * rhs.e;\n    }\n}\n\nimpl Mul<f32> for SpectralSample {\n    type Output = SpectralSample;\n    fn mul(self, rhs: f32) -> Self::Output {\n        SpectralSample {\n            e: self.e * rhs,\n            hero_wavelength: self.hero_wavelength,\n        }\n    }\n}\n\nimpl MulAssign<f32> for SpectralSample {\n    fn mul_assign(&mut self, rhs: f32) {\n        self.e = self.e * rhs;\n    }\n}\n\nimpl Div<f32> for SpectralSample {\n    type Output = SpectralSample;\n    fn div(self, rhs: f32) -> Self::Output {\n        SpectralSample {\n            e: self.e / rhs,\n            hero_wavelength: self.hero_wavelength,\n        }\n    }\n}\n\nimpl DivAssign<f32> for SpectralSample {\n    fn div_assign(&mut self, rhs: f32) {\n        self.e = self.e / rhs;\n    }\n}\n\n//----------------------------------------------------------------\n\n#[derive(Copy, Clone, Debug)]\npub struct XYZ {\n    pub x: f32,\n    pub y: f32,\n    pub z: f32,\n}\n\nimpl XYZ {\n    pub fn new(x: f32, y: f32, z: f32) -> XYZ {\n        XYZ { x: x, y: y, z: z }\n    }\n\n    pub fn from_wavelength(wavelength: f32, intensity: f32) -> XYZ {\n        XYZ {\n            x: x_1931(wavelength) * intensity,\n            y: y_1931(wavelength) * intensity,\n            z: z_1931(wavelength) * intensity,\n        }\n    }\n\n    pub fn from_spectral_sample(ss: &SpectralSample) -> XYZ {\n        let xyz0 = XYZ::from_wavelength(ss.wl_n(0), ss.e[0]);\n        let xyz1 = XYZ::from_wavelength(ss.wl_n(1), ss.e[1]);\n        let xyz2 = XYZ::from_wavelength(ss.wl_n(2), ss.e[2]);\n        let xyz3 = XYZ::from_wavelength(ss.wl_n(3), ss.e[3]);\n        (xyz0 + xyz1 + xyz2 + xyz3) * 0.75\n    }\n\n    pub fn to_tuple(&self) -> (f32, f32, f32) {\n        (self.x, self.y, self.z)\n    }\n}\n\nimpl Lerp for XYZ {\n    fn lerp(self, other: XYZ, alpha: f32) -> XYZ {\n        (self * (1.0 - alpha)) + (other * alpha)\n    }\n}\n\nimpl Add for XYZ {\n    type Output = XYZ;\n    fn add(self, rhs: XYZ) -> Self::Output {\n        XYZ {\n            x: self.x + rhs.x,\n            y: self.y + rhs.y,\n            z: self.z + rhs.z,\n        }\n    }\n}\n\nimpl AddAssign for XYZ {\n    fn add_assign(&mut self, rhs: XYZ) {\n        self.x += rhs.x;\n        self.y += rhs.y;\n        self.z += rhs.z;\n    }\n}\n\nimpl Mul<f32> for XYZ {\n    type Output = XYZ;\n    fn mul(self, rhs: f32) -> Self::Output {\n        XYZ {\n            x: self.x * rhs,\n            y: self.y * rhs,\n            z: self.z * rhs,\n        }\n    }\n}\n\nimpl MulAssign<f32> for XYZ {\n    fn mul_assign(&mut self, rhs: f32) {\n        self.x *= rhs;\n        self.y *= rhs;\n        self.z *= rhs;\n    }\n}\n\nimpl Div<f32> for XYZ {\n    type Output = XYZ;\n    fn div(self, rhs: f32) -> Self::Output {\n        XYZ {\n            x: self.x / rhs,\n            y: self.y / rhs,\n            z: self.z / rhs,\n        }\n    }\n}\n\nimpl DivAssign<f32> for XYZ {\n    fn div_assign(&mut self, rhs: f32) {\n        self.x /= rhs;\n        self.y /= rhs;\n        self.z /= rhs;\n    }\n}\n\n//----------------------------------------------------------------\n\n/// Samples an CIE 1931 XYZ color at a particular set of wavelengths, according to\n/// the method in the paper \"Physically Meaningful Rendering using Tristimulus\n/// Colours\" by Meng et al.\n#[inline(always)]\nfn xyz_to_spectrum_4(xyz: (f32, f32, f32), wavelengths: Vec4) -> Vec4 {\n    spectrum_xyz_to_p_4(wavelengths, xyz) * Vec4::splat(1.0 / EQUAL_ENERGY_REFLECTANCE)\n    // aces_to_spectrum_p4(wavelengths, xyz_to_aces_ap0_e(xyz))\n}\n\n/// Close analytic approximations of the CIE 1931 XYZ color curves.\n/// From the paper \"Simple Analytic Approximations to the CIE XYZ Color Matching\n/// Functions\" by Wyman et al.\npub fn x_1931(wavelength: f32) -> f32 {\n    let t1 = (wavelength - 442.0) * (if wavelength < 442.0 { 0.0624 } else { 0.0374 });\n    let t2 = (wavelength - 599.8) * (if wavelength < 599.8 { 0.0264 } else { 0.0323 });\n    let t3 = (wavelength - 501.1) * (if wavelength < 501.1 { 0.0490 } else { 0.0382 });\n    (0.362 * fast_exp(-0.5 * t1 * t1)) + (1.056 * fast_exp(-0.5 * t2 * t2))\n        - (0.065 * fast_exp(-0.5 * t3 * t3))\n}\n\npub fn y_1931(wavelength: f32) -> f32 {\n    let t1 = (wavelength - 568.8) * (if wavelength < 568.8 { 0.0213 } else { 0.0247 });\n    let t2 = (wavelength - 530.9) * (if wavelength < 530.9 { 0.0613 } else { 0.0322 });\n    (0.821 * fast_exp(-0.5 * t1 * t1)) + (0.286 * fast_exp(-0.5 * t2 * t2))\n}\n\npub fn z_1931(wavelength: f32) -> f32 {\n    let t1 = (wavelength - 437.0) * (if wavelength < 437.0 { 0.0845 } else { 0.0278 });\n    let t2 = (wavelength - 459.0) * (if wavelength < 459.0 { 0.0385 } else { 0.0725 });\n    (1.217 * fast_exp(-0.5 * t1 * t1)) + (0.681 * fast_exp(-0.5 * t2 * t2))\n}\n"
  },
  {
    "path": "src/fp_utils.rs",
    "content": "//! Utilities for handling floating point precision issues\n//!\n//! This is based on the work in section 3.9 of \"Physically Based Rendering:\n//! From Theory to Implementation\" 3rd edition by Pharr et al.\n\nuse crate::math::{dot, Normal, Point, Vector};\n\n#[inline(always)]\npub fn fp_gamma(n: u32) -> f32 {\n    use std::f32::EPSILON;\n    let e = EPSILON * 0.5;\n    (e * n as f32) / (1.0 - (e * n as f32))\n}\n\npub fn increment_ulp(v: f32) -> f32 {\n    if v.is_finite() {\n        if v > 0.0 {\n            f32::from_bits(v.to_bits() + 1)\n        } else if v < -0.0 {\n            f32::from_bits(v.to_bits() - 1)\n        } else {\n            f32::from_bits(0x00_00_00_01)\n        }\n    } else {\n        // Infinity or NaN.\n        v\n    }\n}\n\npub fn decrement_ulp(v: f32) -> f32 {\n    if v.is_finite() {\n        if v > 0.0 {\n            f32::from_bits(v.to_bits() - 1)\n        } else if v < -0.0 {\n            f32::from_bits(v.to_bits() + 1)\n        } else {\n            f32::from_bits(0x80_00_00_01)\n        }\n    } else {\n        // Infinity or NaN.\n        v\n    }\n}\n\npub fn robust_ray_origin(pos: Point, pos_err: f32, nor: Normal, ray_dir: Vector) -> Point {\n    // Get surface normal pointing in the same\n    // direction as ray_dir.\n    let nor = {\n        let nor = nor.into_vector();\n        if dot(nor, ray_dir) >= 0.0 {\n            nor\n        } else {\n            -nor\n        }\n    };\n\n    // Calculate offset point\n    let d = dot(nor.abs(), Vector::new(pos_err, pos_err, pos_err));\n    let offset = nor * d;\n    let p = pos + offset;\n\n    // Calculate ulp offsets\n    let x = if nor.x() >= 0.0 {\n        increment_ulp(p.x())\n    } else {\n        decrement_ulp(p.x())\n    };\n\n    let y = if nor.y() >= 0.0 {\n        increment_ulp(p.y())\n    } else {\n        decrement_ulp(p.y())\n    };\n\n    let z = if nor.z() >= 0.0 {\n        increment_ulp(p.z())\n    } else {\n        decrement_ulp(p.z())\n    };\n\n    Point::new(x, y, z)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn inc_ulp() {\n        assert!(increment_ulp(1.0) > 1.0);\n        assert!(increment_ulp(-1.0) > -1.0);\n    }\n\n    #[test]\n    fn dec_ulp() {\n        assert!(decrement_ulp(1.0) < 1.0);\n        assert!(decrement_ulp(-1.0) < -1.0);\n    }\n\n    #[test]\n    fn inc_ulp_zero() {\n        assert!(increment_ulp(0.0) > 0.0);\n        assert!(increment_ulp(0.0) > -0.0);\n        assert!(increment_ulp(-0.0) > 0.0);\n        assert!(increment_ulp(-0.0) > -0.0);\n    }\n\n    #[test]\n    fn dec_ulp_zero() {\n        assert!(decrement_ulp(0.0) < 0.0);\n        assert!(decrement_ulp(0.0) < -0.0);\n        assert!(decrement_ulp(-0.0) < 0.0);\n        assert!(decrement_ulp(-0.0) < -0.0);\n    }\n\n    #[test]\n    fn inc_dec_ulp() {\n        assert_eq!(decrement_ulp(increment_ulp(1.0)), 1.0);\n        assert_eq!(decrement_ulp(increment_ulp(-1.0)), -1.0);\n        assert_eq!(decrement_ulp(increment_ulp(1.2)), 1.2);\n        assert_eq!(decrement_ulp(increment_ulp(-1.2)), -1.2);\n    }\n\n    #[test]\n    fn dec_inc_ulp() {\n        assert_eq!(increment_ulp(decrement_ulp(1.0)), 1.0);\n        assert_eq!(increment_ulp(decrement_ulp(-1.0)), -1.0);\n        assert_eq!(increment_ulp(decrement_ulp(1.2)), 1.2);\n        assert_eq!(increment_ulp(decrement_ulp(-1.2)), -1.2);\n    }\n}\n"
  },
  {
    "path": "src/hash.rs",
    "content": "pub fn hash_u32(n: u32, seed: u32) -> u32 {\n    let mut hash = n;\n    for _ in 0..3 {\n        hash = hash.wrapping_mul(0x736caf6f);\n        hash ^= hash.wrapping_shr(16);\n        hash ^= seed;\n    }\n\n    hash\n}\n\npub fn hash_u64(n: u64, seed: u64) -> u64 {\n    let mut hash = n;\n    for _ in 0..4 {\n        hash = hash.wrapping_mul(32_416_190_071 * 314_604_959);\n        hash ^= hash.wrapping_shr(32);\n        hash ^= seed;\n    }\n\n    hash\n}\n\n/// Returns a random float in [0, 1] based on 'n' and a seed.\n/// Generally use n for getting a bunch of different random\n/// numbers, and use seed to vary between runs.\npub fn hash_u32_to_f32(n: u32, seed: u32) -> f32 {\n    const INV_MAX: f32 = 1.0 / std::u32::MAX as f32;\n    hash_u32(n, seed) as f32 * INV_MAX\n}\n"
  },
  {
    "path": "src/hilbert.rs",
    "content": "#![allow(dead_code)]\n\nconst N: u32 = 1 << 16;\n\n// Utility function used by the functions below.\nfn hil_rot(n: u32, rx: u32, ry: u32, x: &mut u32, y: &mut u32) {\n    use std::mem;\n    if ry == 0 {\n        if rx == 1 {\n            *x = (n - 1).wrapping_sub(*x);\n            *y = (n - 1).wrapping_sub(*y);\n        }\n        mem::swap(x, y);\n    }\n}\n\n/// Convert (x,y) to hilbert curve index.\n///\n/// x: The x coordinate.  Must be a positive integer no greater than 2^16-1.\n/// y: The y coordinate.  Must be a positive integer no greater than 2^16-1.\n///\n/// Returns the hilbert curve index corresponding to the (x,y) coordinates given.\npub fn xy2d(x: u32, y: u32) -> u32 {\n    assert!(x < N);\n    assert!(y < N);\n\n    let (mut x, mut y) = (x, y);\n    let mut d = 0;\n    let mut s = N >> 1;\n    while s > 0 {\n        let rx = if (x & s) > 0 { 1 } else { 0 };\n        let ry = if (y & s) > 0 { 1 } else { 0 };\n        d += s * s * ((3 * rx) ^ ry);\n        hil_rot(s, rx, ry, &mut x, &mut y);\n\n        s >>= 1\n    }\n\n    d\n}\n\n/// Convert hilbert curve index to (x,y).\n///\n/// d: The hilbert curve index.\n///\n/// Returns the (x, y) coords at the given index.\npub fn d2xy(d: u32) -> (u32, u32) {\n    let (mut x, mut y) = (0, 0);\n    let mut s = 1;\n    let mut t = d;\n    while s < N {\n        let rx = 1 & (t >> 1);\n        let ry = 1 & (t ^ rx);\n        hil_rot(s, rx, ry, &mut x, &mut y);\n        x += s * rx;\n        y += s * ry;\n        t >>= 2;\n\n        s <<= 1;\n    }\n\n    (x, y)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn reversible() {\n        let d = 54;\n        let (x, y) = d2xy(d);\n        let d2 = xy2d(x, y);\n\n        assert_eq!(d, d2);\n    }\n}\n"
  },
  {
    "path": "src/image.rs",
    "content": "#![allow(dead_code)]\n\nuse std::{\n    cell::{RefCell, UnsafeCell},\n    cmp,\n    fs::File,\n    io,\n    io::Write,\n    marker::PhantomData,\n    path::Path,\n    sync::Mutex,\n};\n\nuse half::f16;\n\nuse crate::color::{xyz_to_rec709_e, XYZ};\n\n#[derive(Debug)]\n#[allow(clippy::type_complexity)]\npub struct Image {\n    data: UnsafeCell<Vec<XYZ>>,\n    res: (usize, usize),\n    checked_out_blocks: Mutex<RefCell<Vec<((u32, u32), (u32, u32))>>>, // (min, max)\n}\n\nunsafe impl Sync for Image {}\n\nimpl Image {\n    pub fn new(width: usize, height: usize) -> Image {\n        Image {\n            data: UnsafeCell::new(vec![XYZ::new(0.0, 0.0, 0.0); width * height]),\n            res: (width, height),\n            checked_out_blocks: Mutex::new(RefCell::new(Vec::new())),\n        }\n    }\n\n    pub fn width(&self) -> usize {\n        self.res.0\n    }\n\n    pub fn height(&self) -> usize {\n        self.res.1\n    }\n\n    pub fn get(&mut self, x: usize, y: usize) -> XYZ {\n        assert!(x < self.res.0);\n        assert!(y < self.res.1);\n\n        let data: &Vec<XYZ> = unsafe { &*self.data.get() };\n        data[self.res.0 * y + x]\n    }\n\n    pub fn set(&mut self, x: usize, y: usize, value: XYZ) {\n        assert!(x < self.res.0);\n        assert!(y < self.res.1);\n\n        let data: &mut Vec<XYZ> = unsafe { &mut *self.data.get() };\n        data[self.res.0 * y + x] = value;\n    }\n\n    pub fn get_bucket<'a>(&'a self, min: (u32, u32), max: (u32, u32)) -> Bucket<'a> {\n        let tmp = self.checked_out_blocks.lock().unwrap();\n        let mut bucket_list = tmp.borrow_mut();\n\n        // Make sure this won't overlap with any already checked out buckets\n        for bucket in bucket_list.iter() {\n            // Calculate the intersection between the buckets\n            let inter_min = (cmp::max(min.0, (bucket.0).0), cmp::max(min.1, (bucket.0).1));\n            let inter_max = (cmp::min(max.0, (bucket.1).0), cmp::min(max.1, (bucket.1).1));\n\n            // If it's not degenerate and not zero-sized, there's overlap, so\n            // panic.\n            if inter_min.0 < inter_max.0 && inter_min.1 < inter_max.1 {\n                panic!(\"Attempted to check out a bucket with pixels that are already checked out.\");\n            }\n        }\n\n        // Clip bucket to image\n        let max = (\n            cmp::min(max.0, self.res.0 as u32),\n            cmp::min(max.1, self.res.1 as u32),\n        );\n\n        // Push bucket onto list\n        bucket_list.push((min, max));\n\n        Bucket {\n            min: min,\n            max: max,\n            // This cast to `*mut` is okay, because we have already dynamically\n            // ensured earlier in this function that the same memory locations\n            // aren't aliased.\n            img: self as *const Image as *mut Image,\n            _phantom: PhantomData,\n        }\n    }\n\n    pub fn write_ascii_ppm(&mut self, path: &Path) -> io::Result<()> {\n        // Open file.\n        let mut f = io::BufWriter::new(File::create(path)?);\n\n        // Write header\n        write!(f, \"P3\\n{} {}\\n255\\n\", self.res.0, self.res.1)?;\n\n        // Write pixels\n        for y in 0..self.res.1 {\n            for x in 0..self.res.0 {\n                let (r, g, b) = quantize_tri_255(xyz_to_srgbe(self.get(x, y).to_tuple()));\n                write!(f, \"{} {} {} \", r, g, b)?;\n            }\n            write!(f, \"\\n\")?;\n        }\n\n        // Done\n        Ok(())\n    }\n\n    pub fn write_binary_ppm(&mut self, path: &Path) -> io::Result<()> {\n        // Open file.\n        let mut f = io::BufWriter::new(File::create(path)?);\n\n        // Write header\n        write!(f, \"P6\\n{} {}\\n255\\n\", self.res.0, self.res.1)?;\n\n        // Write pixels\n        for y in 0..self.res.1 {\n            for x in 0..self.res.0 {\n                let (r, g, b) = quantize_tri_255(xyz_to_srgbe(self.get(x, y).to_tuple()));\n                let d = [r, g, b];\n                f.write_all(&d)?;\n            }\n        }\n\n        // Done\n        Ok(())\n    }\n\n    pub fn write_png(&mut self, path: &Path) -> io::Result<()> {\n        let mut image = Vec::new();\n\n        // Convert pixels\n        let res_x = self.res.0;\n        let res_y = self.res.1;\n        for y in 0..res_y {\n            for x in 0..res_x {\n                let (r, g, b) =\n                    quantize_tri_255(xyz_to_srgbe(self.get(x, res_y - 1 - y).to_tuple()));\n                image.push(r);\n                image.push(g);\n                image.push(b);\n                image.push(255);\n            }\n        }\n\n        // Write file\n        png_encode_mini::write_rgba_from_u8(\n            &mut File::create(path)?,\n            &image,\n            self.res.0 as u32,\n            self.res.1 as u32,\n        )?;\n\n        // Done\n        Ok(())\n    }\n\n    pub fn write_exr(&mut self, path: &Path) {\n        let mut image = Vec::new();\n\n        // Convert pixels\n        for y in 0..self.res.1 {\n            for x in 0..self.res.0 {\n                let (r, g, b) = xyz_to_rec709_e(self.get(x, y).to_tuple());\n                image.push((f16::from_f32(r), f16::from_f32(g), f16::from_f32(b)));\n            }\n        }\n\n        let mut file = io::BufWriter::new(File::create(path).unwrap());\n        let mut wr = openexr::ScanlineOutputFile::new(\n            &mut file,\n            openexr::Header::new()\n                .set_resolution(self.res.0 as u32, self.res.1 as u32)\n                .add_channel(\"R\", openexr::PixelType::HALF)\n                .add_channel(\"G\", openexr::PixelType::HALF)\n                .add_channel(\"B\", openexr::PixelType::HALF)\n                .set_compression(openexr::header::Compression::PIZ_COMPRESSION),\n        )\n        .unwrap();\n\n        wr.write_pixels(\n            openexr::FrameBuffer::new(self.res.0 as u32, self.res.1 as u32)\n                .insert_channels(&[\"R\", \"G\", \"B\"], &image),\n        )\n        .unwrap();\n    }\n}\n\n#[derive(Debug)]\npub struct Bucket<'a> {\n    min: (u32, u32),\n    max: (u32, u32),\n    img: *mut Image,\n    _phantom: PhantomData<&'a Image>,\n}\n\nimpl<'a> Bucket<'a> {\n    pub fn get(&mut self, x: u32, y: u32) -> XYZ {\n        assert!(x >= self.min.0 && x < self.max.0);\n        assert!(y >= self.min.1 && y < self.max.1);\n\n        let img: &mut Image = unsafe { &mut *self.img };\n        let data: &Vec<XYZ> = unsafe { &mut *img.data.get() };\n\n        data[img.res.0 * y as usize + x as usize]\n    }\n\n    pub fn set(&mut self, x: u32, y: u32, value: XYZ) {\n        assert!(x >= self.min.0 && x < self.max.0);\n        assert!(y >= self.min.1 && y < self.max.1);\n\n        let img: &mut Image = unsafe { &mut *self.img };\n        let data: &mut Vec<XYZ> = unsafe { &mut *img.data.get() };\n\n        data[img.res.0 * y as usize + x as usize] = value;\n    }\n\n    /// Returns the bucket's contents encoded in base64.\n    ///\n    /// `color_convert` lets you do a colorspace conversion before base64\n    /// encoding if desired.\n    ///\n    /// The data is laid out as four-floats-per-pixel in scanline order before\n    /// encoding to base64.  The fourth channel is alpha, and is set to 1.0 for\n    /// all pixels.\n    pub fn rgba_base64<F>(&mut self, color_convert: F) -> String\n    where\n        F: Fn((f32, f32, f32)) -> (f32, f32, f32),\n    {\n        use std::slice;\n        let mut data = Vec::with_capacity(\n            (4 * (self.max.0 - self.min.0) * (self.max.1 - self.min.1)) as usize,\n        );\n        for y in self.min.1..self.max.1 {\n            for x in self.min.0..self.max.0 {\n                let color = color_convert(self.get(x, y).to_tuple());\n                data.push(color.0);\n                data.push(color.1);\n                data.push(color.2);\n                data.push(1.0);\n            }\n        }\n        let data_u8 =\n            unsafe { slice::from_raw_parts(&data[0] as *const f32 as *const u8, data.len() * 4) };\n        base64::encode(data_u8)\n    }\n}\n\nimpl<'a> Drop for Bucket<'a> {\n    fn drop(&mut self) {\n        let img: &mut Image = unsafe { &mut *self.img };\n        let tmp = img.checked_out_blocks.lock().unwrap();\n        let mut bucket_list = tmp.borrow_mut();\n\n        // Find matching bucket and remove it\n        let i = bucket_list.iter().position(|bucket| {\n            (bucket.0).0 == self.min.0\n                && (bucket.0).1 == self.min.1\n                && (bucket.1).0 == self.max.0\n                && (bucket.1).1 == self.max.1\n        });\n        bucket_list.swap_remove(i.unwrap());\n    }\n}\n\nfn srgb_gamma(n: f32) -> f32 {\n    if n < 0.003_130_8 {\n        n * 12.92\n    } else {\n        (1.055 * n.powf(1.0 / 2.4)) - 0.055\n    }\n}\n\nfn srgb_inv_gamma(n: f32) -> f32 {\n    if n < 0.04045 {\n        n / 12.92\n    } else {\n        ((n + 0.055) / 1.055).powf(2.4)\n    }\n}\n\nfn xyz_to_srgbe(xyz: (f32, f32, f32)) -> (f32, f32, f32) {\n    let rgb = xyz_to_rec709_e(xyz);\n    (srgb_gamma(rgb.0), srgb_gamma(rgb.1), srgb_gamma(rgb.2))\n}\n\nfn quantize_tri_255(tri: (f32, f32, f32)) -> (u8, u8, u8) {\n    fn quantize(n: f32) -> u8 {\n        let n = 1.0f32.min(0.0f32.max(n)) * 255.0;\n        n as u8\n    }\n\n    (quantize(tri.0), quantize(tri.1), quantize(tri.2))\n}\n"
  },
  {
    "path": "src/lerp.rs",
    "content": "#![allow(dead_code)]\n\nuse math3d::{Normal, Point, Transform, Vector};\n\n/// Trait for allowing a type to be linearly interpolated.\npub trait Lerp: Copy {\n    fn lerp(self, other: Self, alpha: f32) -> Self;\n}\n\n/// Interpolates between two instances of a Lerp types.\npub fn lerp<T: Lerp>(a: T, b: T, alpha: f32) -> T {\n    debug_assert!(alpha >= 0.0);\n    debug_assert!(alpha <= 1.0);\n\n    a.lerp(b, alpha)\n}\n\n/// Interpolates a slice of data as if each adjecent pair of elements\n/// represent a linear segment.\npub fn lerp_slice<T: Lerp>(s: &[T], alpha: f32) -> T {\n    debug_assert!(!s.is_empty());\n    debug_assert!(alpha >= 0.0);\n    debug_assert!(alpha <= 1.0);\n\n    if s.len() == 1 || alpha == 1.0 {\n        *s.last().unwrap()\n    } else {\n        let tmp = alpha * ((s.len() - 1) as f32);\n        let i1 = tmp as usize;\n        let i2 = i1 + 1;\n        let alpha2 = tmp - (i1 as f32);\n\n        lerp(s[i1], s[i2], alpha2)\n    }\n}\n\npub fn lerp_slice_with<T, F>(s: &[T], alpha: f32, f: F) -> T\nwhere\n    T: Copy,\n    F: Fn(T, T, f32) -> T,\n{\n    debug_assert!(!s.is_empty());\n    debug_assert!(alpha >= 0.0);\n    debug_assert!(alpha <= 1.0);\n\n    if s.len() == 1 || alpha == 1.0 {\n        *s.last().unwrap()\n    } else {\n        let tmp = alpha * ((s.len() - 1) as f32);\n        let i1 = tmp as usize;\n        let i2 = i1 + 1;\n        let alpha2 = tmp - (i1 as f32);\n\n        f(s[i1], s[i2], alpha2)\n    }\n}\n\nimpl Lerp for f32 {\n    fn lerp(self, other: f32, alpha: f32) -> f32 {\n        (self * (1.0 - alpha)) + (other * alpha)\n    }\n}\n\nimpl Lerp for f64 {\n    fn lerp(self, other: f64, alpha: f32) -> f64 {\n        (self * (1.0 - alpha as f64)) + (other * alpha as f64)\n    }\n}\n\nimpl<T: Lerp> Lerp for (T, T) {\n    fn lerp(self, other: (T, T), alpha: f32) -> (T, T) {\n        (self.0.lerp(other.0, alpha), self.1.lerp(other.1, alpha))\n    }\n}\n\nimpl<T: Lerp> Lerp for [T; 2] {\n    fn lerp(self, other: Self, alpha: f32) -> Self {\n        [self[0].lerp(other[0], alpha), self[1].lerp(other[1], alpha)]\n    }\n}\n\nimpl<T: Lerp> Lerp for [T; 3] {\n    fn lerp(self, other: Self, alpha: f32) -> Self {\n        [\n            self[0].lerp(other[0], alpha),\n            self[1].lerp(other[1], alpha),\n            self[2].lerp(other[2], alpha),\n        ]\n    }\n}\n\nimpl<T: Lerp> Lerp for [T; 4] {\n    fn lerp(self, other: Self, alpha: f32) -> Self {\n        [\n            self[0].lerp(other[0], alpha),\n            self[1].lerp(other[1], alpha),\n            self[2].lerp(other[2], alpha),\n            self[3].lerp(other[3], alpha),\n        ]\n    }\n}\n\nimpl Lerp for glam::Vec4 {\n    fn lerp(self, other: glam::Vec4, alpha: f32) -> glam::Vec4 {\n        (self * (1.0 - alpha)) + (other * alpha)\n    }\n}\n\nimpl Lerp for Transform {\n    fn lerp(self, other: Transform, alpha: f32) -> Transform {\n        (self * (1.0 - alpha)) + (other * alpha)\n    }\n}\n\nimpl Lerp for Normal {\n    fn lerp(self, other: Normal, alpha: f32) -> Normal {\n        (self * (1.0 - alpha)) + (other * alpha)\n    }\n}\n\nimpl Lerp for Point {\n    fn lerp(self, other: Point, alpha: f32) -> Point {\n        let s = self;\n        let o = other;\n        Point {\n            co: (s.co * (1.0 - alpha)) + (o.co * alpha),\n        }\n    }\n}\n\nimpl Lerp for Vector {\n    fn lerp(self, other: Vector, alpha: f32) -> Vector {\n        (self * (1.0 - alpha)) + (other * alpha)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn lerp1() {\n        let a = 1.0f32;\n        let b = 2.0f32;\n        let alpha = 0.0f32;\n\n        assert_eq!(1.0, lerp(a, b, alpha));\n    }\n\n    #[test]\n    fn lerp2() {\n        let a = 1.0f32;\n        let b = 2.0f32;\n        let alpha = 1.0f32;\n\n        assert_eq!(2.0, lerp(a, b, alpha));\n    }\n\n    #[test]\n    fn lerp3() {\n        let a = 1.0f32;\n        let b = 2.0f32;\n        let alpha = 0.5f32;\n\n        assert_eq!(1.5, lerp(a, b, alpha));\n    }\n\n    #[test]\n    fn lerp_slice1() {\n        let s = [0.0f32, 1.0, 2.0, 3.0, 4.0];\n        let alpha = 0.0f32;\n\n        assert_eq!(0.0, lerp_slice(&s[..], alpha));\n    }\n\n    #[test]\n    fn lerp_slice2() {\n        let s = [0.0f32, 1.0, 2.0, 3.0, 4.0];\n        let alpha = 1.0f32;\n\n        assert_eq!(4.0, lerp_slice(&s[..], alpha));\n    }\n\n    #[test]\n    fn lerp_slice3() {\n        let s = [0.0f32, 1.0, 2.0, 3.0, 4.0];\n        let alpha = 0.5f32;\n\n        assert_eq!(2.0, lerp_slice(&s[..], alpha));\n    }\n\n    #[test]\n    fn lerp_slice4() {\n        let s = [0.0f32, 1.0, 2.0, 3.0, 4.0];\n        let alpha = 0.25f32;\n\n        assert_eq!(1.0, lerp_slice(&s[..], alpha));\n    }\n\n    #[test]\n    fn lerp_slice5() {\n        let s = [0.0f32, 1.0, 2.0, 3.0, 4.0];\n        let alpha = 0.75f32;\n\n        assert_eq!(3.0, lerp_slice(&s[..], alpha));\n    }\n\n    #[test]\n    fn lerp_slice6() {\n        let s = [0.0f32, 1.0, 2.0, 3.0, 4.0];\n        let alpha = 0.625f32;\n\n        assert_eq!(2.5, lerp_slice(&s[..], alpha));\n    }\n\n    #[test]\n    fn lerp_matrix() {\n        let a = Transform::new_from_values(\n            0.0, 2.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0,\n        );\n        let b = Transform::new_from_values(\n            -1.0, 1.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,\n        );\n\n        let c1 = Transform::new_from_values(\n            -0.25, 1.75, 2.25, 3.25, 4.25, 5.25, 6.25, 7.25, 8.25, 9.25, 10.25, 11.25,\n        );\n        let c2 = Transform::new_from_values(\n            -0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5, 11.5,\n        );\n        let c3 = Transform::new_from_values(\n            -0.75, 1.25, 2.75, 3.75, 4.75, 5.75, 6.75, 7.75, 8.75, 9.75, 10.75, 11.75,\n        );\n\n        assert_eq!(a.lerp(b, 0.0), a);\n        assert_eq!(a.lerp(b, 0.25), c1);\n        assert_eq!(a.lerp(b, 0.5), c2);\n        assert_eq!(a.lerp(b, 0.75), c3);\n        assert_eq!(a.lerp(b, 1.0), b);\n    }\n\n    #[test]\n    fn lerp_point_1() {\n        let p1 = Point::new(1.0, 2.0, 1.0);\n        let p2 = Point::new(-2.0, 1.0, -1.0);\n        let p3 = Point::new(1.0, 2.0, 1.0);\n\n        assert_eq!(p3, p1.lerp(p2, 0.0));\n    }\n\n    #[test]\n    fn lerp_point_2() {\n        let p1 = Point::new(1.0, 2.0, 1.0);\n        let p2 = Point::new(-2.0, 1.0, -1.0);\n        let p3 = Point::new(-2.0, 1.0, -1.0);\n\n        assert_eq!(p3, p1.lerp(p2, 1.0));\n    }\n\n    #[test]\n    fn lerp_point_3() {\n        let p1 = Point::new(1.0, 2.0, 1.0);\n        let p2 = Point::new(-2.0, 1.0, -1.0);\n        let p3 = Point::new(-0.5, 1.5, 0.0);\n\n        assert_eq!(p3, p1.lerp(p2, 0.5));\n    }\n\n    #[test]\n    fn lerp_normal_1() {\n        let n1 = Normal::new(1.0, 2.0, 1.0);\n        let n2 = Normal::new(-2.0, 1.0, -1.0);\n        let n3 = Normal::new(1.0, 2.0, 1.0);\n\n        assert_eq!(n3, n1.lerp(n2, 0.0));\n    }\n\n    #[test]\n    fn lerp_normal_2() {\n        let n1 = Normal::new(1.0, 2.0, 1.0);\n        let n2 = Normal::new(-2.0, 1.0, -1.0);\n        let n3 = Normal::new(-2.0, 1.0, -1.0);\n\n        assert_eq!(n3, n1.lerp(n2, 1.0));\n    }\n\n    #[test]\n    fn lerp_normal_3() {\n        let n1 = Normal::new(1.0, 2.0, 1.0);\n        let n2 = Normal::new(-2.0, 1.0, -1.0);\n        let n3 = Normal::new(-0.5, 1.5, 0.0);\n\n        assert_eq!(n3, n1.lerp(n2, 0.5));\n    }\n\n    #[test]\n    fn lerp_vector_1() {\n        let v1 = Vector::new(1.0, 2.0, 1.0);\n        let v2 = Vector::new(-2.0, 1.0, -1.0);\n        let v3 = Vector::new(1.0, 2.0, 1.0);\n\n        assert_eq!(v3, v1.lerp(v2, 0.0));\n    }\n\n    #[test]\n    fn lerp_vector_2() {\n        let v1 = Vector::new(1.0, 2.0, 1.0);\n        let v2 = Vector::new(-2.0, 1.0, -1.0);\n        let v3 = Vector::new(-2.0, 1.0, -1.0);\n\n        assert_eq!(v3, v1.lerp(v2, 1.0));\n    }\n\n    #[test]\n    fn lerp_vector_3() {\n        let v1 = Vector::new(1.0, 2.0, 1.0);\n        let v2 = Vector::new(-2.0, 1.0, -1.0);\n        let v3 = Vector::new(-0.5, 1.5, 0.0);\n\n        assert_eq!(v3, v1.lerp(v2, 0.5));\n    }\n}\n"
  },
  {
    "path": "src/light/distant_disk_light.rs",
    "content": "use std::f64::consts::PI as PI_64;\n\nuse kioku::Arena;\n\nuse crate::{\n    color::{Color, SpectralSample},\n    lerp::lerp_slice,\n    math::{coordinate_system_from_vector, Vector},\n    sampling::{uniform_sample_cone, uniform_sample_cone_pdf},\n};\n\nuse super::WorldLightSource;\n\n// TODO: handle case where radius = 0.0.\n\n#[derive(Copy, Clone, Debug)]\npub struct DistantDiskLight<'a> {\n    radii: &'a [f32],\n    directions: &'a [Vector],\n    colors: &'a [Color],\n}\n\nimpl<'a> DistantDiskLight<'a> {\n    pub fn new(\n        arena: &'a Arena,\n        radii: &[f32],\n        directions: &[Vector],\n        colors: &[Color],\n    ) -> DistantDiskLight<'a> {\n        DistantDiskLight {\n            radii: arena.copy_slice(&radii),\n            directions: arena.copy_slice(&directions),\n            colors: arena.copy_slice(&colors),\n        }\n    }\n\n    // fn sample_pdf(&self, sample_dir: Vector, wavelength: f32, time: f32) -> f32 {\n    //     // We're not using these, silence warnings\n    //     let _ = (sample_dir, wavelength);\n\n    //     let radius: f64 = lerp_slice(self.radii, time) as f64;\n\n    //     uniform_sample_cone_pdf(radius.cos()) as f32\n    // }\n\n    // fn outgoing(&self, dir: Vector, wavelength: f32, time: f32) -> SpectralSample {\n    //     // We're not using this, silence warning\n    //     let _ = dir;\n\n    //     let radius = lerp_slice(self.radii, time) as f64;\n    //     let col = lerp_slice(self.colors, time);\n    //     let solid_angle = 2.0 * PI_64 * (1.0 - radius.cos());\n\n    //     (col / solid_angle as f32).to_spectral_sample(wavelength)\n    // }\n}\n\nimpl<'a> WorldLightSource for DistantDiskLight<'a> {\n    fn sample_from_point(\n        &self,\n        u: f32,\n        v: f32,\n        wavelength: f32,\n        time: f32,\n    ) -> (SpectralSample, Vector, f32) {\n        // Calculate time interpolated values\n        let radius: f64 = lerp_slice(self.radii, time) as f64;\n        let direction = lerp_slice(self.directions, time);\n        let col = lerp_slice(self.colors, time);\n        let solid_angle_inv = 1.0 / (2.0 * PI_64 * (1.0 - radius.cos()));\n\n        // Create a coordinate system from the vector pointing at the center of\n        // of the light.\n        let (z, x, y) = coordinate_system_from_vector(-direction.normalized());\n\n        // Sample the cone subtended by the light.\n        let cos_theta_max: f64 = radius.cos();\n        let sample = uniform_sample_cone(u, v, cos_theta_max).normalized();\n\n        // Calculate the final values and return everything.\n        let spectral_sample = col.to_spectral_sample(wavelength) * solid_angle_inv as f32;\n        let shadow_vec = (x * sample.x()) + (y * sample.y()) + (z * sample.z());\n        let pdf = uniform_sample_cone_pdf(cos_theta_max);\n        (spectral_sample, shadow_vec, pdf as f32)\n    }\n\n    fn is_delta(&self) -> bool {\n        false\n    }\n\n    fn approximate_energy(&self) -> f32 {\n        self.colors\n            .iter()\n            .fold(0.0, |a, &b| a + b.approximate_energy())\n            / self.colors.len() as f32\n    }\n}\n"
  },
  {
    "path": "src/light/mod.rs",
    "content": "mod distant_disk_light;\nmod rectangle_light;\nmod sphere_light;\n\nuse std::fmt::Debug;\n\nuse crate::{\n    color::SpectralSample,\n    math::{Normal, Point, Transform, Vector},\n    surface::Surface,\n};\n\npub use self::{\n    distant_disk_light::DistantDiskLight, rectangle_light::RectangleLight,\n    sphere_light::SphereLight,\n};\n\n/// A finite light source that can be bounded in space.\npub trait SurfaceLight: Surface {\n    /// Samples the surface given a point to be illuminated.\n    ///\n    /// - `space`: The world-to-object space transform of the light.\n    /// - `arr`: The point to be illuminated (in world space).\n    /// - `u`: Random parameter U.\n    /// - `v`: Random parameter V.\n    /// - `wavelength`: The wavelength of light to sample at.\n    /// - `time`: The time to sample at.\n    ///\n    /// Returns:\n    /// - The light arriving at the point arr.\n    /// - A tuple with the sample point on the light, the surface normal at\n    ///   that point, and the point's error magnitude.  These are used\n    ///   elsewhere to create a robust shadow ray.\n    /// - The pdf of the sample.\n    fn sample_from_point(\n        &self,\n        space: &Transform,\n        arr: Point,\n        u: f32,\n        v: f32,\n        wavelength: f32,\n        time: f32,\n    ) -> (SpectralSample, (Point, Normal, f32), f32);\n\n    /// Returns whether the light has a delta distribution.\n    ///\n    /// If a light has no chance of a ray hitting it through random process\n    /// then it is a delta light source.  For example, point light sources,\n    /// lights that only emit in a single direction, etc.\n    fn is_delta(&self) -> bool;\n\n    /// Returns an approximation of the total energy emitted by the surface.\n    ///\n    /// Note: this does not need to be exact, but it does need to be non-zero\n    /// for any surface that does emit light.  This is used for importance\n    /// sampling.\n    fn approximate_energy(&self) -> f32;\n}\n\n/// An infinite light source that cannot be bounded in space.  E.g.\n/// a sun light source.\npub trait WorldLightSource: Debug + Sync {\n    /// Samples the light source for a given point to be illuminated.\n    ///\n    ///     - u: Random parameter U.\n    ///     - v: Random parameter V.\n    ///     - wavelength: The wavelength of light to sample at.\n    ///     - time: The time to sample at.\n    ///\n    /// Returns: The light arriving from the shadow-testing direction, the\n    /// vector to use for shadow testing, and the pdf of the sample.\n    fn sample_from_point(\n        &self,\n        u: f32,\n        v: f32,\n        wavelength: f32,\n        time: f32,\n    ) -> (SpectralSample, Vector, f32);\n\n    /// Returns whether the light has a delta distribution.\n    ///\n    /// If a light has no chance of a ray hitting it through random process\n    /// then it is a delta light source.  For example, point light sources,\n    /// lights that only emit in a single direction, etc.\n    fn is_delta(&self) -> bool;\n\n    /// Returns an approximation of the total energy emitted by the light\n    /// source.\n    ///\n    /// Note: this does not need to be exact, but it does need to be non-zero\n    /// for any light that emits any light.  This is used for importance\n    /// sampling.\n    fn approximate_energy(&self) -> f32;\n}\n"
  },
  {
    "path": "src/light/rectangle_light.rs",
    "content": "use kioku::Arena;\n\nuse crate::{\n    bbox::BBox,\n    boundable::Boundable,\n    color::{Color, SpectralSample},\n    lerp::lerp_slice,\n    math::{cross, dot, Normal, Point, Transform, Vector},\n    ray::{RayBatch, RayStack},\n    sampling::{\n        spherical_triangle_solid_angle, triangle_surface_area, uniform_sample_spherical_triangle,\n        uniform_sample_triangle,\n    },\n    shading::surface_closure::SurfaceClosure,\n    shading::SurfaceShader,\n    surface::{triangle, Surface, SurfaceIntersection, SurfaceIntersectionData},\n};\n\nuse super::SurfaceLight;\n\nconst SIMPLE_SAMPLING_THRESHOLD: f32 = 0.01;\n\n#[derive(Copy, Clone, Debug)]\npub struct RectangleLight<'a> {\n    dimensions: &'a [(f32, f32)],\n    colors: &'a [Color],\n    bounds_: &'a [BBox],\n}\n\nimpl<'a> RectangleLight<'a> {\n    pub fn new<'b>(\n        arena: &'b Arena,\n        dimensions: &[(f32, f32)],\n        colors: &[Color],\n    ) -> RectangleLight<'b> {\n        let bbs: Vec<_> = dimensions\n            .iter()\n            .map(|d| BBox {\n                min: Point::new(d.0 * -0.5, d.1 * -0.5, 0.0),\n                max: Point::new(d.0 * 0.5, d.1 * 0.5, 0.0),\n            })\n            .collect();\n        RectangleLight {\n            dimensions: arena.copy_slice(&dimensions),\n            colors: arena.copy_slice(&colors),\n            bounds_: arena.copy_slice(&bbs),\n        }\n    }\n\n    // TODO: this is only used from within `intersect_rays`, and could be done\n    // more efficiently by inlining it there.\n    fn sample_pdf(\n        &self,\n        space: &Transform,\n        arr: Point,\n        sample_dir: Vector,\n        hit_point: Point,\n        wavelength: f32,\n        time: f32,\n    ) -> f32 {\n        // We're not using these, silence warnings\n        let _ = wavelength;\n\n        let dim = lerp_slice(self.dimensions, time);\n\n        // Get the four corners of the rectangle, transformed into world space\n        let space_inv = space.inverse();\n        let p1 = Point::new(dim.0 * 0.5, dim.1 * 0.5, 0.0) * space_inv;\n        let p2 = Point::new(dim.0 * -0.5, dim.1 * 0.5, 0.0) * space_inv;\n        let p3 = Point::new(dim.0 * -0.5, dim.1 * -0.5, 0.0) * space_inv;\n        let p4 = Point::new(dim.0 * 0.5, dim.1 * -0.5, 0.0) * space_inv;\n\n        // Get the four corners of the rectangle, projected on to the unit\n        // sphere centered around arr.\n        let sp1 = (p1 - arr).normalized();\n        let sp2 = (p2 - arr).normalized();\n        let sp3 = (p3 - arr).normalized();\n        let sp4 = (p4 - arr).normalized();\n\n        // Get the solid angles of the rectangle split into two triangles\n        let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3);\n        let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3);\n\n        // World-space surface normal\n        let normal = Normal::new(0.0, 0.0, 1.0) * space_inv;\n\n        // PDF\n        if (area_1 + area_2) < SIMPLE_SAMPLING_THRESHOLD {\n            let area = triangle_surface_area(p2, p1, p3) + triangle_surface_area(p4, p1, p3);\n            (hit_point - arr).length2()\n                / dot(sample_dir.normalized(), normal.into_vector().normalized()).abs()\n                / area\n        } else {\n            1.0 / (area_1 + area_2)\n        }\n    }\n\n    // fn outgoing(\n    //     &self,\n    //     space: &Transform,\n    //     dir: Vector,\n    //     u: f32,\n    //     v: f32,\n    //     wavelength: f32,\n    //     time: f32,\n    // ) -> SpectralSample {\n    //     // We're not using these, silence warnings\n    //     let _ = (space, dir, u, v);\n\n    //     let dim = lerp_slice(self.dimensions, time);\n    //     let col = lerp_slice(self.colors, time);\n\n    //     // TODO: Is this right?  Do we need to get the surface area post-transform?\n    //     let surface_area_inv: f64 = 1.0 / (dim.0 as f64 * dim.1 as f64);\n\n    //     (col * surface_area_inv as f32 * 0.5).to_spectral_sample(wavelength)\n    // }\n}\n\nimpl<'a> SurfaceLight for RectangleLight<'a> {\n    fn sample_from_point(\n        &self,\n        space: &Transform,\n        arr: Point,\n        u: f32,\n        v: f32,\n        wavelength: f32,\n        time: f32,\n    ) -> (SpectralSample, (Point, Normal, f32), f32) {\n        // Calculate time interpolated values\n        let dim = lerp_slice(self.dimensions, time);\n        let col = lerp_slice(self.colors, time);\n\n        let surface_area: f64 = dim.0 as f64 * dim.1 as f64;\n        let surface_area_inv: f64 = 1.0 / surface_area;\n\n        // Get the four corners of the rectangle, transformed into world space\n        let space_inv = space.inverse();\n        let p1 = Point::new(dim.0 * 0.5, dim.1 * 0.5, 0.0) * space_inv;\n        let p2 = Point::new(dim.0 * -0.5, dim.1 * 0.5, 0.0) * space_inv;\n        let p3 = Point::new(dim.0 * -0.5, dim.1 * -0.5, 0.0) * space_inv;\n        let p4 = Point::new(dim.0 * 0.5, dim.1 * -0.5, 0.0) * space_inv;\n\n        // Get the four corners of the rectangle relative to arr.\n        let lp1 = p1 - arr;\n        let lp2 = p2 - arr;\n        let lp3 = p3 - arr;\n        let lp4 = p4 - arr;\n\n        // Four corners projected on to the unit sphere.\n        let sp1 = lp1.normalized();\n        let sp2 = lp2.normalized();\n        let sp3 = lp3.normalized();\n        let sp4 = lp4.normalized();\n\n        // Get the solid angles of the rectangle split into two triangles\n        let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3);\n        let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3);\n\n        // Calculate world-space surface normal\n        let normal = Normal::new(0.0, 0.0, 1.0) * space_inv;\n\n        if (area_1 + area_2) < SIMPLE_SAMPLING_THRESHOLD {\n            // Simple sampling for more distant lights\n            let surface_area_1 = triangle_surface_area(p2, p1, p3);\n            let surface_area_2 = triangle_surface_area(p4, p1, p3);\n            let sample_point = {\n                // Select which triangle to sample\n                let threshhold = surface_area_1 / (surface_area_1 + surface_area_2);\n                if u < threshhold {\n                    uniform_sample_triangle(\n                        p2.into_vector(),\n                        p1.into_vector(),\n                        p3.into_vector(),\n                        v,\n                        u / threshhold,\n                    )\n                } else {\n                    uniform_sample_triangle(\n                        p4.into_vector(),\n                        p1.into_vector(),\n                        p3.into_vector(),\n                        v,\n                        (u - threshhold) / (1.0 - threshhold),\n                    )\n                }\n            }\n            .into_point();\n            let shadow_vec = sample_point - arr;\n            let spectral_sample =\n                (col).to_spectral_sample(wavelength) * surface_area_inv as f32 * 0.5;\n            let pdf = (sample_point - arr).length2()\n                / dot(shadow_vec.normalized(), normal.into_vector().normalized()).abs()\n                / (surface_area_1 + surface_area_2);\n            let point_err = 0.0001; // TODO: this is a hack, do properly.\n            (spectral_sample, (sample_point, normal, point_err), pdf)\n        } else {\n            // Sophisticated sampling for close lights.\n\n            // Normalize the solid angles for selection purposes\n            let prob_1 = if area_1.is_infinite() {\n                1.0\n            } else if area_2.is_infinite() {\n                0.0\n            } else {\n                area_1 / (area_1 + area_2)\n            };\n            let prob_2 = 1.0 - prob_1;\n\n            // Select one of the triangles and sample it\n            let shadow_vec = if u < prob_1 {\n                uniform_sample_spherical_triangle(sp2, sp1, sp3, v, u / prob_1)\n            } else {\n                uniform_sample_spherical_triangle(sp4, sp1, sp3, v, 1.0 - ((u - prob_1) / prob_2))\n            };\n\n            // Project shadow_vec back onto the light's surface\n            let arr_local = arr * *space;\n            let shadow_vec_local = shadow_vec * *space;\n            let shadow_vec_local = shadow_vec_local * (-arr_local.z() / shadow_vec_local.z());\n            let mut sample_point_local = arr_local + shadow_vec_local;\n            {\n                let x = sample_point_local.x().max(dim.0 * -0.5).min(dim.0 * 0.5);\n                let y = sample_point_local.y().max(dim.1 * -0.5).min(dim.1 * 0.5);\n                sample_point_local.set_x(x);\n                sample_point_local.set_y(y);\n                sample_point_local.set_z(0.0);\n            }\n            let sample_point = sample_point_local * space_inv;\n            let point_err = 0.0001; // TODO: this is a hack, do properly.\n\n            // Calculate pdf and light energy\n            let pdf = 1.0 / (area_1 + area_2); // PDF of the ray direction being sampled\n            let spectral_sample =\n                col.to_spectral_sample(wavelength) * surface_area_inv as f32 * 0.5;\n\n            (\n                spectral_sample,\n                (sample_point, normal, point_err),\n                pdf as f32,\n            )\n        }\n    }\n\n    fn is_delta(&self) -> bool {\n        false\n    }\n\n    fn approximate_energy(&self) -> f32 {\n        self.colors\n            .iter()\n            .fold(0.0, |a, &b| a + b.approximate_energy())\n            / self.colors.len() as f32\n    }\n}\n\nimpl<'a> Surface for RectangleLight<'a> {\n    fn intersect_rays(\n        &self,\n        rays: &mut RayBatch,\n        ray_stack: &mut RayStack,\n        isects: &mut [SurfaceIntersection],\n        shader: &dyn SurfaceShader,\n        space: &[Transform],\n    ) {\n        let _ = shader; // Silence 'unused' warning\n\n        ray_stack.pop_do_next_task(|ray_idx| {\n            let time = rays.time(ray_idx);\n            let orig = rays.orig(ray_idx);\n            let dir = rays.dir(ray_idx);\n            let max_t = rays.max_t(ray_idx);\n\n            // Calculate time interpolated values\n            let dim = lerp_slice(self.dimensions, time);\n            let xform = lerp_slice(space, time);\n\n            let space_inv = xform.inverse();\n\n            // Get the four corners of the rectangle, transformed into world space\n            let p1 = Point::new(dim.0 * 0.5, dim.1 * 0.5, 0.0) * space_inv;\n            let p2 = Point::new(dim.0 * -0.5, dim.1 * 0.5, 0.0) * space_inv;\n            let p3 = Point::new(dim.0 * -0.5, dim.1 * -0.5, 0.0) * space_inv;\n            let p4 = Point::new(dim.0 * 0.5, dim.1 * -0.5, 0.0) * space_inv;\n\n            // Test against two triangles that make up the light\n            let ray_pre = triangle::RayTriPrecompute::new(dir);\n            for tri in &[(p1, p2, p3), (p3, p4, p1)] {\n                if let Some((t, b0, b1, b2)) = triangle::intersect_ray(orig, ray_pre, max_t, *tri) {\n                    if t < max_t {\n                        if rays.is_occlusion(ray_idx) {\n                            isects[ray_idx] = SurfaceIntersection::Occlude;\n                            rays.mark_done(ray_idx);\n                        } else {\n                            let (pos, pos_err) = triangle::surface_point(*tri, (b0, b1, b2));\n                            let normal = cross(tri.0 - tri.1, tri.0 - tri.2).into_normal();\n\n                            let intersection_data = SurfaceIntersectionData {\n                                incoming: dir,\n                                t: t,\n                                pos: pos,\n                                pos_err: pos_err,\n                                nor: normal,\n                                nor_g: normal,\n                                local_space: xform,\n                                sample_pdf: self.sample_pdf(\n                                    &xform,\n                                    orig,\n                                    dir,\n                                    pos,\n                                    rays.wavelength(ray_idx),\n                                    time,\n                                ),\n                            };\n\n                            let closure = {\n                                let inv_surface_area = (1.0 / (dim.0 as f64 * dim.1 as f64)) as f32;\n                                let color = lerp_slice(self.colors, time) * inv_surface_area;\n                                SurfaceClosure::Emit(color)\n                            };\n\n                            // Fill in intersection\n                            isects[ray_idx] = SurfaceIntersection::Hit {\n                                intersection_data: intersection_data,\n                                closure: closure,\n                            };\n\n                            // Set ray's max t\n                            rays.set_max_t(ray_idx, t);\n                        }\n\n                        break;\n                    }\n                }\n            }\n        });\n    }\n}\n\nimpl<'a> Boundable for RectangleLight<'a> {\n    fn bounds(&self) -> &[BBox] {\n        self.bounds_\n    }\n}\n"
  },
  {
    "path": "src/light/sphere_light.rs",
    "content": "use std::f64::consts::PI as PI_64;\n\nuse kioku::Arena;\n\nuse crate::{\n    bbox::BBox,\n    boundable::Boundable,\n    color::{Color, SpectralSample},\n    lerp::lerp_slice,\n    math::{coordinate_system_from_vector, dot, Normal, Point, Transform, Vector},\n    ray::{RayBatch, RayStack},\n    sampling::{uniform_sample_cone, uniform_sample_cone_pdf, uniform_sample_sphere},\n    shading::surface_closure::SurfaceClosure,\n    shading::SurfaceShader,\n    surface::{Surface, SurfaceIntersection, SurfaceIntersectionData},\n};\n\nuse super::SurfaceLight;\n\n// TODO: use proper error bounds for sample generation to avoid self-shadowing\n// instead of these fudge factors.\nconst SAMPLE_POINT_FUDGE: f32 = 0.001;\n\n// TODO: handle case where radius = 0.0.\n\n#[derive(Copy, Clone, Debug)]\npub struct SphereLight<'a> {\n    radii: &'a [f32],\n    colors: &'a [Color],\n    bounds_: &'a [BBox],\n}\n\nimpl<'a> SphereLight<'a> {\n    pub fn new<'b>(arena: &'b Arena, radii: &[f32], colors: &[Color]) -> SphereLight<'b> {\n        let bbs: Vec<_> = radii\n            .iter()\n            .map(|r| BBox {\n                min: Point::new(-*r, -*r, -*r),\n                max: Point::new(*r, *r, *r),\n            })\n            .collect();\n        SphereLight {\n            radii: arena.copy_slice(&radii),\n            colors: arena.copy_slice(&colors),\n            bounds_: arena.copy_slice(&bbs),\n        }\n    }\n\n    // TODO: this is only used from within `intersect_rays`, and could be done\n    // more efficiently by inlining it there.\n    fn sample_pdf(\n        &self,\n        space: &Transform,\n        arr: Point,\n        sample_dir: Vector,\n        sample_u: f32,\n        sample_v: f32,\n        wavelength: f32,\n        time: f32,\n    ) -> f32 {\n        // We're not using these, silence warnings\n        let _ = (sample_dir, sample_u, sample_v, wavelength);\n\n        let arr = arr * *space;\n        let pos = Point::new(0.0, 0.0, 0.0);\n        let radius: f64 = lerp_slice(self.radii, time) as f64;\n\n        let d2: f64 = (pos - arr).length2() as f64; // Distance from center of sphere squared\n        let d: f64 = d2.sqrt(); // Distance from center of sphere\n\n        if d > radius {\n            // Calculate the portion of the sphere visible from the point\n            let sin_theta_max2: f64 = ((radius * radius) / d2).min(1.0);\n            let cos_theta_max2: f64 = 1.0 - sin_theta_max2;\n            let cos_theta_max: f64 = cos_theta_max2.sqrt();\n\n            uniform_sample_cone_pdf(cos_theta_max) as f32\n        } else {\n            (1.0 / (4.0 * PI_64)) as f32\n        }\n    }\n}\n\nimpl<'a> SurfaceLight for SphereLight<'a> {\n    fn sample_from_point(\n        &self,\n        space: &Transform,\n        arr: Point,\n        u: f32,\n        v: f32,\n        wavelength: f32,\n        time: f32,\n    ) -> (SpectralSample, (Point, Normal, f32), f32) {\n        // TODO: track fp error due to transforms\n        let arr = arr * *space;\n        let pos = Point::new(0.0, 0.0, 0.0);\n\n        // Precalculate local->world space transform matrix\n        let inv_space = space.inverse();\n\n        // Calculate time interpolated values\n        let radius: f64 = lerp_slice(self.radii, time) as f64;\n        let col = lerp_slice(self.colors, time);\n        let surface_area_inv: f64 = 1.0 / (4.0 * PI_64 * radius * radius);\n\n        // Create a coordinate system from the vector between the\n        // point and the center of the light\n        let z = pos - arr;\n        let d2: f64 = z.length2() as f64; // Distance from center of sphere squared\n        let d = d2.sqrt(); // Distance from center of sphere\n        let (z, x, y) = coordinate_system_from_vector(z);\n        let (x, y, z) = (x.normalized(), y.normalized(), z.normalized());\n\n        // Pre-calculate sample point error magnitude.\n        // TODO: do this properly.  This is a total hack.\n        let sample_point_err = {\n            let v = Vector::new(radius as f32, radius as f32, radius as f32);\n            let v2 = v * inv_space;\n            v2.length() * SAMPLE_POINT_FUDGE\n        };\n\n        // If we're outside the sphere, sample the surface based on\n        // the angle it subtends from the point being lit.\n        if d > radius {\n            // Calculate the portion of the sphere visible from the point\n            let sin_theta_max2: f64 = ((radius * radius) / d2).min(1.0);\n            let cos_theta_max2: f64 = 1.0 - sin_theta_max2;\n            let sin_theta_max: f64 = sin_theta_max2.sqrt();\n            let cos_theta_max: f64 = cos_theta_max2.sqrt();\n\n            // Sample the cone subtended by the sphere and calculate\n            // useful data from that.\n            let sample = uniform_sample_cone(u, v, cos_theta_max).normalized();\n            let cos_theta: f64 = sample.z() as f64;\n            let cos_theta2: f64 = cos_theta * cos_theta;\n            let sin_theta2: f64 = (1.0 - cos_theta2).max(0.0);\n            let sin_theta: f64 = sin_theta2.sqrt();\n\n            // Convert to a point on the sphere.\n            // The technique for this is from \"Akalin\" on ompf2.com:\n            // http://ompf2.com/viewtopic.php?f=3&t=1914#p4414\n            let dd = 1.0 - (d2 * sin_theta * sin_theta / (radius * radius));\n            let cos_a = if dd <= 0.0 {\n                sin_theta_max\n            } else {\n                ((d / radius) * sin_theta2) + (cos_theta * dd.sqrt())\n            };\n            let sin_a = ((1.0 - (cos_a * cos_a)).max(0.0)).sqrt();\n            let phi = v as f64 * 2.0 * PI_64;\n            let sample = Vector::new(\n                (phi.cos() * sin_a * radius) as f32,\n                (phi.sin() * sin_a * radius) as f32,\n                (d - (cos_a * radius)) as f32,\n            );\n\n            // Calculate the final values and return everything.\n            let (sample_point, normal) = {\n                let sample_vec = (x * sample.x()) + (y * sample.y()) + (z * sample.z());\n                let normal = (arr + sample_vec).into_vector().normalized();\n                let point = normal * radius as f32;\n                (\n                    point.into_point() * inv_space,\n                    normal.into_normal() * inv_space,\n                )\n            };\n            let pdf = uniform_sample_cone_pdf(cos_theta_max);\n            let spectral_sample = col.to_spectral_sample(wavelength) * surface_area_inv as f32;\n            return (\n                spectral_sample,\n                (sample_point, normal, sample_point_err),\n                pdf as f32,\n            );\n        } else {\n            // If we're inside the sphere, there's light from every direction.\n            let (sample_point, normal) = {\n                let sample_vec = uniform_sample_sphere(u, v);\n                let normal = (arr + sample_vec).into_vector().normalized();\n                let point = normal * radius as f32;\n                (\n                    point.into_point() * inv_space,\n                    normal.into_normal() * inv_space,\n                )\n            };\n            let pdf = 1.0 / (4.0 * PI_64);\n            let spectral_sample = col.to_spectral_sample(wavelength) * surface_area_inv as f32;\n            return (\n                spectral_sample,\n                (sample_point, normal, sample_point_err),\n                pdf as f32,\n            );\n        }\n    }\n\n    fn is_delta(&self) -> bool {\n        false\n    }\n\n    fn approximate_energy(&self) -> f32 {\n        self.colors\n            .iter()\n            .fold(0.0, |a, &b| a + b.approximate_energy())\n            / self.colors.len() as f32\n    }\n}\n\nimpl<'a> Surface for SphereLight<'a> {\n    fn intersect_rays(\n        &self,\n        rays: &mut RayBatch,\n        ray_stack: &mut RayStack,\n        isects: &mut [SurfaceIntersection],\n        shader: &dyn SurfaceShader,\n        space: &[Transform],\n    ) {\n        let _ = shader; // Silence 'unused' warning\n\n        ray_stack.pop_do_next_task(|ray_idx| {\n            let time = rays.time(ray_idx);\n\n            // Get the transform space\n            let xform = lerp_slice(space, time);\n\n            // Get the radius of the sphere at the ray's time\n            let radius = lerp_slice(self.radii, time); // Radius of the sphere\n\n            // Get the ray origin and direction in local space\n            let orig = rays.orig_local(ray_idx).into_vector();\n            let dir = rays.dir(ray_idx) * xform;\n\n            // Code adapted to Rust from https://github.com/Tecla/Rayito\n            // Ray-sphere intersection can result in either zero, one or two points\n            // of intersection.  It turns into a quadratic equation, so we just find\n            // the solution using the quadratic formula.  Note that there is a\n            // slightly more stable form of it when computing it on a computer, and\n            // we use that method to keep everything accurate.\n\n            // Calculate quadratic coeffs\n            let a = dir.length2();\n            let b = 2.0 * dot(dir, orig);\n            let c = orig.length2() - (radius * radius);\n\n            let discriminant = (b * b) - (4.0 * a * c);\n            if discriminant < 0.0 {\n                // Discriminant less than zero?  No solution => no intersection.\n                return;\n            }\n            let discriminant = discriminant.sqrt();\n\n            // Compute a more stable form of our param t (t0 = q/a, t1 = c/q)\n            // q = -0.5 * (b - sqrt(b * b - 4.0 * a * c)) if b < 0, or\n            // q = -0.5 * (b + sqrt(b * b - 4.0 * a * c)) if b >= 0\n            let q = if b < 0.0 {\n                -0.5 * (b - discriminant)\n            } else {\n                -0.5 * (b + discriminant)\n            };\n\n            // Get our final parametric values\n            let mut t0 = q / a;\n            let mut t1 = if q != 0.0 { c / q } else { rays.max_t(ray_idx) };\n\n            // Swap them so they are ordered right\n            if t0 > t1 {\n                use std::mem::swap;\n                swap(&mut t0, &mut t1);\n            }\n\n            // Check our intersection for validity against this ray's extents\n            if t0 > rays.max_t(ray_idx) || t1 <= 0.0 {\n                // Didn't hit because sphere is entirely outside of ray's extents\n                return;\n            }\n\n            let t = if t0 > 0.0 {\n                t0\n            } else if t1 <= rays.max_t(ray_idx) {\n                t1\n            } else {\n                // Didn't hit because ray is entirely within the sphere, and\n                // therefore doesn't hit its surface.\n                return;\n            };\n\n            // We hit the sphere, so calculate intersection info.\n            if rays.is_occlusion(ray_idx) {\n                isects[ray_idx] = SurfaceIntersection::Occlude;\n                rays.mark_done(ray_idx);\n            } else {\n                let inv_xform = xform.inverse();\n\n                // Position is calculated from the local-space ray and t, and then\n                // re-projected onto the surface of the sphere.\n                let t_pos = orig + (dir * t);\n                let unit_pos = t_pos.normalized();\n                let pos = (unit_pos * radius * inv_xform).into_point();\n\n                // TODO: proper error bounds.\n                let pos_err = 0.001;\n\n                let normal = unit_pos.into_normal() * inv_xform;\n\n                let intersection_data = SurfaceIntersectionData {\n                    incoming: rays.dir(ray_idx),\n                    t: t,\n                    pos: pos,\n                    pos_err: pos_err,\n                    nor: normal,\n                    nor_g: normal,\n                    local_space: xform,\n                    sample_pdf: self.sample_pdf(\n                        &xform,\n                        rays.orig(ray_idx),\n                        rays.dir(ray_idx),\n                        0.0,\n                        0.0,\n                        rays.wavelength(ray_idx),\n                        time,\n                    ),\n                };\n\n                let closure = {\n                    let inv_surface_area =\n                        (1.0 / (4.0 * PI_64 * radius as f64 * radius as f64)) as f32;\n                    let color = lerp_slice(self.colors, time) * inv_surface_area;\n                    SurfaceClosure::Emit(color)\n                };\n\n                // Fill in intersection\n                isects[ray_idx] = SurfaceIntersection::Hit {\n                    intersection_data: intersection_data,\n                    closure: closure,\n                };\n\n                // Set ray's max t\n                rays.set_max_t(ray_idx, t);\n            }\n        });\n    }\n}\n\nimpl<'a> Boundable for SphereLight<'a> {\n    fn bounds(&self) -> &[BBox] {\n        self.bounds_\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "#![allow(clippy::float_cmp)]\n#![allow(clippy::inline_always)]\n#![allow(clippy::many_single_char_names)]\n#![allow(clippy::needless_lifetimes)]\n#![allow(clippy::needless_return)]\n#![allow(clippy::or_fun_call)]\n#![allow(clippy::too_many_arguments)]\n#![allow(clippy::redundant_field_names)]\n#![allow(clippy::enum_variant_names)]\n#![allow(clippy::cast_lossless)]\n#![allow(clippy::needless_range_loop)]\n#![allow(clippy::excessive_precision)]\n#![allow(clippy::transmute_ptr_to_ptr)]\n\nextern crate lazy_static;\n\nmod accel;\nmod algorithm;\nmod bbox;\nmod bbox4;\nmod boundable;\nmod camera;\nmod color;\nmod fp_utils;\nmod hash;\nmod hilbert;\nmod image;\nmod lerp;\nmod light;\nmod math;\nmod mis;\nmod parse;\nmod ray;\nmod renderer;\nmod sampling;\nmod scene;\nmod shading;\nmod surface;\nmod timer;\nmod tracer;\nmod transform_stack;\n\nuse std::{fs::File, io, io::Read, mem, path::Path, str::FromStr};\n\nuse clap::{App, Arg};\nuse nom::bytes::complete::take_until;\n\nuse kioku::Arena;\n\nuse crate::{\n    accel::BVH4Node,\n    bbox::BBox,\n    parse::{parse_scene, DataTree},\n    renderer::LightPath,\n    surface::SurfaceIntersection,\n    timer::Timer,\n};\n\nconst VERSION: &str = env!(\"CARGO_PKG_VERSION\");\n\n#[allow(clippy::cognitive_complexity)]\nfn main() {\n    let mut t = Timer::new();\n\n    // Parse command line arguments.\n    let args = App::new(\"Psychopath\")\n        .version(VERSION)\n        .about(\"A slightly psychotic path tracer\")\n        .arg(\n            Arg::with_name(\"input\")\n                .short(\"i\")\n                .long(\"input\")\n                .value_name(\"FILE\")\n                .help(\"Input .psy file\")\n                .takes_value(true)\n                .required_unless_one(&[\"dev\", \"use_stdin\"]),\n        )\n        .arg(\n            Arg::with_name(\"spp\")\n                .short(\"s\")\n                .long(\"spp\")\n                .value_name(\"N\")\n                .help(\"Number of samples per pixel\")\n                .takes_value(true)\n                .validator(|s| {\n                    usize::from_str(&s)\n                        .and(Ok(()))\n                        .or(Err(\"must be an integer\".to_string()))\n                }),\n        )\n        .arg(\n            Arg::with_name(\"max_bucket_samples\")\n                .short(\"b\")\n                .long(\"spb\")\n                .value_name(\"N\")\n                .help(\"Target number of samples per bucket (determines bucket size)\")\n                .takes_value(true)\n                .validator(|s| {\n                    usize::from_str(&s)\n                        .and(Ok(()))\n                        .or(Err(\"must be an integer\".to_string()))\n                }),\n        )\n        .arg(\n            Arg::with_name(\"crop\")\n                .long(\"crop\")\n                .value_name(\"X1 Y1 X2 Y2\")\n                .help(\n                    \"Only render the image between pixel coordinates (X1, Y1) \\\n                     and (X2, Y2).  Coordinates are zero-indexed and inclusive.\",\n                )\n                .takes_value(true)\n                .number_of_values(4)\n                .validator(|s| {\n                    usize::from_str(&s)\n                        .and(Ok(()))\n                        .or(Err(\"must be four integers\".to_string()))\n                }),\n        )\n        .arg(\n            Arg::with_name(\"threads\")\n                .short(\"t\")\n                .long(\"threads\")\n                .value_name(\"N\")\n                .help(\n                    \"Number of threads to render with.  Defaults to the number of logical \\\n                     cores on the system.\",\n                )\n                .takes_value(true)\n                .validator(|s| {\n                    usize::from_str(&s)\n                        .and(Ok(()))\n                        .or(Err(\"must be an integer\".to_string()))\n                }),\n        )\n        .arg(\n            Arg::with_name(\"stats\")\n                .long(\"stats\")\n                .help(\"Print additional statistics about rendering\"),\n        )\n        .arg(\n            Arg::with_name(\"dev\")\n                .long(\"dev\")\n                .help(\"Show useful dev/debug info.\"),\n        )\n        .arg(\n            Arg::with_name(\"serialized_output\")\n                .long(\"serialized_output\")\n                .help(\"Serialize and send render output to standard output.\")\n                .hidden(true),\n        )\n        .arg(\n            Arg::with_name(\"use_stdin\")\n                .long(\"use_stdin\")\n                .help(\"Take scene file in from stdin instead of a file path.\")\n                .hidden(true),\n        )\n        .get_matches();\n\n    // Print some misc useful dev info.\n    if args.is_present(\"dev\") {\n        println!(\n            \"SurfaceIntersection size:  {} bytes\",\n            mem::size_of::<SurfaceIntersection>()\n        );\n        println!(\"LightPath size: {} bytes\", mem::size_of::<LightPath>());\n        println!(\"BBox size: {} bytes\", mem::size_of::<BBox>());\n        // println!(\"BVHNode size: {} bytes\", mem::size_of::<BVHNode>());\n        println!(\"BVH4Node size: {} bytes\", mem::size_of::<BVH4Node>());\n        return;\n    }\n\n    let crop = args.values_of(\"crop\").map(|mut vals| {\n        let coords = (\n            u32::from_str(vals.next().unwrap()).unwrap(),\n            u32::from_str(vals.next().unwrap()).unwrap(),\n            u32::from_str(vals.next().unwrap()).unwrap(),\n            u32::from_str(vals.next().unwrap()).unwrap(),\n        );\n        if coords.0 > coords.2 {\n            panic!(\"Argument '--crop': X1 must be less than or equal to X2\");\n        }\n        if coords.1 > coords.3 {\n            panic!(\"Argument '--crop': Y1 must be less than or equal to Y2\");\n        }\n        coords\n    });\n\n    // Parse data tree of scene file\n    if !args.is_present(\"serialized_output\") {\n        println!(\"Parsing scene file...\",);\n    }\n    t.tick();\n    let psy_contents = if args.is_present(\"use_stdin\") {\n        // Read from stdin\n        let mut input = Vec::new();\n        let tmp = std::io::stdin();\n        let mut stdin = tmp.lock();\n        let mut buf = vec![0u8; 4096];\n        loop {\n            let count = stdin\n                .read(&mut buf)\n                .expect(\"Unexpected end of scene input.\");\n            let start = if input.len() < 11 {\n                0\n            } else {\n                input.len() - 11\n            };\n            let end = input.len() + count;\n            input.extend(&buf[..count]);\n\n            let mut done = false;\n            let mut trunc_len = 0;\n            if let nom::IResult::Ok((remaining, _)) =\n                take_until::<&str, &[u8], ()>(\"__PSY_EOF__\")(&input[start..end])\n            {\n                done = true;\n                trunc_len = input.len() - remaining.len();\n            }\n            if done {\n                input.truncate(trunc_len);\n                break;\n            }\n        }\n        String::from_utf8(input).unwrap()\n    } else {\n        // Read from file\n        let mut input = String::new();\n        let fp = args.value_of(\"input\").unwrap();\n        let mut f = io::BufReader::new(File::open(fp).unwrap());\n        let _ = f.read_to_string(&mut input);\n        input\n    };\n\n    let dt = DataTree::from_str(&psy_contents).unwrap();\n    if !args.is_present(\"serialized_output\") {\n        println!(\"\\tParsed scene file in {:.3}s\", t.tick());\n    }\n\n    // Iterate through scenes and render them\n    if let DataTree::Internal { ref children, .. } = dt {\n        for child in children {\n            t.tick();\n            if child.type_name() == \"Scene\" {\n                if !args.is_present(\"serialized_output\") {\n                    println!(\"Building scene...\");\n                }\n\n                let arena = Arena::new().with_block_size((1 << 20) * 4);\n                let mut r = parse_scene(&arena, child).unwrap_or_else(|e| {\n                    e.print(&psy_contents);\n                    panic!(\"Parse error.\");\n                });\n\n                if let Some(spp) = args.value_of(\"spp\") {\n                    if !args.is_present(\"serialized_output\") {\n                        println!(\"\\tOverriding scene spp: {}\", spp);\n                    }\n                    r.spp = usize::from_str(spp).unwrap();\n                }\n\n                let max_samples_per_bucket =\n                    if let Some(max_samples_per_bucket) = args.value_of(\"max_bucket_samples\") {\n                        u32::from_str(max_samples_per_bucket).unwrap()\n                    } else {\n                        4096\n                    };\n\n                let thread_count = if let Some(threads) = args.value_of(\"threads\") {\n                    u32::from_str(threads).unwrap()\n                } else {\n                    num_cpus::get() as u32\n                };\n\n                if !args.is_present(\"serialized_output\") {\n                    println!(\"\\tBuilt scene in {:.3}s\", t.tick());\n                }\n\n                if !args.is_present(\"serialized_output\") {\n                    println!(\"Rendering scene with {} threads...\", thread_count);\n                }\n                let (mut image, rstats) = r.render(\n                    max_samples_per_bucket,\n                    crop,\n                    thread_count,\n                    args.is_present(\"serialized_output\"),\n                );\n                // Print render stats\n                if !args.is_present(\"serialized_output\") {\n                    let rtime = t.tick();\n                    let ntime = rtime as f64 / rstats.total_time;\n                    println!(\"\\tRendered scene in {:.3}s\", rtime);\n                    println!(\n                        \"\\t\\tTrace:                  {:.3}s\",\n                        ntime * rstats.trace_time\n                    );\n                    println!(\"\\t\\t\\tRays traced:          {}\", rstats.ray_count);\n                    println!(\n                        \"\\t\\t\\tRays/sec:             {}\",\n                        (rstats.ray_count as f64 / (ntime * rstats.trace_time) as f64) as u64\n                    );\n                    println!(\"\\t\\t\\tRay/node tests:       {}\", rstats.accel_node_visits);\n                    println!(\n                        \"\\t\\tInitial ray generation: {:.3}s\",\n                        ntime * rstats.initial_ray_generation_time\n                    );\n                    println!(\n                        \"\\t\\tRay generation:         {:.3}s\",\n                        ntime * rstats.ray_generation_time\n                    );\n                    println!(\n                        \"\\t\\tSample writing:         {:.3}s\",\n                        ntime * rstats.sample_writing_time\n                    );\n                }\n\n                // Write to disk\n                if !args.is_present(\"serialized_output\") {\n                    println!(\"Writing image to disk into '{}'...\", r.output_file);\n                    if r.output_file.ends_with(\".png\") {\n                        image\n                            .write_png(Path::new(&r.output_file))\n                            .expect(\"Failed to write png...\");\n                    } else if r.output_file.ends_with(\".exr\") {\n                        image.write_exr(Path::new(&r.output_file));\n                    } else {\n                        panic!(\"Unknown output file extension.\");\n                    }\n                    println!(\"\\tWrote image in {:.3}s\", t.tick());\n                }\n\n                // Print memory stats if stats are wanted.\n                if args.is_present(\"stats\") {\n                    // let arena_stats = arena.stats();\n                    // let mib_occupied = arena_stats.0 as f64 / 1_048_576.0;\n                    // let mib_allocated = arena_stats.1 as f64 / 1_048_576.0;\n\n                    // println!(\"MemArena stats:\");\n\n                    // if mib_occupied >= 1.0 {\n                    //     println!(\"\\tOccupied:      {:.1} MiB\", mib_occupied);\n                    // } else {\n                    //     println!(\"\\tOccupied:      {:.4} MiB\", mib_occupied);\n                    // }\n\n                    // if mib_allocated >= 1.0 {\n                    //     println!(\"\\tUsed:          {:.1} MiB\", mib_allocated);\n                    // } else {\n                    //     println!(\"\\tUsed:          {:.4} MiB\", mib_allocated);\n                    // }\n\n                    // println!(\"\\tTotal blocks:  {}\", arena_stats.2);\n                }\n            }\n        }\n    }\n\n    // End with blank line\n    println!();\n}\n"
  },
  {
    "path": "src/math.rs",
    "content": "#![allow(dead_code)]\n\nuse std::f32;\n\npub use math3d::{cross, dot, CrossProduct, DotProduct, Normal, Point, Transform, Vector};\n\n/// Gets the log base 2 of the given integer\npub fn log2_64(n: u64) -> u64 {\n    // This works by finding the largest non-zero binary digit in the\n    // number.  Its bit position is then the log2 of the integer.\n\n    if n == 0 {\n        0\n    } else {\n        (63 - n.leading_zeros()) as u64\n    }\n}\n\n/// Creates a coordinate system from a single vector.\n///\n/// The input vector, v, becomes the first vector of the\n/// returned tuple, with the other two vectors in the returned\n/// tuple defining the orthoganal axes.\n///\n/// Algorithm taken from \"Building an Orthonormal Basis, Revisited\" by Duff et al.\npub fn coordinate_system_from_vector(v: Vector) -> (Vector, Vector, Vector) {\n    let sign = v.z().signum();\n    let a = -1.0 / (sign + v.z());\n    let b = v.x() * v.y() * a;\n    let v2 = Vector::new(1.0 + sign * v.x() * v.x() * a, sign * b, -sign * v.x());\n    let v3 = Vector::new(b, sign + v.y() * v.y() * a, -v.y());\n\n    (v, v2, v3)\n}\n\n/// Simple mapping of a vector that exists in a z-up space to\n/// the space of another vector who's direction is considered\n/// z-up for the purpose.\n/// Obviously this doesn't care about the direction _around_\n/// the z-up, although it will be sufficiently consistent for\n/// isotropic sampling purposes.\n///\n/// from: The vector we're transforming.\n/// toz: The vector whose space we are transforming \"from\" into.\n///\n/// Returns the transformed vector.\npub fn zup_to_vec(from: Vector, toz: Vector) -> Vector {\n    let (toz, tox, toy) = coordinate_system_from_vector(toz.normalized());\n\n    // Use simple linear algebra to convert the \"from\"\n    // vector to a space composed of tox, toy, and toz\n    // as the x, y, and z axes.\n    (tox * from.x()) + (toy * from.y()) + (toz * from.z())\n}\n\n/// A highly accurate approximation of the probit function.\n///\n/// From Peter John Acklam, sourced from here:\n/// https://web.archive.org/web/20151030215612/http://home.online.no/~pjacklam/notes/invnorm/\n///\n/// Regarding the approximation error, he says, \"The absolute value of the\n/// relative error is less than 1.15 × 10−9 in the entire region.\"\n///\n/// Given that this implementation outputs 32-bit floating point values,\n/// and 32-bit floating point has significantly less precision than that,\n/// this approximation can essentially be considered exact.\npub fn probit(n: f32, width: f32) -> f32 {\n    let n = n as f64;\n\n    // Coefficients of the rational approximations.\n    const A1: f64 = -3.969683028665376e+01;\n    const A2: f64 = 2.209460984245205e+02;\n    const A3: f64 = -2.759285104469687e+02;\n    const A4: f64 = 1.383577518672690e+02;\n    const A5: f64 = -3.066479806614716e+01;\n    const A6: f64 = 2.506628277459239e+00;\n\n    const B1: f64 = -5.447609879822406e+01;\n    const B2: f64 = 1.615858368580409e+02;\n    const B3: f64 = -1.556989798598866e+02;\n    const B4: f64 = 6.680131188771972e+01;\n    const B5: f64 = -1.328068155288572e+01;\n\n    const C1: f64 = -7.784894002430293e-03;\n    const C2: f64 = -3.223964580411365e-01;\n    const C3: f64 = -2.400758277161838e+00;\n    const C4: f64 = -2.549732539343734e+00;\n    const C5: f64 = 4.374664141464968e+00;\n    const C6: f64 = 2.938163982698783e+00;\n\n    const D1: f64 = 7.784695709041462e-03;\n    const D2: f64 = 3.224671290700398e-01;\n    const D3: f64 = 2.445134137142996e+00;\n    const D4: f64 = 3.754408661907416e+00;\n\n    // Transition points between the rational functions.\n    const N_LOW: f64 = 0.02425;\n    const N_HIGH: f64 = 1.0 - N_LOW;\n\n    let x = match n {\n        // Lower region.\n        n if 0.0 < n && n < N_LOW => {\n            let q = (-2.0 * n.ln()).sqrt();\n            (((((C1 * q + C2) * q + C3) * q + C4) * q + C5) * q + C6)\n                / ((((D1 * q + D2) * q + D3) * q + D4) * q + 1.0)\n        }\n\n        // Central region.\n        n if n <= N_HIGH => {\n            let q = n - 0.5;\n            let r = q * q;\n            (((((A1 * r + A2) * r + A3) * r + A4) * r + A5) * r + A6) * q\n                / (((((B1 * r + B2) * r + B3) * r + B4) * r + B5) * r + 1.0)\n        }\n\n        // Upper region.\n        n if n < 1.0 => {\n            let q = (-2.0 * (1.0 - n).ln()).sqrt();\n            -(((((C1 * q + C2) * q + C3) * q + C4) * q + C5) * q + C6)\n                / ((((D1 * q + D2) * q + D3) * q + D4) * q + 1.0)\n        }\n\n        // Exactly 1 or 0.  Should be extremely rare.\n        n if n == 0.0 => -std::f64::INFINITY,\n        n if n == 1.0 => std::f64::INFINITY,\n\n        // Outside of valid input range.\n        _ => std::f64::NAN,\n    };\n\n    x as f32 * width\n}\n\npub fn fast_ln(x: f32) -> f32 {\n    fastapprox::fast::ln(x)\n}\n\npub fn fast_pow2(p: f32) -> f32 {\n    fastapprox::fast::pow2(p)\n}\n\npub fn fast_log2(x: f32) -> f32 {\n    fastapprox::fast::log2(x)\n}\n\npub fn fast_exp(p: f32) -> f32 {\n    fastapprox::fast::exp(p)\n}\n\npub fn fast_pow(x: f32, p: f32) -> f32 {\n    fastapprox::fast::pow(x, p)\n}\n\npub fn faster_pow2(p: f32) -> f32 {\n    fastapprox::faster::pow2(p)\n}\n\npub fn faster_exp(p: f32) -> f32 {\n    fastapprox::faster::exp(p)\n}\n\n//----------------------------------------------------------------\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn log2_64_test() {\n        assert_eq!(0, log2_64(0));\n\n        for i in 0..64 {\n            assert_eq!(i, log2_64(1 << i));\n        }\n\n        for i in 8..64 {\n            assert_eq!(i, log2_64((1 << i) + 227));\n        }\n\n        for i in 16..64 {\n            assert_eq!(i, log2_64((1 << i) + 56369));\n        }\n\n        for i in 32..64 {\n            assert_eq!(i, log2_64((1 << i) + 2514124923));\n        }\n    }\n}\n"
  },
  {
    "path": "src/mis.rs",
    "content": "#![allow(dead_code)]\n\npub fn balance_heuristic(a: f32, b: f32) -> f32 {\n    if a.is_infinite() {\n        a\n    } else {\n        let mis_fac = a / (a + b);\n        a / mis_fac\n    }\n}\n\npub fn power_heuristic(a: f32, b: f32) -> f32 {\n    if a.is_infinite() {\n        a\n    } else {\n        let a2 = a * a;\n        let b2 = b * b;\n        let mis_fac = a2 / (a2 + b2);\n        a / mis_fac\n    }\n}\n"
  },
  {
    "path": "src/parse/basics.rs",
    "content": "//! Some basic nom parsers\n#![allow(dead_code)]\n\nuse std::str::{self, FromStr};\n\nuse nom::{\n    character::complete::{digit1, multispace0, one_of},\n    combinator::{map_res, opt, recognize},\n    number::complete::float,\n    sequence::{delimited, tuple},\n    IResult,\n};\n\n// ========================================================\n\npub fn ws_f32(input: &str) -> IResult<&str, f32, ()> {\n    delimited(multispace0, float, multispace0)(input)\n}\n\npub fn ws_u32(input: &str) -> IResult<&str, u32, ()> {\n    map_res(delimited(multispace0, digit1, multispace0), u32::from_str)(input)\n}\n\npub fn ws_usize(input: &str) -> IResult<&str, usize, ()> {\n    map_res(delimited(multispace0, digit1, multispace0), usize::from_str)(input)\n}\n\npub fn ws_i32(input: &str) -> IResult<&str, i32, ()> {\n    map_res(\n        delimited(\n            multispace0,\n            recognize(tuple((opt(one_of(\"-\")), digit1))),\n            multispace0,\n        ),\n        i32::from_str,\n    )(input)\n}\n\n// ========================================================\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use nom::{combinator::all_consuming, sequence::tuple};\n\n    #[test]\n    fn ws_u32_1() {\n        assert_eq!(ws_u32(\"42\"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_u32(\"     42\"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_u32(\"42   \"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_u32(\"     42\"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_u32(\"     42   53\"), Ok((&\"53\"[..], 42)));\n    }\n\n    #[test]\n    fn ws_usize_1() {\n        assert_eq!(ws_usize(\"42\"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_usize(\"     42\"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_usize(\"42   \"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_usize(\"     42\"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_usize(\"     42   53\"), Ok((&\"53\"[..], 42)));\n    }\n\n    #[test]\n    fn ws_i32_1() {\n        assert_eq!(ws_i32(\"42\"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_i32(\"     42\"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_i32(\"42   \"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_i32(\"     42\"), Ok((&\"\"[..], 42)));\n        assert_eq!(ws_i32(\"     42   53\"), Ok((&\"53\"[..], 42)));\n    }\n\n    #[test]\n    fn ws_i32_2() {\n        assert_eq!(ws_i32(\"-42\"), Ok((&\"\"[..], -42)));\n        assert_eq!(ws_i32(\"     -42\"), Ok((&\"\"[..], -42)));\n        assert_eq!(ws_i32(\"-42   \"), Ok((&\"\"[..], -42)));\n        assert_eq!(ws_i32(\"     -42\"), Ok((&\"\"[..], -42)));\n        assert_eq!(ws_i32(\"     -42   53\"), Ok((&\"53\"[..], -42)));\n        assert_eq!(ws_i32(\"--42\").is_err(), true);\n    }\n\n    #[test]\n    fn ws_f32_1() {\n        assert_eq!(ws_f32(\"42\"), Ok((&\"\"[..], 42.0)));\n        assert_eq!(ws_f32(\"     42\"), Ok((&\"\"[..], 42.0)));\n        assert_eq!(ws_f32(\"42   \"), Ok((&\"\"[..], 42.0)));\n        assert_eq!(ws_f32(\"     42\"), Ok((&\"\"[..], 42.0)));\n        assert_eq!(ws_f32(\"     42   53\"), Ok((&\"53\"[..], 42.0)));\n    }\n\n    #[test]\n    fn ws_f32_2() {\n        assert_eq!(ws_f32(\"42.5\"), Ok((&\"\"[..], 42.5)));\n        assert_eq!(ws_f32(\"     42.5\"), Ok((&\"\"[..], 42.5)));\n        assert_eq!(ws_f32(\"42.5   \"), Ok((&\"\"[..], 42.5)));\n        assert_eq!(ws_f32(\"     42.5\"), Ok((&\"\"[..], 42.5)));\n        assert_eq!(ws_f32(\"     42.5   53\"), Ok((&\"53\"[..], 42.5)));\n    }\n\n    #[test]\n    fn ws_f32_3() {\n        assert_eq!(ws_f32(\"-42.5\"), Ok((&\"\"[..], -42.5)));\n        assert_eq!(ws_f32(\"     -42.5\"), Ok((&\"\"[..], -42.5)));\n        assert_eq!(ws_f32(\"-42.5   \"), Ok((&\"\"[..], -42.5)));\n        assert_eq!(ws_f32(\"     -42.5\"), Ok((&\"\"[..], -42.5)));\n        assert_eq!(ws_f32(\"     -42.5   53\"), Ok((&\"53\"[..], -42.5)));\n    }\n\n    #[test]\n    fn ws_f32_4() {\n        assert_eq!(ws_f32(\"a1.0\").is_err(), true);\n        assert_eq!(all_consuming(ws_f32)(\"0abc\").is_err(), true);\n        assert_eq!(tuple((ws_f32, ws_f32))(\"0.abc 1.2\").is_err(), true);\n    }\n}\n"
  },
  {
    "path": "src/parse/data_tree.rs",
    "content": "#![allow(dead_code)]\n\nuse std::{iter::Iterator, result::Result, slice};\n\n#[derive(Debug, Eq, PartialEq)]\npub enum DataTree<'a> {\n    Internal {\n        type_name: &'a str,\n        ident: Option<&'a str>,\n        children: Vec<DataTree<'a>>,\n        byte_offset: usize,\n    },\n\n    Leaf {\n        type_name: &'a str,\n        contents: &'a str,\n        byte_offset: usize,\n    },\n}\n\nimpl<'a> DataTree<'a> {\n    pub fn from_str(source_text: &'a str) -> Result<DataTree<'a>, ParseError> {\n        let mut items = Vec::new();\n        let mut remaining_text = (0, source_text);\n\n        while let Some((item, text)) = parse_node(remaining_text)? {\n            remaining_text = text;\n            items.push(item);\n        }\n\n        remaining_text = skip_ws_and_comments(remaining_text);\n\n        if remaining_text.1.is_empty() {\n            return Ok(DataTree::Internal {\n                type_name: \"ROOT\",\n                ident: None,\n                children: items,\n                byte_offset: 0,\n            });\n        } else {\n            // If the whole text wasn't parsed, something went wrong.\n            return Err(ParseError::Other((0, \"Failed to parse the entire string.\")));\n        }\n    }\n\n    pub fn type_name(&'a self) -> &'a str {\n        match *self {\n            DataTree::Internal { type_name, .. } | DataTree::Leaf { type_name, .. } => type_name,\n        }\n    }\n\n    pub fn byte_offset(&'a self) -> usize {\n        match *self {\n            DataTree::Internal { byte_offset, .. } | DataTree::Leaf { byte_offset, .. } => {\n                byte_offset\n            }\n        }\n    }\n\n    pub fn is_internal(&self) -> bool {\n        match *self {\n            DataTree::Internal { .. } => true,\n            DataTree::Leaf { .. } => false,\n        }\n    }\n\n    pub fn is_leaf(&self) -> bool {\n        match *self {\n            DataTree::Internal { .. } => false,\n            DataTree::Leaf { .. } => true,\n        }\n    }\n\n    pub fn leaf_contents(&'a self) -> Option<&'a str> {\n        match *self {\n            DataTree::Internal { .. } => None,\n            DataTree::Leaf { contents, .. } => Some(contents),\n        }\n    }\n\n    pub fn iter_children(&'a self) -> slice::Iter<'a, DataTree<'a>> {\n        if let DataTree::Internal { ref children, .. } = *self {\n            children.iter()\n        } else {\n            [].iter()\n        }\n    }\n\n    pub fn iter_children_with_type(&'a self, type_name: &'static str) -> DataTreeFilterIter<'a> {\n        if let DataTree::Internal { ref children, .. } = *self {\n            DataTreeFilterIter {\n                type_name: type_name,\n                iter: children.iter(),\n            }\n        } else {\n            DataTreeFilterIter {\n                type_name: type_name,\n                iter: [].iter(),\n            }\n        }\n    }\n\n    pub fn iter_internal_children_with_type(\n        &'a self,\n        type_name: &'static str,\n    ) -> DataTreeFilterInternalIter<'a> {\n        if let DataTree::Internal { ref children, .. } = *self {\n            DataTreeFilterInternalIter {\n                type_name: type_name,\n                iter: children.iter(),\n            }\n        } else {\n            DataTreeFilterInternalIter {\n                type_name: type_name,\n                iter: [].iter(),\n            }\n        }\n    }\n\n    pub fn iter_leaf_children_with_type(\n        &'a self,\n        type_name: &'static str,\n    ) -> DataTreeFilterLeafIter<'a> {\n        if let DataTree::Internal { ref children, .. } = *self {\n            DataTreeFilterLeafIter {\n                type_name: type_name,\n                iter: children.iter(),\n            }\n        } else {\n            DataTreeFilterLeafIter {\n                type_name: type_name,\n                iter: [].iter(),\n            }\n        }\n    }\n\n    // For unit tests\n    fn internal_data_or_panic(&'a self) -> (&'a str, Option<&'a str>, &'a Vec<DataTree<'a>>) {\n        if let DataTree::Internal {\n            type_name,\n            ident,\n            ref children,\n            ..\n        } = *self\n        {\n            (type_name, ident, children)\n        } else {\n            panic!(\"Expected DataTree::Internal, found DataTree::Leaf\")\n        }\n    }\n    fn leaf_data_or_panic(&'a self) -> (&'a str, &'a str) {\n        if let DataTree::Leaf {\n            type_name,\n            contents,\n            ..\n        } = *self\n        {\n            (type_name, contents)\n        } else {\n            panic!(\"Expected DataTree::Leaf, found DataTree::Internal\")\n        }\n    }\n}\n\n/// An iterator over the children of a `DataTree` node that filters out the\n/// children not matching a specified type name.\npub struct DataTreeFilterIter<'a> {\n    type_name: &'a str,\n    iter: slice::Iter<'a, DataTree<'a>>,\n}\n\nimpl<'a> Iterator for DataTreeFilterIter<'a> {\n    type Item = &'a DataTree<'a>;\n\n    fn next(&mut self) -> Option<&'a DataTree<'a>> {\n        loop {\n            if let Some(dt) = self.iter.next() {\n                if dt.type_name() == self.type_name {\n                    return Some(dt);\n                } else {\n                    continue;\n                }\n            } else {\n                return None;\n            }\n        }\n    }\n}\n\n/// An iterator over the children of a `DataTree` node that filters out the\n/// children that aren't internal nodes and that don't match a specified\n/// type name.\npub struct DataTreeFilterInternalIter<'a> {\n    type_name: &'a str,\n    iter: slice::Iter<'a, DataTree<'a>>,\n}\n\nimpl<'a> Iterator for DataTreeFilterInternalIter<'a> {\n    type Item = (&'a str, Option<&'a str>, &'a Vec<DataTree<'a>>, usize);\n\n    fn next(&mut self) -> Option<(&'a str, Option<&'a str>, &'a Vec<DataTree<'a>>, usize)> {\n        loop {\n            match self.iter.next() {\n                Some(&DataTree::Internal {\n                    type_name,\n                    ident,\n                    ref children,\n                    byte_offset,\n                }) => {\n                    if type_name == self.type_name {\n                        return Some((type_name, ident, children, byte_offset));\n                    } else {\n                        continue;\n                    }\n                }\n\n                Some(&DataTree::Leaf { .. }) => {\n                    continue;\n                }\n\n                None => {\n                    return None;\n                }\n            }\n        }\n    }\n}\n\n/// An iterator over the children of a `DataTree` node that filters out the\n/// children that aren't internal nodes and that don't match a specified\n/// type name.\npub struct DataTreeFilterLeafIter<'a> {\n    type_name: &'a str,\n    iter: slice::Iter<'a, DataTree<'a>>,\n}\n\nimpl<'a> Iterator for DataTreeFilterLeafIter<'a> {\n    type Item = (&'a str, &'a str, usize);\n\n    fn next(&mut self) -> Option<(&'a str, &'a str, usize)> {\n        loop {\n            match self.iter.next() {\n                Some(&DataTree::Internal { .. }) => {\n                    continue;\n                }\n\n                Some(&DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                }) => {\n                    if type_name == self.type_name {\n                        return Some((type_name, contents, byte_offset));\n                    } else {\n                        continue;\n                    }\n                }\n\n                None => {\n                    return None;\n                }\n            }\n        }\n    }\n}\n\n#[derive(Copy, Clone, Eq, PartialEq, Debug)]\npub enum ParseError {\n    MissingOpener(usize),\n    MissingOpenInternal(usize),\n    MissingCloseInternal(usize),\n    MissingOpenLeaf(usize),\n    MissingCloseLeaf(usize),\n    MissingTypeName(usize),\n    UnexpectedIdent(usize),\n    UnknownToken(usize),\n    Other((usize, &'static str)),\n}\n\n// ================================================================\n\n#[derive(Debug, PartialEq, Eq)]\nenum Token<'a> {\n    OpenInner,\n    CloseInner,\n    OpenLeaf,\n    CloseLeaf,\n    TypeName(&'a str),\n    Ident(&'a str),\n    End,\n    Unknown,\n}\n\ntype ParseResult<'a> = Result<Option<(DataTree<'a>, (usize, &'a str))>, ParseError>;\n\nfn parse_node<'a>(source_text: (usize, &'a str)) -> ParseResult<'a> {\n    let (token, text1) = next_token(source_text);\n    if let Token::TypeName(type_name) = token {\n        match next_token(text1) {\n            // Internal with name\n            (Token::Ident(n), text2) => {\n                if let (Token::OpenInner, text3) = next_token(text2) {\n                    let mut children = Vec::new();\n                    let mut text_remaining = text3;\n                    while let Some((node, text4)) = parse_node(text_remaining)? {\n                        text_remaining = text4;\n                        children.push(node);\n                    }\n                    if let (Token::CloseInner, text4) = next_token(text_remaining) {\n                        return Ok(Some((\n                            DataTree::Internal {\n                                type_name: type_name,\n                                ident: Some(n),\n                                children: children,\n                                byte_offset: text1.0,\n                            },\n                            text4,\n                        )));\n                    } else {\n                        return Err(ParseError::MissingCloseInternal(text_remaining.0));\n                    }\n                } else {\n                    return Err(ParseError::MissingOpenInternal(text2.0));\n                }\n            }\n\n            // Internal without name\n            (Token::OpenInner, text2) => {\n                let mut children = Vec::new();\n                let mut text_remaining = text2;\n                while let Some((node, text3)) = parse_node(text_remaining)? {\n                    text_remaining = text3;\n                    children.push(node);\n                }\n\n                if let (Token::CloseInner, text3) = next_token(text_remaining) {\n                    return Ok(Some((\n                        DataTree::Internal {\n                            type_name: type_name,\n                            ident: None,\n                            children: children,\n                            byte_offset: text1.0,\n                        },\n                        text3,\n                    )));\n                } else {\n                    return Err(ParseError::MissingCloseInternal(text_remaining.0));\n                }\n            }\n\n            // Leaf\n            (Token::OpenLeaf, text2) => {\n                let (contents, text3) = parse_leaf_content(text2);\n                if let (Token::CloseLeaf, text4) = next_token(text3) {\n                    return Ok(Some((\n                        DataTree::Leaf {\n                            type_name: type_name,\n                            contents: contents,\n                            byte_offset: text1.0,\n                        },\n                        text4,\n                    )));\n                } else {\n                    return Err(ParseError::MissingCloseLeaf(text3.0));\n                }\n            }\n\n            // Other\n            _ => {\n                return Err(ParseError::MissingOpener(text1.0));\n            }\n        }\n    } else {\n        return Ok(None);\n    }\n}\n\nfn parse_leaf_content(source_text: (usize, &str)) -> (&str, (usize, &str)) {\n    let mut si = 1;\n    let mut escaped = false;\n    let mut reached_end = true;\n    for (i, c) in source_text.1.char_indices() {\n        si = i;\n        if escaped {\n            escaped = false;\n        } else if c == '\\\\' {\n            escaped = true;\n        } else if c == ']' {\n            reached_end = false;\n            break;\n        }\n    }\n\n    if reached_end {\n        si = source_text.1.len();\n    }\n\n    return (\n        &source_text.1[0..si],\n        (source_text.0 + si, &source_text.1[si..]),\n    );\n}\n\nfn next_token<'a>(source_text: (usize, &'a str)) -> (Token<'a>, (usize, &'a str)) {\n    let text1 = skip_ws_and_comments(source_text);\n\n    if let Some(c) = text1.1.chars().nth(0) {\n        let text2 = (text1.0 + c.len_utf8(), &text1.1[c.len_utf8()..]);\n        match c {\n            '{' => {\n                return (Token::OpenInner, text2);\n            }\n\n            '}' => {\n                return (Token::CloseInner, text2);\n            }\n\n            '[' => {\n                return (Token::OpenLeaf, text2);\n            }\n\n            ']' => {\n                return (Token::CloseLeaf, text2);\n            }\n\n            '$' => {\n                // Parse name\n                let mut si = 1;\n                let mut escaped = false;\n                let mut reached_end = true;\n                for (i, c) in text1.1.char_indices().skip(1) {\n                    si = i;\n                    if escaped {\n                        escaped = false;\n                    } else if c == '\\\\' {\n                        escaped = true;\n                    } else if !is_ident_char(c) {\n                        reached_end = false;\n                        break;\n                    }\n                }\n\n                if reached_end {\n                    si = text1.1.len();\n                }\n\n                return (\n                    Token::Ident(&text1.1[0..si]),\n                    (text1.0 + si, &text1.1[si..]),\n                );\n            }\n\n            _ => {\n                if is_ident_char(c) {\n                    // Parse type\n                    let mut si = 0;\n                    let mut reached_end = true;\n                    for (i, c) in text1.1.char_indices() {\n                        si = i;\n                        if !is_ident_char(c) {\n                            reached_end = false;\n                            break;\n                        }\n                    }\n\n                    if reached_end {\n                        si = text1.1.len();\n                    }\n\n                    return (\n                        Token::TypeName(&text1.1[0..si]),\n                        (text1.0 + si, &text1.1[si..]),\n                    );\n                }\n            }\n        }\n    } else {\n        return (Token::End, text1);\n    }\n\n    return (Token::Unknown, text1);\n}\n\nfn is_ws(c: char) -> bool {\n    match c {\n        '\\n' | '\\r' | '\\t' | ' ' => true,\n        _ => false,\n    }\n}\n\nfn is_nl(c: char) -> bool {\n    match c {\n        '\\n' | '\\r' => true,\n        _ => false,\n    }\n}\n\nfn is_reserved_char(c: char) -> bool {\n    match c {\n        '{' | '}' | '[' | ']' | '$' | '#' | '\\\\' => true,\n        _ => false,\n    }\n}\n\nfn is_ident_char(c: char) -> bool {\n    // Anything that isn't whitespace or a reserved character\n    !is_ws(c) && !is_reserved_char(c)\n}\n\nfn skip_ws(text: &str) -> &str {\n    let mut si = 0;\n    let mut reached_end = true;\n    for (i, c) in text.char_indices() {\n        si = i;\n        if !is_ws(c) {\n            reached_end = false;\n            break;\n        }\n    }\n\n    if reached_end {\n        si = text.len();\n    }\n\n    return &text[si..];\n}\n\nfn skip_comment(text: &str) -> &str {\n    let mut si = 0;\n    if Some('#') == text.chars().nth(0) {\n        let mut reached_end = true;\n        for (i, c) in text.char_indices() {\n            si = i;\n            if is_nl(c) {\n                reached_end = false;\n                break;\n            }\n        }\n\n        if reached_end {\n            si = text.len();\n        }\n    }\n\n    return &text[si..];\n}\n\nfn skip_ws_and_comments(text: (usize, &str)) -> (usize, &str) {\n    let mut remaining_text = text.1;\n\n    loop {\n        let tmp = skip_comment(skip_ws(remaining_text));\n\n        if tmp.len() == remaining_text.len() {\n            break;\n        } else {\n            remaining_text = tmp;\n        }\n    }\n\n    let offset = text.0 + text.1.len() - remaining_text.len();\n    return (offset, remaining_text);\n}\n\n// ================================================================\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use super::{next_token, Token};\n\n    #[test]\n    fn tokenize_1() {\n        let input = (0, \"Thing\");\n\n        assert_eq!(next_token(input), (Token::TypeName(\"Thing\"), (5, \"\")));\n    }\n\n    #[test]\n    fn tokenize_2() {\n        let input = (0, \"  \\n# gdfgdf gfdg dggdf\\\\sg dfgsd \\n   Thing\");\n\n        assert_eq!(next_token(input), (Token::TypeName(\"Thing\"), (41, \"\")));\n    }\n\n    #[test]\n    fn tokenize_3() {\n        let input1 = (0, \" Thing { }\");\n        let (token1, input2) = next_token(input1);\n        let (token2, input3) = next_token(input2);\n        let (token3, input4) = next_token(input3);\n\n        assert_eq!((token1, input2.1), (Token::TypeName(\"Thing\"), \" { }\"));\n        assert_eq!((token2, input3.1), (Token::OpenInner, \" }\"));\n        assert_eq!((token3, input4.1), (Token::CloseInner, \"\"));\n    }\n\n    #[test]\n    fn tokenize_4() {\n        let input = (0, \" $hi_there \");\n\n        assert_eq!(next_token(input), (Token::Ident(\"$hi_there\"), (10, \" \")));\n    }\n\n    #[test]\n    fn tokenize_5() {\n        let input = (0, \" $hi\\\\ t\\\\#he\\\\[re \");\n\n        assert_eq!(\n            next_token(input),\n            (Token::Ident(\"$hi\\\\ t\\\\#he\\\\[re\"), (15, \" \"),)\n        );\n    }\n\n    #[test]\n    fn tokenize_6() {\n        let input1 = (0, \" $hi the[re\");\n        let (token1, input2) = next_token(input1);\n        let (token2, input3) = next_token(input2);\n        let (token3, input4) = next_token(input3);\n        let (token4, input5) = next_token(input4);\n        let (token5, input6) = next_token(input5);\n\n        assert_eq!((token1, input2), (Token::Ident(\"$hi\"), (4, \" the[re\")));\n        assert_eq!((token2, input3), (Token::TypeName(\"the\"), (8, \"[re\")));\n        assert_eq!((token3, input4), (Token::OpenLeaf, (9, \"re\")));\n        assert_eq!((token4, input5), (Token::TypeName(\"re\"), (11, \"\")));\n        assert_eq!((token5, input6), (Token::End, (11, \"\")));\n    }\n\n    #[test]\n    fn tokenize_7() {\n        let input1 = (0, \"Thing $yar { # A comment\\n\\tThing2 []\\n}\");\n        let (token1, input2) = next_token(input1);\n        let (token2, input3) = next_token(input2);\n        let (token3, input4) = next_token(input3);\n        let (token4, input5) = next_token(input4);\n        let (token5, input6) = next_token(input5);\n        let (token6, input7) = next_token(input6);\n        let (token7, input8) = next_token(input7);\n        let (token8, input9) = next_token(input8);\n\n        assert_eq!(\n            (token1, input2),\n            (\n                Token::TypeName(\"Thing\"),\n                (5, \" $yar { # A comment\\n\\tThing2 []\\n}\",)\n            )\n        );\n        assert_eq!(\n            (token2, input3),\n            (\n                Token::Ident(\"$yar\"),\n                (10, \" { # A comment\\n\\tThing2 []\\n}\",)\n            )\n        );\n        assert_eq!(\n            (token3, input4),\n            (Token::OpenInner, (12, \" # A comment\\n\\tThing2 []\\n}\",))\n        );\n        assert_eq!(\n            (token4, input5),\n            (Token::TypeName(\"Thing2\"), (32, \" []\\n}\"))\n        );\n        assert_eq!((token5, input6), (Token::OpenLeaf, (34, \"]\\n}\")));\n        assert_eq!((token6, input7), (Token::CloseLeaf, (35, \"\\n}\")));\n        assert_eq!((token7, input8), (Token::CloseInner, (37, \"\")));\n        assert_eq!((token8, input9), (Token::End, (37, \"\")));\n    }\n\n    #[test]\n    fn parse_1() {\n        let input = r#\"\n            Thing {}\n        \"#;\n\n        let dt = DataTree::from_str(input).unwrap();\n\n        // Root\n        let (t, i, c) = dt.internal_data_or_panic();\n        assert_eq!(t, \"ROOT\");\n        assert_eq!(i, None);\n        assert_eq!(c.len(), 1);\n\n        // First (and only) child\n        let (t, i, c) = c[0].internal_data_or_panic();\n        assert_eq!(t, \"Thing\");\n        assert_eq!(i, None);\n        assert_eq!(c.len(), 0);\n    }\n\n    #[test]\n    fn iter_1() {\n        let dt = DataTree::from_str(\n            r#\"\n            A {}\n            B {}\n            A []\n            A {}\n            B {}\n        \"#,\n        )\n        .unwrap();\n\n        let i = dt.iter_children_with_type(\"A\");\n        assert_eq!(i.count(), 3);\n    }\n\n    #[test]\n    fn iter_2() {\n        let dt = DataTree::from_str(\n            r#\"\n            A {}\n            B {}\n            A []\n            A {}\n            B {}\n        \"#,\n        )\n        .unwrap();\n\n        let i = dt.iter_internal_children_with_type(\"A\");\n        assert_eq!(i.count(), 2);\n    }\n\n    #[test]\n    fn iter_3() {\n        let dt = DataTree::from_str(\n            r#\"\n            A []\n            B {}\n            A {}\n            A []\n            B {}\n        \"#,\n        )\n        .unwrap();\n\n        let i = dt.iter_leaf_children_with_type(\"A\");\n        assert_eq!(i.count(), 2);\n    }\n}\n"
  },
  {
    "path": "src/parse/mod.rs",
    "content": "pub mod basics;\nmod data_tree;\nmod psy;\nmod psy_assembly;\nmod psy_light;\nmod psy_mesh_surface;\nmod psy_surface_shader;\n\npub use self::{data_tree::DataTree, psy::parse_scene};\n"
  },
  {
    "path": "src/parse/psy.rs",
    "content": "#![allow(dead_code)]\n\nuse std::{f32, result::Result};\n\nuse nom::{combinator::all_consuming, sequence::tuple, IResult};\n\nuse kioku::Arena;\n\nuse crate::{\n    camera::Camera,\n    color::{rec709_e_to_xyz, Color},\n    light::WorldLightSource,\n    math::Transform,\n    renderer::Renderer,\n    scene::Scene,\n    scene::World,\n};\n\nuse super::{\n    basics::{ws_f32, ws_u32},\n    psy_assembly::parse_assembly,\n    psy_light::parse_distant_disk_light,\n    DataTree,\n};\n\n#[derive(Debug)]\npub enum PsyParseError {\n    // The first usize for all errors is their byte offset\n    // into the psy content where they occured.\n    UnknownError(usize),\n    UnknownVariant(usize, &'static str),        // Error message\n    ExpectedInternalNode(usize, &'static str),  // Error message\n    ExpectedLeafNode(usize, &'static str),      // Error message\n    MissingNode(usize, &'static str),           // Error message\n    IncorrectLeafData(usize, &'static str),     // Error message\n    WrongNodeCount(usize, &'static str, usize), // Error message, sections found\n    InstancedMissingData(usize, &'static str, String), // Error message, data name\n}\n\nimpl PsyParseError {\n    pub fn print(&self, psy_content: &str) {\n        match *self {\n            PsyParseError::UnknownError(offset) => {\n                let line = line_count_to_byte_offset(psy_content, offset);\n                println!(\n                    \"Line {}: Unknown parse error.  If you get this message, please report \\\n                     it to the developers so they can improve the error messages.\",\n                    line\n                );\n            }\n\n            PsyParseError::UnknownVariant(offset, error) => {\n                let line = line_count_to_byte_offset(psy_content, offset);\n                println!(\"Line {}: {}\", line, error);\n            }\n\n            PsyParseError::ExpectedInternalNode(offset, error) => {\n                let line = line_count_to_byte_offset(psy_content, offset);\n                println!(\"Line {}: {}\", line, error);\n            }\n\n            PsyParseError::ExpectedLeafNode(offset, error) => {\n                let line = line_count_to_byte_offset(psy_content, offset);\n                println!(\"Line {}: {}\", line, error);\n            }\n\n            PsyParseError::MissingNode(offset, error) => {\n                let line = line_count_to_byte_offset(psy_content, offset);\n                println!(\"Line {}: {}\", line, error);\n            }\n\n            PsyParseError::IncorrectLeafData(offset, error) => {\n                let line = line_count_to_byte_offset(psy_content, offset);\n                println!(\"Line {}: {}\", line, error);\n            }\n\n            PsyParseError::WrongNodeCount(offset, error, count) => {\n                let line = line_count_to_byte_offset(psy_content, offset);\n                println!(\"Line {}: {}  Found: {}\", line, error, count);\n            }\n\n            PsyParseError::InstancedMissingData(offset, error, ref data_name) => {\n                let line = line_count_to_byte_offset(psy_content, offset);\n                println!(\"Line {}: {} Data name: '{}'\", line, error, data_name);\n            }\n        }\n    }\n}\n\nfn line_count_to_byte_offset(text: &str, offset: usize) -> usize {\n    text[..offset].matches('\\n').count() + 1\n}\n\n/// Takes in a `DataTree` representing a Scene node and returns\npub fn parse_scene<'a>(\n    arena: &'a Arena,\n    tree: &'a DataTree,\n) -> Result<Renderer<'a>, PsyParseError> {\n    // Verify we have the right number of each section\n    if tree.iter_children_with_type(\"Output\").count() != 1 {\n        let count = tree.iter_children_with_type(\"Output\").count();\n        return Err(PsyParseError::WrongNodeCount(\n            tree.byte_offset(),\n            \"Scene should have precisely one Output \\\n             section.\",\n            count,\n        ));\n    }\n    if tree.iter_children_with_type(\"RenderSettings\").count() != 1 {\n        let count = tree.iter_children_with_type(\"RenderSettings\").count();\n        return Err(PsyParseError::WrongNodeCount(\n            tree.byte_offset(),\n            \"Scene should have precisely one \\\n             RenderSettings section.\",\n            count,\n        ));\n    }\n    if tree.iter_children_with_type(\"Camera\").count() != 1 {\n        let count = tree.iter_children_with_type(\"Camera\").count();\n        return Err(PsyParseError::WrongNodeCount(\n            tree.byte_offset(),\n            \"Scene should have precisely one Camera \\\n             section.\",\n            count,\n        ));\n    }\n    if tree.iter_children_with_type(\"World\").count() != 1 {\n        let count = tree.iter_children_with_type(\"World\").count();\n        return Err(PsyParseError::WrongNodeCount(\n            tree.byte_offset(),\n            \"Scene should have precisely one World section.\",\n            count,\n        ));\n    }\n    if tree.iter_children_with_type(\"Assembly\").count() != 1 {\n        let count = tree.iter_children_with_type(\"Assembly\").count();\n        return Err(PsyParseError::WrongNodeCount(\n            tree.byte_offset(),\n            \"Scene should have precisely one Root Assembly \\\n             section.\",\n            count,\n        ));\n    }\n\n    // Parse output info\n    let output_info = parse_output_info(tree.iter_children_with_type(\"Output\").nth(0).unwrap())?;\n\n    // Parse render settings\n    let render_settings = parse_render_settings(\n        tree.iter_children_with_type(\"RenderSettings\")\n            .nth(0)\n            .unwrap(),\n    )?;\n\n    // Parse camera\n    let camera = parse_camera(\n        arena,\n        tree.iter_children_with_type(\"Camera\").nth(0).unwrap(),\n    )?;\n\n    // Parse world\n    let world = parse_world(arena, tree.iter_children_with_type(\"World\").nth(0).unwrap())?;\n\n    // Parse root scene assembly\n    let assembly = parse_assembly(\n        arena,\n        tree.iter_children_with_type(\"Assembly\").nth(0).unwrap(),\n    )?;\n\n    // Put scene together\n    let scene_name = if let DataTree::Internal { ident, .. } = *tree {\n        if let Some(name) = ident {\n            Some(name.to_string())\n        } else {\n            None\n        }\n    } else {\n        None\n    };\n    let scene = Scene {\n        name: scene_name,\n        camera: camera,\n        world: world,\n        root: assembly,\n    };\n\n    // Put renderer together\n    let renderer = Renderer {\n        output_file: output_info.clone(),\n        resolution: (\n            (render_settings.0).0 as usize,\n            (render_settings.0).1 as usize,\n        ),\n        spp: render_settings.1 as usize,\n        seed: render_settings.2,\n        scene: scene,\n    };\n\n    return Ok(renderer);\n}\n\nfn parse_output_info(tree: &DataTree) -> Result<String, PsyParseError> {\n    if let DataTree::Internal { ref children, .. } = *tree {\n        let mut found_path = false;\n        let mut path = String::new();\n\n        for child in children {\n            match *child {\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Path\" => {\n                    // Trim and validate\n                    let tc = contents.trim();\n                    if tc.chars().count() < 2 {\n                        return Err(PsyParseError::IncorrectLeafData(\n                            byte_offset,\n                            \"File path format is \\\n                             incorrect.\",\n                        ));\n                    }\n                    if tc.chars().nth(0).unwrap() != '\"' || !tc.ends_with('\"') {\n                        return Err(PsyParseError::IncorrectLeafData(\n                            byte_offset,\n                            \"File paths must be \\\n                             surrounded by quotes.\",\n                        ));\n                    }\n                    let len = tc.len();\n                    let tc = &tc[1..len - 1];\n\n                    // Parse\n                    // TODO: proper string escaping\n                    found_path = true;\n                    path = tc.to_string();\n                }\n\n                _ => {}\n            }\n        }\n\n        if found_path {\n            return Ok(path);\n        } else {\n            return Err(PsyParseError::MissingNode(\n                tree.byte_offset(),\n                \"Output section must contain a Path.\",\n            ));\n        }\n    } else {\n        return Err(PsyParseError::ExpectedInternalNode(\n            tree.byte_offset(),\n            \"Output section should be an internal \\\n             node, containing at least a Path.\",\n        ));\n    };\n}\n\nfn parse_render_settings(tree: &DataTree) -> Result<((u32, u32), u32, u32), PsyParseError> {\n    if let DataTree::Internal { ref children, .. } = *tree {\n        let mut found_res = false;\n        let mut found_spp = false;\n        let mut res = (0, 0);\n        let mut spp = 0;\n        let mut seed = 0;\n\n        for child in children {\n            match *child {\n                // Resolution\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Resolution\" => {\n                    if let IResult::Ok((_, (w, h))) =\n                        all_consuming(tuple((ws_u32, ws_u32)))(contents)\n                    {\n                        found_res = true;\n                        res = (w, h);\n                    } else {\n                        // Found Resolution, but its contents is not in the right format\n                        return Err(PsyParseError::IncorrectLeafData(\n                            byte_offset,\n                            \"Resolution should be specified with two \\\n                             integers in the form '[width height]'.\",\n                        ));\n                    }\n                }\n\n                // SamplesPerPixel\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"SamplesPerPixel\" => {\n                    if let IResult::Ok((_, n)) = all_consuming(ws_u32)(contents) {\n                        found_spp = true;\n                        spp = n;\n                    } else {\n                        // Found SamplesPerPixel, but its contents is not in the right format\n                        return Err(PsyParseError::IncorrectLeafData(\n                            byte_offset,\n                            \"SamplesPerPixel should be \\\n                             an integer specified in \\\n                             the form '[samples]'.\",\n                        ));\n                    }\n                }\n\n                // Seed\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Seed\" => {\n                    if let IResult::Ok((_, n)) = all_consuming(ws_u32)(contents) {\n                        seed = n;\n                    } else {\n                        // Found Seed, but its contents is not in the right format\n                        return Err(PsyParseError::IncorrectLeafData(\n                            byte_offset,\n                            \"Seed should be an integer \\\n                             specified in the form \\\n                             '[samples]'.\",\n                        ));\n                    }\n                }\n\n                _ => {}\n            }\n        }\n\n        if found_res && found_spp {\n            return Ok((res, spp, seed));\n        } else {\n            return Err(PsyParseError::MissingNode(\n                tree.byte_offset(),\n                \"RenderSettings must have both Resolution and \\\n                 SamplesPerPixel specified.\",\n            ));\n        }\n    } else {\n        return Err(PsyParseError::ExpectedInternalNode(\n            tree.byte_offset(),\n            \"RenderSettings section should be an \\\n             internal node, containing at least \\\n             Resolution and SamplesPerPixel.\",\n        ));\n    };\n}\n\nfn parse_camera<'a>(arena: &'a Arena, tree: &'a DataTree) -> Result<Camera<'a>, PsyParseError> {\n    if let DataTree::Internal { ref children, .. } = *tree {\n        let mut mats = Vec::new();\n        let mut fovs = Vec::new();\n        let mut focus_distances = Vec::new();\n        let mut aperture_radii = Vec::new();\n\n        // Parse\n        for child in children.iter() {\n            match *child {\n                // Fov\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Fov\" => {\n                    if let IResult::Ok((_, fov)) = all_consuming(ws_f32)(contents) {\n                        fovs.push(fov * (f32::consts::PI / 180.0));\n                    } else {\n                        // Found Fov, but its contents is not in the right format\n                        return Err(PsyParseError::IncorrectLeafData(\n                            byte_offset,\n                            \"Fov should be a decimal \\\n                             number specified in the \\\n                             form '[fov]'.\",\n                        ));\n                    }\n                }\n\n                // FocalDistance\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"FocalDistance\" => {\n                    if let IResult::Ok((_, fd)) = all_consuming(ws_f32)(contents) {\n                        focus_distances.push(fd);\n                    } else {\n                        // Found FocalDistance, but its contents is not in the right format\n                        return Err(PsyParseError::IncorrectLeafData(\n                            byte_offset,\n                            \"FocalDistance should be a \\\n                             decimal number specified \\\n                             in the form '[fov]'.\",\n                        ));\n                    }\n                }\n\n                // ApertureRadius\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"ApertureRadius\" => {\n                    if let IResult::Ok((_, ar)) = all_consuming(ws_f32)(contents) {\n                        aperture_radii.push(ar);\n                    } else {\n                        // Found ApertureRadius, but its contents is not in the right format\n                        return Err(PsyParseError::IncorrectLeafData(\n                            byte_offset,\n                            \"ApertureRadius should be a \\\n                             decimal number specified \\\n                             in the form '[fov]'.\",\n                        ));\n                    }\n                }\n\n                // Transform\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Transform\" => {\n                    if let Ok(mat) = parse_matrix(contents) {\n                        mats.push(mat);\n                    } else {\n                        // Found Transform, but its contents is not in the right format\n                        return Err(make_transform_format_error(byte_offset));\n                    }\n                }\n\n                _ => {}\n            }\n        }\n\n        return Ok(Camera::new(\n            arena,\n            &mats,\n            &fovs,\n            &aperture_radii,\n            &focus_distances,\n        ));\n    } else {\n        return Err(PsyParseError::ExpectedInternalNode(\n            tree.byte_offset(),\n            \"Camera section should be an internal \\\n             node, containing at least Fov and \\\n             Transform.\",\n        ));\n    }\n}\n\nfn parse_world<'a>(arena: &'a Arena, tree: &'a DataTree) -> Result<World<'a>, PsyParseError> {\n    if tree.is_internal() {\n        let background_color;\n        let mut lights: Vec<&dyn WorldLightSource> = Vec::new();\n\n        // Parse background shader\n        let bgs = {\n            if tree.iter_children_with_type(\"BackgroundShader\").count() != 1 {\n                return Err(PsyParseError::WrongNodeCount(\n                    tree.byte_offset(),\n                    \"World should have precisely one BackgroundShader section.\",\n                    tree.iter_children_with_type(\"BackgroundShader\").count(),\n                ));\n            }\n            tree.iter_children_with_type(\"BackgroundShader\")\n                .nth(0)\n                .unwrap()\n        };\n        let bgs_type = {\n            if bgs.iter_children_with_type(\"Type\").count() != 1 {\n                return Err(PsyParseError::WrongNodeCount(\n                    bgs.byte_offset(),\n                    \"BackgroundShader should have \\\n                     precisely one Type specified.\",\n                    bgs.iter_children_with_type(\"Type\").count(),\n                ));\n            }\n            if let DataTree::Leaf { contents, .. } =\n                *bgs.iter_children_with_type(\"Type\").nth(0).unwrap()\n            {\n                contents.trim()\n            } else {\n                return Err(PsyParseError::ExpectedLeafNode(\n                    bgs.byte_offset(),\n                    \"BackgroundShader's Type should be a \\\n                     leaf node.\",\n                ));\n            }\n        };\n        match bgs_type {\n            \"Color\" => {\n                if let Some(&DataTree::Leaf {\n                    contents,\n                    byte_offset,\n                    ..\n                }) = bgs.iter_children_with_type(\"Color\").nth(0)\n                {\n                    if let Ok(color) = parse_color(contents) {\n                        background_color = color;\n                    } else {\n                        return Err(PsyParseError::IncorrectLeafData(\n                            byte_offset,\n                            \"Color should be specified \\\n                             with three decimal numbers \\\n                             in the form '[R G B]'.\",\n                        ));\n                    }\n                } else {\n                    return Err(PsyParseError::MissingNode(\n                        bgs.byte_offset(),\n                        \"BackgroundShader's Type is Color, \\\n                         but no Color is specified.\",\n                    ));\n                }\n            }\n\n            _ => {\n                return Err(PsyParseError::UnknownVariant(\n                    bgs.byte_offset(),\n                    \"The specified BackgroundShader Type \\\n                     isn't a recognized type.\",\n                ))\n            }\n        }\n\n        // Parse light sources\n        for child in tree.iter_children() {\n            match *child {\n                DataTree::Internal { type_name, .. } if type_name == \"DistantDiskLight\" => {\n                    lights.push(arena.alloc(parse_distant_disk_light(arena, child)?));\n                }\n\n                _ => {}\n            }\n        }\n\n        // Build and return the world\n        return Ok(World {\n            background_color: background_color,\n            lights: arena.copy_slice(&lights),\n        });\n    } else {\n        return Err(PsyParseError::ExpectedInternalNode(\n            tree.byte_offset(),\n            \"World section should be an internal \\\n             node, containing at least a \\\n             BackgroundShader.\",\n        ));\n    }\n}\n\npub fn parse_matrix(contents: &str) -> Result<Transform, PsyParseError> {\n    if let IResult::Ok((leftover, ns)) = all_consuming(tuple((\n        ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32,\n        ws_f32, ws_f32, ws_f32, ws_f32, ws_f32,\n    )))(contents)\n    {\n        if leftover.is_empty() {\n            return Ok(Transform::new_from_values(\n                // We throw away the last row, since it's not necessarily affine.\n                // TODO: is there a more correct way to handle this?\n                ns.0, ns.4, ns.8, ns.12, ns.1, ns.5, ns.9, ns.13, ns.2, ns.6, ns.10, ns.14,\n            ));\n        }\n    }\n\n    return Err(PsyParseError::UnknownError(0));\n}\n\npub fn make_transform_format_error(byte_offset: usize) -> PsyParseError {\n    PsyParseError::IncorrectLeafData(\n        byte_offset,\n        \"Transform should be sixteen integers specified in \\\n         the form '[# # # # # # # # # # # # # # # #]'.\",\n    )\n}\n\npub fn parse_color(contents: &str) -> Result<Color, PsyParseError> {\n    let items: Vec<_> = contents.split(',').map(|s| s.trim()).collect();\n    if items.len() != 2 {\n        return Err(PsyParseError::UnknownError(0));\n    }\n\n    match items[0] {\n        \"rec709\" => {\n            if let IResult::Ok((_, color)) = tuple((ws_f32, ws_f32, ws_f32))(items[1]) {\n                return Ok(Color::new_xyz(rec709_e_to_xyz(color)));\n            } else {\n                return Err(PsyParseError::UnknownError(0));\n            }\n        }\n\n        \"blackbody\" => {\n            if let IResult::Ok((_, (temperature, factor))) = tuple((ws_f32, ws_f32))(items[1]) {\n                return Ok(Color::new_blackbody(temperature, factor));\n            } else {\n                return Err(PsyParseError::UnknownError(0));\n            }\n        }\n\n        \"color_temperature\" => {\n            if let IResult::Ok((_, (temperature, factor))) = tuple((ws_f32, ws_f32))(items[1]) {\n                return Ok(Color::new_temperature(temperature, factor));\n            } else {\n                return Err(PsyParseError::UnknownError(0));\n            }\n        }\n\n        _ => return Err(PsyParseError::UnknownError(0)),\n    }\n}\n"
  },
  {
    "path": "src/parse/psy_assembly.rs",
    "content": "#![allow(dead_code)]\n\nuse std::result::Result;\n\nuse kioku::Arena;\n\nuse crate::scene::{Assembly, AssemblyBuilder, Object};\n\nuse super::{\n    psy::{parse_matrix, PsyParseError},\n    psy_light::{parse_rectangle_light, parse_sphere_light},\n    psy_mesh_surface::parse_mesh_surface,\n    psy_surface_shader::parse_surface_shader,\n    DataTree,\n};\n\npub fn parse_assembly<'a>(\n    arena: &'a Arena,\n    tree: &'a DataTree,\n) -> Result<Assembly<'a>, PsyParseError> {\n    let mut builder = AssemblyBuilder::new(arena);\n\n    if tree.is_internal() {\n        for child in tree.iter_children() {\n            match child.type_name() {\n                // Sub-Assembly\n                \"Assembly\" => {\n                    if let DataTree::Internal {\n                        ident: Some(ident), ..\n                    } = *child\n                    {\n                        builder.add_assembly(ident, parse_assembly(arena, child)?);\n                    } else {\n                        return Err(PsyParseError::UnknownError(child.byte_offset()));\n                    }\n                }\n\n                // Instance\n                \"Instance\" => {\n                    // Pre-conditions\n                    if !child.is_internal() {\n                        return Err(PsyParseError::UnknownError(child.byte_offset()));\n                    }\n\n                    // Get data name\n                    let name = {\n                        if child.iter_leaf_children_with_type(\"Data\").count() != 1 {\n                            return Err(PsyParseError::UnknownError(child.byte_offset()));\n                        }\n                        child.iter_leaf_children_with_type(\"Data\").nth(0).unwrap().1\n                    };\n\n                    // Get surface shader binding, if any.\n                    let surface_shader_name = if child\n                        .iter_leaf_children_with_type(\"SurfaceShaderBind\")\n                        .count()\n                        > 0\n                    {\n                        Some(\n                            child\n                                .iter_leaf_children_with_type(\"SurfaceShaderBind\")\n                                .nth(0)\n                                .unwrap()\n                                .1,\n                        )\n                    } else {\n                        None\n                    };\n\n                    // Get xforms\n                    let mut xforms = Vec::new();\n                    for (_, contents, _) in child.iter_leaf_children_with_type(\"Transform\") {\n                        xforms.push(parse_matrix(contents)?);\n                    }\n\n                    // Add instance\n                    if builder.name_exists(name) {\n                        builder.add_instance(name, surface_shader_name, Some(&xforms));\n                    } else {\n                        return Err(PsyParseError::InstancedMissingData(\n                            child.iter_leaf_children_with_type(\"Data\").nth(0).unwrap().2,\n                            \"Attempted to add \\\n                             instance for data with \\\n                             a name that doesn't \\\n                             exist.\",\n                            name.to_string(),\n                        ));\n                    }\n                }\n\n                // SurfaceShader\n                \"SurfaceShader\" => {\n                    if let DataTree::Internal {\n                        ident: Some(ident), ..\n                    } = *child\n                    {\n                        builder.add_surface_shader(ident, parse_surface_shader(arena, child)?);\n                    } else {\n                        // TODO: error condition of some kind, because no ident\n                        panic!(\n                            \"SurfaceShader encountered that was a leaf, but SurfaceShaders cannot \\\n                             be a leaf: {}\",\n                            child.byte_offset()\n                        );\n                    }\n                }\n\n                // MeshSurface\n                \"MeshSurface\" => {\n                    if let DataTree::Internal {\n                        ident: Some(ident), ..\n                    } = *child\n                    {\n                        builder.add_object(\n                            ident,\n                            Object::Surface(arena.alloc(parse_mesh_surface(arena, child)?)),\n                        );\n                    } else {\n                        // TODO: error condition of some kind, because no ident\n                        panic!(\n                            \"MeshSurface encountered that was a leaf, but MeshSurfaces cannot \\\n                             be a leaf: {}\",\n                            child.byte_offset()\n                        );\n                    }\n                }\n\n                // Sphere Light\n                \"SphereLight\" => {\n                    if let DataTree::Internal {\n                        ident: Some(ident), ..\n                    } = *child\n                    {\n                        builder.add_object(\n                            ident,\n                            Object::SurfaceLight(arena.alloc(parse_sphere_light(arena, child)?)),\n                        );\n                    } else {\n                        // No ident\n                        return Err(PsyParseError::UnknownError(child.byte_offset()));\n                    }\n                }\n\n                // Rectangle Light\n                \"RectangleLight\" => {\n                    if let DataTree::Internal {\n                        ident: Some(ident), ..\n                    } = *child\n                    {\n                        builder.add_object(\n                            ident,\n                            Object::SurfaceLight(arena.alloc(parse_rectangle_light(arena, child)?)),\n                        );\n                    } else {\n                        // No ident\n                        return Err(PsyParseError::UnknownError(child.byte_offset()));\n                    }\n                }\n\n                _ => {\n                    // TODO: some kind of error, because not a known type name\n                } // // Bilinear Patch\n                  // \"BilinearPatch\" => {\n                  //     assembly->add_object(child.name, parse_bilinear_patch(child));\n                  // }\n                  //\n                  // // Bicubic Patch\n                  // else if (child.type == \"BicubicPatch\") {\n                  //     assembly->add_object(child.name, parse_bicubic_patch(child));\n                  // }\n                  //\n                  // // Subdivision surface\n                  // else if (child.type == \"SubdivisionSurface\") {\n                  //     assembly->add_object(child.name, parse_subdivision_surface(child));\n                  // }\n                  //\n                  // // Sphere\n                  // else if (child.type == \"Sphere\") {\n                  //     assembly->add_object(child.name, parse_sphere(child));\n                  // }\n            }\n        }\n    } else {\n        return Err(PsyParseError::UnknownError(tree.byte_offset()));\n    }\n\n    return Ok(builder.build());\n}\n"
  },
  {
    "path": "src/parse/psy_light.rs",
    "content": "#![allow(dead_code)]\n\nuse std::result::Result;\n\nuse nom::{combinator::all_consuming, sequence::tuple, IResult};\n\nuse kioku::Arena;\n\nuse crate::{\n    light::{DistantDiskLight, RectangleLight, SphereLight},\n    math::Vector,\n};\n\nuse super::{\n    basics::ws_f32,\n    psy::{parse_color, PsyParseError},\n    DataTree,\n};\n\npub fn parse_distant_disk_light<'a>(\n    arena: &'a Arena,\n    tree: &'a DataTree,\n) -> Result<DistantDiskLight<'a>, PsyParseError> {\n    if let DataTree::Internal { ref children, .. } = *tree {\n        let mut radii = Vec::new();\n        let mut directions = Vec::new();\n        let mut colors = Vec::new();\n\n        // Parse\n        for child in children.iter() {\n            match *child {\n                // Radius\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Radius\" => {\n                    if let IResult::Ok((_, radius)) = all_consuming(ws_f32)(contents) {\n                        radii.push(radius);\n                    } else {\n                        // Found radius, but its contents is not in the right format\n                        return Err(PsyParseError::UnknownError(byte_offset));\n                    }\n                }\n\n                // Direction\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Direction\" => {\n                    if let IResult::Ok((_, direction)) =\n                        all_consuming(tuple((ws_f32, ws_f32, ws_f32)))(contents)\n                    {\n                        directions.push(Vector::new(direction.0, direction.1, direction.2));\n                    } else {\n                        // Found direction, but its contents is not in the right format\n                        return Err(PsyParseError::UnknownError(byte_offset));\n                    }\n                }\n\n                // Color\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Color\" => {\n                    if let Ok(color) = parse_color(contents) {\n                        colors.push(color);\n                    } else {\n                        // Found color, but its contents is not in the right format\n                        return Err(PsyParseError::UnknownError(byte_offset));\n                    }\n                }\n\n                _ => {}\n            }\n        }\n\n        return Ok(DistantDiskLight::new(arena, &radii, &directions, &colors));\n    } else {\n        return Err(PsyParseError::UnknownError(tree.byte_offset()));\n    }\n}\n\npub fn parse_sphere_light<'a>(\n    arena: &'a Arena,\n    tree: &'a DataTree,\n) -> Result<SphereLight<'a>, PsyParseError> {\n    if let DataTree::Internal { ref children, .. } = *tree {\n        let mut radii = Vec::new();\n        let mut colors = Vec::new();\n\n        // Parse\n        for child in children.iter() {\n            match *child {\n                // Radius\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Radius\" => {\n                    if let IResult::Ok((_, radius)) = all_consuming(ws_f32)(contents) {\n                        radii.push(radius);\n                    } else {\n                        // Found radius, but its contents is not in the right format\n                        return Err(PsyParseError::UnknownError(byte_offset));\n                    }\n                }\n\n                // Color\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Color\" => {\n                    if let Ok(color) = parse_color(contents) {\n                        colors.push(color);\n                    } else {\n                        // Found color, but its contents is not in the right format\n                        return Err(PsyParseError::UnknownError(byte_offset));\n                    }\n                }\n\n                _ => {}\n            }\n        }\n\n        return Ok(SphereLight::new(arena, &radii, &colors));\n    } else {\n        return Err(PsyParseError::UnknownError(tree.byte_offset()));\n    }\n}\n\npub fn parse_rectangle_light<'a>(\n    arena: &'a Arena,\n    tree: &'a DataTree,\n) -> Result<RectangleLight<'a>, PsyParseError> {\n    if let DataTree::Internal { ref children, .. } = *tree {\n        let mut dimensions = Vec::new();\n        let mut colors = Vec::new();\n\n        // Parse\n        for child in children.iter() {\n            match *child {\n                // Dimensions\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Dimensions\" => {\n                    if let IResult::Ok((_, radius)) =\n                        all_consuming(tuple((ws_f32, ws_f32)))(contents)\n                    {\n                        dimensions.push(radius);\n                    } else {\n                        // Found dimensions, but its contents is not in the right format\n                        return Err(PsyParseError::UnknownError(byte_offset));\n                    }\n                }\n\n                // Color\n                DataTree::Leaf {\n                    type_name,\n                    contents,\n                    byte_offset,\n                } if type_name == \"Color\" => {\n                    if let Ok(color) = parse_color(contents) {\n                        colors.push(color);\n                    } else {\n                        // Found color, but its contents is not in the right format\n                        return Err(PsyParseError::UnknownError(byte_offset));\n                    }\n                }\n\n                _ => {}\n            }\n        }\n\n        return Ok(RectangleLight::new(arena, &dimensions, &colors));\n    } else {\n        return Err(PsyParseError::UnknownError(tree.byte_offset()));\n    }\n}\n"
  },
  {
    "path": "src/parse/psy_mesh_surface.rs",
    "content": "#![allow(dead_code)]\n\nuse std::result::Result;\n\nuse nom::{sequence::tuple, IResult};\n\nuse kioku::Arena;\n\nuse crate::{\n    math::{Normal, Point},\n    surface::triangle_mesh::TriangleMesh,\n};\n\nuse super::{\n    basics::{ws_f32, ws_usize},\n    psy::PsyParseError,\n    DataTree,\n};\n\n// pub struct TriangleMesh {\n//    time_samples: usize,\n//    geo: Vec<(Point, Point, Point)>,\n//    indices: Vec<usize>,\n//    accel: BVH,\n// }\n\npub fn parse_mesh_surface<'a>(\n    arena: &'a Arena,\n    tree: &'a DataTree,\n) -> Result<TriangleMesh<'a>, PsyParseError> {\n    let mut verts = Vec::new(); // Vec of vecs, one for each time sample\n    let mut normals = Vec::new(); // Vec of vecs, on for each time sample\n    let mut face_vert_counts = Vec::new();\n    let mut face_vert_indices = Vec::new();\n\n    // TODO: make sure there are the right number of various children,\n    // and other validation.\n\n    // Get verts\n    for (_, mut text, _) in tree.iter_leaf_children_with_type(\"Vertices\") {\n        // Collect verts for this time sample\n        let mut tverts = Vec::new();\n        while let IResult::Ok((remaining, vert)) = tuple((ws_f32, ws_f32, ws_f32))(text) {\n            text = remaining;\n\n            tverts.push(Point::new(vert.0, vert.1, vert.2));\n        }\n        verts.push(tverts);\n    }\n\n    // Make sure all time samples have same vert count\n    let vert_count = verts[0].len();\n    for vs in &verts {\n        assert_eq!(vert_count, vs.len());\n    }\n\n    // Get normals, if they exist\n    for (_, mut text, _) in tree.iter_leaf_children_with_type(\"Normals\") {\n        // Collect normals for this time sample\n        let mut tnormals = Vec::new();\n        while let IResult::Ok((remaining, nor)) = tuple((ws_f32, ws_f32, ws_f32))(text) {\n            text = remaining;\n\n            tnormals.push(Normal::new(nor.0, nor.1, nor.2).normalized());\n        }\n        normals.push(tnormals);\n    }\n\n    // Make sure normal's time samples and vert count match the vertices\n    if !normals.is_empty() {\n        assert_eq!(normals.len(), verts.len());\n        for ns in &normals {\n            assert_eq!(vert_count, ns.len());\n        }\n    }\n\n    // Get face vert counts\n    if let Some((_, mut text, _)) = tree.iter_leaf_children_with_type(\"FaceVertCounts\").nth(0) {\n        while let IResult::Ok((remaining, count)) = ws_usize(text) {\n            text = remaining;\n\n            face_vert_counts.push(count);\n        }\n    }\n\n    // Get face vert indices\n    if let Some((_, mut text, _)) = tree.iter_leaf_children_with_type(\"FaceVertIndices\").nth(0) {\n        while let IResult::Ok((remaining, index)) = ws_usize(text) {\n            text = remaining;\n\n            face_vert_indices.push(index);\n        }\n    }\n\n    // Build triangle mesh\n    let mut tri_vert_indices = Vec::new();\n    let mut ii = 0;\n    for fvc in &face_vert_counts {\n        if *fvc >= 3 {\n            // Store the polygon, split up into triangles if >3 verts\n            let v1 = ii;\n            for vi in 0..(fvc - 2) {\n                tri_vert_indices.push((\n                    face_vert_indices[v1],\n                    face_vert_indices[v1 + vi + 1],\n                    face_vert_indices[v1 + vi + 2],\n                ));\n            }\n        } else {\n            // TODO: proper error\n            panic!(\"Cannot handle polygons with less than three vertices.\");\n        }\n\n        ii += *fvc;\n    }\n\n    Ok(TriangleMesh::from_verts_and_indices(\n        arena,\n        &verts,\n        &if normals.is_empty() {\n            None\n        } else {\n            Some(normals)\n        },\n        &tri_vert_indices,\n    ))\n}\n"
  },
  {
    "path": "src/parse/psy_surface_shader.rs",
    "content": "#![allow(dead_code)]\n\nuse std::result::Result;\n\nuse nom::{combinator::all_consuming, IResult};\n\nuse kioku::Arena;\n\nuse crate::shading::{SimpleSurfaceShader, SurfaceShader};\n\nuse super::{\n    basics::ws_f32,\n    psy::{parse_color, PsyParseError},\n    DataTree,\n};\n\n// pub struct TriangleMesh {\n//    time_samples: usize,\n//    geo: Vec<(Point, Point, Point)>,\n//    indices: Vec<usize>,\n//    accel: BVH,\n// }\n\npub fn parse_surface_shader<'a>(\n    arena: &'a Arena,\n    tree: &'a DataTree,\n) -> Result<&'a dyn SurfaceShader, PsyParseError> {\n    let type_name = if let Some((_, text, _)) = tree.iter_leaf_children_with_type(\"Type\").nth(0) {\n        text.trim()\n    } else {\n        return Err(PsyParseError::MissingNode(\n            tree.byte_offset(),\n            \"Expected a Type field in SurfaceShader.\",\n        ));\n    };\n\n    let shader = match type_name {\n        \"Lambert\" => {\n            let color = if let Some((_, contents, byte_offset)) =\n                tree.iter_leaf_children_with_type(\"Color\").nth(0)\n            {\n                if let Ok(color) = parse_color(contents) {\n                    color\n                } else {\n                    // Found color, but its contents is not in the right format\n                    return Err(PsyParseError::UnknownError(byte_offset));\n                }\n            } else {\n                return Err(PsyParseError::MissingNode(\n                    tree.byte_offset(),\n                    \"Expected a Color field in Lambert SurfaceShader.\",\n                ));\n            };\n\n            arena.alloc(SimpleSurfaceShader::Lambert { color: color })\n        }\n\n        \"GGX\" => {\n            // Color\n            let color = if let Some((_, contents, byte_offset)) =\n                tree.iter_leaf_children_with_type(\"Color\").nth(0)\n            {\n                if let Ok(color) = parse_color(contents) {\n                    color\n                } else {\n                    // Found color, but its contents is not in the right format\n                    return Err(PsyParseError::UnknownError(byte_offset));\n                }\n            } else {\n                return Err(PsyParseError::MissingNode(\n                    tree.byte_offset(),\n                    \"Expected a Color field in GTR SurfaceShader.\",\n                ));\n            };\n\n            // Roughness\n            let roughness = if let Some((_, contents, byte_offset)) =\n                tree.iter_leaf_children_with_type(\"Roughness\").nth(0)\n            {\n                if let IResult::Ok((_, roughness)) = all_consuming(ws_f32)(contents) {\n                    roughness\n                } else {\n                    return Err(PsyParseError::UnknownError(byte_offset));\n                }\n            } else {\n                return Err(PsyParseError::MissingNode(\n                    tree.byte_offset(),\n                    \"Expected a Roughness field in GTR SurfaceShader.\",\n                ));\n            };\n\n            // Fresnel\n            let fresnel = if let Some((_, contents, byte_offset)) =\n                tree.iter_leaf_children_with_type(\"Fresnel\").nth(0)\n            {\n                if let IResult::Ok((_, fresnel)) = all_consuming(ws_f32)(contents) {\n                    fresnel\n                } else {\n                    return Err(PsyParseError::UnknownError(byte_offset));\n                }\n            } else {\n                return Err(PsyParseError::MissingNode(\n                    tree.byte_offset(),\n                    \"Expected a Fresnel field in GTR SurfaceShader.\",\n                ));\n            };\n\n            arena.alloc(SimpleSurfaceShader::GGX {\n                color: color,\n                roughness: roughness,\n                fresnel: fresnel,\n            })\n        }\n\n        \"Emit\" => {\n            let color = if let Some((_, contents, byte_offset)) =\n                tree.iter_leaf_children_with_type(\"Color\").nth(0)\n            {\n                if let Ok(color) = parse_color(contents) {\n                    color\n                } else {\n                    // Found color, but its contents is not in the right format\n                    return Err(PsyParseError::UnknownError(byte_offset));\n                }\n            } else {\n                return Err(PsyParseError::MissingNode(\n                    tree.byte_offset(),\n                    \"Expected a Color field in Emit SurfaceShader.\",\n                ));\n            };\n\n            arena.alloc(SimpleSurfaceShader::Emit { color: color })\n        }\n\n        _ => unimplemented!(),\n    };\n\n    Ok(shader)\n}\n"
  },
  {
    "path": "src/ray.rs",
    "content": "#![allow(dead_code)]\n\nuse glam::BVec4A;\n\nuse crate::math::{Point, Transform, Vector};\n\ntype RayIndexType = u16;\ntype FlagType = u8;\nconst OCCLUSION_FLAG: FlagType = 1;\nconst DONE_FLAG: FlagType = 1 << 1;\n\n/// This is never used directly in ray tracing--it's only used as a convenience\n/// for filling the RayBatch structure.\n#[derive(Debug, Copy, Clone)]\npub struct Ray {\n    pub orig: Point,\n    pub dir: Vector,\n    pub time: f32,\n    pub wavelength: f32,\n    pub max_t: f32,\n}\n\n/// The hot (frequently accessed) parts of ray data.\n#[derive(Debug, Copy, Clone)]\nstruct RayHot {\n    orig_local: Point,     // Local-space ray origin\n    dir_inv_local: Vector, // Local-space 1.0/ray direction\n    max_t: f32,\n    time: f32,\n    flags: FlagType,\n}\n\n/// The cold (infrequently accessed) parts of ray data.\n#[derive(Debug, Copy, Clone)]\nstruct RayCold {\n    orig: Point, // World-space ray origin\n    dir: Vector, // World-space ray direction\n    wavelength: f32,\n}\n\n/// A batch of rays, separated into hot and cold parts.\n#[derive(Debug)]\npub struct RayBatch {\n    hot: Vec<RayHot>,\n    cold: Vec<RayCold>,\n}\n\nimpl RayBatch {\n    /// Creates a new empty ray batch.\n    pub fn new() -> RayBatch {\n        RayBatch {\n            hot: Vec::new(),\n            cold: Vec::new(),\n        }\n    }\n\n    /// Creates a new empty ray batch, with pre-allocated capacity for\n    /// `n` rays.\n    pub fn with_capacity(n: usize) -> RayBatch {\n        RayBatch {\n            hot: Vec::with_capacity(n),\n            cold: Vec::with_capacity(n),\n        }\n    }\n\n    pub fn push(&mut self, ray: Ray, is_occlusion: bool) {\n        self.hot.push(RayHot {\n            orig_local: ray.orig,   // Bogus, to place-hold.\n            dir_inv_local: ray.dir, // Bogus, to place-hold.\n            max_t: ray.max_t,\n            time: ray.time,\n            flags: if is_occlusion { OCCLUSION_FLAG } else { 0 },\n        });\n        self.cold.push(RayCold {\n            orig: ray.orig,\n            dir: ray.dir,\n            wavelength: ray.wavelength,\n        });\n    }\n\n    pub fn swap(&mut self, a: usize, b: usize) {\n        self.hot.swap(a, b);\n        self.cold.swap(a, b);\n    }\n\n    pub fn set_from_ray(&mut self, ray: &Ray, is_occlusion: bool, idx: usize) {\n        self.hot[idx].orig_local = ray.orig;\n        self.hot[idx].dir_inv_local = Vector {\n            co: ray.dir.co.recip(),\n        };\n        self.hot[idx].max_t = ray.max_t;\n        self.hot[idx].time = ray.time;\n        self.hot[idx].flags = if is_occlusion { OCCLUSION_FLAG } else { 0 };\n\n        self.cold[idx].orig = ray.orig;\n        self.cold[idx].dir = ray.dir;\n        self.cold[idx].wavelength = ray.wavelength;\n    }\n\n    pub fn truncate(&mut self, len: usize) {\n        self.hot.truncate(len);\n        self.cold.truncate(len);\n    }\n\n    /// Clear all rays, settings the size of the batch back to zero.\n    ///\n    /// Capacity is maintained.\n    pub fn clear(&mut self) {\n        self.hot.clear();\n        self.cold.clear();\n    }\n\n    pub fn len(&self) -> usize {\n        self.hot.len()\n    }\n\n    /// Updates the accel data of the given ray (at index `idx`) with the\n    /// given world-to-local-space transform matrix.\n    ///\n    /// This should be called when entering (and exiting) traversal of a\n    /// new transform space.\n    pub fn update_local(&mut self, idx: usize, xform: &Transform) {\n        self.hot[idx].orig_local = self.cold[idx].orig * *xform;\n        self.hot[idx].dir_inv_local = Vector {\n            co: (self.cold[idx].dir * *xform).co.recip(),\n        };\n    }\n\n    //==========================================================\n    // Data access\n\n    #[inline(always)]\n    pub fn orig(&self, idx: usize) -> Point {\n        self.cold[idx].orig\n    }\n\n    #[inline(always)]\n    pub fn dir(&self, idx: usize) -> Vector {\n        self.cold[idx].dir\n    }\n\n    #[inline(always)]\n    pub fn orig_local(&self, idx: usize) -> Point {\n        self.hot[idx].orig_local\n    }\n\n    #[inline(always)]\n    pub fn dir_inv_local(&self, idx: usize) -> Vector {\n        self.hot[idx].dir_inv_local\n    }\n\n    #[inline(always)]\n    pub fn time(&self, idx: usize) -> f32 {\n        self.hot[idx].time\n    }\n\n    #[inline(always)]\n    pub fn max_t(&self, idx: usize) -> f32 {\n        self.hot[idx].max_t\n    }\n\n    #[inline(always)]\n    pub fn set_max_t(&mut self, idx: usize, new_max_t: f32) {\n        self.hot[idx].max_t = new_max_t;\n    }\n\n    #[inline(always)]\n    pub fn wavelength(&self, idx: usize) -> f32 {\n        self.cold[idx].wavelength\n    }\n\n    /// Returns whether the given ray (at index `idx`) is an occlusion ray.\n    #[inline(always)]\n    pub fn is_occlusion(&self, idx: usize) -> bool {\n        (self.hot[idx].flags & OCCLUSION_FLAG) != 0\n    }\n\n    /// Returns whether the given ray (at index `idx`) has finished traversal.\n    #[inline(always)]\n    pub fn is_done(&self, idx: usize) -> bool {\n        (self.hot[idx].flags & DONE_FLAG) != 0\n    }\n\n    /// Marks the given ray (at index `idx`) as an occlusion ray.\n    #[inline(always)]\n    pub fn mark_occlusion(&mut self, idx: usize) {\n        self.hot[idx].flags |= OCCLUSION_FLAG\n    }\n\n    /// Marks the given ray (at index `idx`) as having finished traversal.\n    #[inline(always)]\n    pub fn mark_done(&mut self, idx: usize) {\n        self.hot[idx].flags |= DONE_FLAG\n    }\n}\n\n/// A structure used for tracking traversal of a ray batch through a scene.\n#[derive(Debug)]\npub struct RayStack {\n    lanes: Vec<Lane>,\n    tasks: Vec<RayTask>,\n}\n\nimpl RayStack {\n    pub fn new() -> RayStack {\n        RayStack {\n            lanes: Vec::new(),\n            tasks: Vec::new(),\n        }\n    }\n\n    /// Returns whether the stack is empty of tasks or not.\n    pub fn is_empty(&self) -> bool {\n        self.tasks.is_empty()\n    }\n\n    /// Makes sure there are at least `count` lanes.\n    pub fn ensure_lane_count(&mut self, count: usize) {\n        while self.lanes.len() < count {\n            self.lanes.push(Lane {\n                idxs: Vec::new(),\n                end_len: 0,\n            })\n        }\n    }\n\n    pub fn ray_count_in_next_task(&self) -> usize {\n        let task = self.tasks.last().unwrap();\n        let end = self.lanes[task.lane].end_len;\n        end - task.start_idx\n    }\n\n    pub fn next_task_ray_idx(&self, i: usize) -> usize {\n        let task = self.tasks.last().unwrap();\n        let i = i + task.start_idx;\n        debug_assert!(i < self.lanes[task.lane].end_len);\n        self.lanes[task.lane].idxs[i] as usize\n    }\n\n    /// Clears the lanes and tasks of the RayStack.\n    ///\n    /// Note: this is (importantly) different than calling clear individually\n    /// on the `lanes` and `tasks` members.  Specifically, we don't want to\n    /// clear `lanes` itself, as that would also free all the memory of the\n    /// individual lanes.  Instead, we want to iterate over the individual\n    /// lanes and clear them, but leave `lanes` itself untouched.\n    pub fn clear(&mut self) {\n        for lane in self.lanes.iter_mut() {\n            lane.idxs.clear();\n            lane.end_len = 0;\n        }\n\n        self.tasks.clear();\n    }\n\n    /// Pushes the given ray index onto the end of the specified lane.\n    pub fn push_ray_index(&mut self, ray_idx: usize, lane: usize) {\n        assert!(self.lanes.len() > lane);\n        self.lanes[lane].idxs.push(ray_idx as RayIndexType);\n    }\n\n    /// Pushes any excess indices on the given lane to a new task on the\n    /// task stack.\n    ///\n    /// Returns whether a task was pushed or not.  No task will be pushed\n    /// if there are no excess indices on the end of the lane.\n    pub fn push_lane_to_task(&mut self, lane_idx: usize) -> bool {\n        if self.lanes[lane_idx].end_len < self.lanes[lane_idx].idxs.len() {\n            self.tasks.push(RayTask {\n                lane: lane_idx,\n                start_idx: self.lanes[lane_idx].end_len,\n            });\n            self.lanes[lane_idx].end_len = self.lanes[lane_idx].idxs.len();\n            true\n        } else {\n            false\n        }\n    }\n\n    /// Takes the given list of lane indices, and pushes any excess indices on\n    /// the end of each into a new task, in the order provided.\n    pub fn push_lanes_to_tasks(&mut self, lane_idxs: &[usize]) {\n        for &l in lane_idxs {\n            self.push_lane_to_task(l);\n        }\n    }\n\n    pub fn duplicate_next_task(&mut self) {\n        let task = self.tasks.last().unwrap();\n        let l = task.lane;\n        let start = task.start_idx;\n        let end = self.lanes[l].end_len;\n\n        // Extend the indices vector\n        self.lanes[l].idxs.reserve(end - start);\n        let old_len = self.lanes[l].idxs.len();\n        let new_len = old_len + end - start;\n        unsafe {\n            self.lanes[l].idxs.set_len(new_len);\n        }\n\n        // Copy elements\n        copy_in_place::copy_in_place(&mut self.lanes[l].idxs, start..end, end);\n\n        // Push the new task onto the stack\n        self.tasks.push(RayTask {\n            lane: l,\n            start_idx: end,\n        });\n\n        self.lanes[l].end_len = self.lanes[l].idxs.len();\n    }\n\n    // Pops the next task off the stack.\n    pub fn pop_task(&mut self) {\n        let task = self.tasks.pop().unwrap();\n        self.lanes[task.lane].end_len = task.start_idx;\n        self.lanes[task.lane].idxs.truncate(task.start_idx);\n    }\n\n    // Executes a task without popping it from the task stack.\n    pub fn do_next_task<F>(&mut self, mut handle_ray: F)\n    where\n        F: FnMut(usize),\n    {\n        let task = self.tasks.last().unwrap();\n        let task_range = (task.start_idx, self.lanes[task.lane].end_len);\n\n        // Execute task.\n        for i in task_range.0..task_range.1 {\n            let ray_idx = self.lanes[task.lane].idxs[i];\n            handle_ray(ray_idx as usize);\n        }\n    }\n\n    /// Pops the next task off the stack, and executes the provided closure for\n    /// each ray index in the task.\n    #[inline(always)]\n    pub fn pop_do_next_task<F>(&mut self, handle_ray: F)\n    where\n        F: FnMut(usize),\n    {\n        self.do_next_task(handle_ray);\n        self.pop_task();\n    }\n\n    /// Pops the next task off the stack, executes the provided closure for\n    /// each ray index in the task, and pushes the ray indices back onto the\n    /// indicated lanes.\n    pub fn pop_do_next_task_and_push_rays<F>(&mut self, output_lane_count: usize, mut handle_ray: F)\n    where\n        F: FnMut(usize) -> BVec4A,\n    {\n        // Pop the task and do necessary bookkeeping.\n        let task = self.tasks.pop().unwrap();\n        let task_range = (task.start_idx, self.lanes[task.lane].end_len);\n        self.lanes[task.lane].end_len = task.start_idx;\n\n        // SAFETY: this is probably evil, and depends on behavior of Vec that\n        // are not actually promised.  But we're essentially truncating the lane\n        // to the start of our task range, but will continue to access it's\n        // elements beyond that range via `get_unchecked()` below.  Because the\n        // memory is not freed nor altered, this is safe.  However, again, the\n        // Vec apis don't promise this behavior.  So:\n        //\n        // TODO: build a slightly different lane abstraction to get this same\n        // efficiency without depending on implicit Vec behavior.\n        unsafe {\n            self.lanes[task.lane].idxs.set_len(task.start_idx);\n        }\n\n        // Execute task.\n        for i in task_range.0..task_range.1 {\n            let ray_idx = *unsafe { self.lanes[task.lane].idxs.get_unchecked(i) };\n            let push_mask = handle_ray(ray_idx as usize).bitmask();\n            for l in 0..output_lane_count {\n                if (push_mask & (1 << l)) != 0 {\n                    self.lanes[l as usize].idxs.push(ray_idx);\n                }\n            }\n        }\n    }\n}\n\n/// A lane within a RayStack.\n#[derive(Debug)]\nstruct Lane {\n    idxs: Vec<RayIndexType>,\n    end_len: usize,\n}\n\n/// A task within a RayStack.\n//\n// Specifies the lane that the relevant ray pointers are in, and the\n// starting index within that lane.  The relevant pointers are always\n// `&[start_idx..]` within the given lane.\n#[derive(Debug)]\nstruct RayTask {\n    lane: usize,\n    start_idx: usize,\n}\n"
  },
  {
    "path": "src/renderer.rs",
    "content": "use std::{\n    cell::Cell,\n    cmp,\n    cmp::min,\n    io::{self, Write},\n    sync::{Mutex, RwLock},\n};\n\nuse crossbeam::sync::MsQueue;\nuse scoped_threadpool::Pool;\n\nuse glam::Vec4;\n\nuse crate::{\n    accel::ACCEL_NODE_RAY_TESTS,\n    color::{map_0_1_to_wavelength, SpectralSample, XYZ},\n    fp_utils::robust_ray_origin,\n    hash::hash_u32,\n    hilbert,\n    image::Image,\n    math::probit,\n    mis::power_heuristic,\n    ray::{Ray, RayBatch},\n    scene::{Scene, SceneLightSample},\n    surface,\n    timer::Timer,\n    tracer::Tracer,\n    transform_stack::TransformStack,\n};\n\n#[derive(Debug)]\npub struct Renderer<'a> {\n    pub output_file: String,\n    pub resolution: (usize, usize),\n    pub spp: usize,\n    pub seed: u32,\n    pub scene: Scene<'a>,\n}\n\n#[derive(Debug, Copy, Clone)]\npub struct RenderStats {\n    pub trace_time: f64,\n    pub accel_node_visits: u64,\n    pub ray_count: u64,\n    pub initial_ray_generation_time: f64,\n    pub ray_generation_time: f64,\n    pub sample_writing_time: f64,\n    pub total_time: f64,\n}\n\nimpl RenderStats {\n    fn new() -> RenderStats {\n        RenderStats {\n            trace_time: 0.0,\n            accel_node_visits: 0,\n            ray_count: 0,\n            initial_ray_generation_time: 0.0,\n            ray_generation_time: 0.0,\n            sample_writing_time: 0.0,\n            total_time: 0.0,\n        }\n    }\n\n    fn collect(&mut self, other: RenderStats) {\n        self.trace_time += other.trace_time;\n        self.accel_node_visits += other.accel_node_visits;\n        self.ray_count += other.ray_count;\n        self.initial_ray_generation_time += other.initial_ray_generation_time;\n        self.ray_generation_time += other.ray_generation_time;\n        self.sample_writing_time += other.sample_writing_time;\n        self.total_time += other.total_time;\n    }\n}\n\nimpl<'a> Renderer<'a> {\n    pub fn render(\n        &self,\n        max_samples_per_bucket: u32,\n        crop: Option<(u32, u32, u32, u32)>,\n        thread_count: u32,\n        do_blender_output: bool,\n    ) -> (Image, RenderStats) {\n        let mut tpool = Pool::new(thread_count);\n\n        let image = Image::new(self.resolution.0, self.resolution.1);\n        let (img_width, img_height) = (image.width(), image.height());\n\n        let all_jobs_queued = RwLock::new(false);\n\n        let collective_stats = RwLock::new(RenderStats::new());\n\n        // Set up job queue\n        let job_queue = MsQueue::new();\n\n        // For printing render progress\n        let pixels_rendered = Mutex::new(Cell::new(0));\n\n        // Calculate dimensions and coordinates of what we're rendering.  This\n        // accounts for cropping.\n        let (width, height, start_x, start_y) = if let Some((x1, y1, x2, y2)) = crop {\n            let x1 = min(x1 as usize, img_width - 1);\n            let y1 = min(y1 as usize, img_height - 1);\n            let x2 = min(x2 as usize, img_width - 1);\n            let y2 = min(y2 as usize, img_height - 1);\n            (x2 - x1 + 1, y2 - y1 + 1, x1, y1)\n        } else {\n            (img_width, img_height, 0, 0)\n        };\n\n        // Render\n        tpool.scoped(|scope| {\n            // Spawn worker tasks\n            for _ in 0..thread_count {\n                let jq = &job_queue;\n                let ajq = &all_jobs_queued;\n                let img = &image;\n                let pixrenref = &pixels_rendered;\n                let cstats = &collective_stats;\n                scope.execute(move || {\n                    self.render_job(\n                        jq,\n                        ajq,\n                        img,\n                        width * height,\n                        pixrenref,\n                        cstats,\n                        do_blender_output,\n                    )\n                });\n            }\n\n            // Print initial 0.00% progress\n            print!(\"0.00%\");\n            let _ = io::stdout().flush();\n\n            // Determine bucket size based on the per-thread maximum number of samples to\n            // calculate at a time.\n            let (bucket_w, bucket_h) = {\n                let target_pixels_per_bucket = max_samples_per_bucket as f64 / self.spp as f64;\n                let target_bucket_dim = if target_pixels_per_bucket.sqrt() < 1.0 {\n                    1usize\n                } else {\n                    target_pixels_per_bucket.sqrt() as usize\n                };\n\n                (target_bucket_dim, target_bucket_dim)\n            };\n\n            // Populate job queue\n            let bucket_n = {\n                let bucket_count_x = ((width / bucket_w) + 1) as u32;\n                let bucket_count_y = ((height / bucket_h) + 1) as u32;\n                let larger = cmp::max(bucket_count_x, bucket_count_y);\n                let pow2 = larger.next_power_of_two();\n                pow2 * pow2\n            };\n            for hilbert_d in 0..bucket_n {\n                let (bx, by) = hilbert::d2xy(hilbert_d);\n\n                let x = bx as usize * bucket_w;\n                let y = by as usize * bucket_h;\n                let w = if width >= x {\n                    min(bucket_w, width - x)\n                } else {\n                    bucket_w\n                };\n                let h = if height >= y {\n                    min(bucket_h, height - y)\n                } else {\n                    bucket_h\n                };\n                if x < width && y < height && w > 0 && h > 0 {\n                    job_queue.push(BucketJob {\n                        x: (start_x + x) as u32,\n                        y: (start_y + y) as u32,\n                        w: w as u32,\n                        h: h as u32,\n                    });\n                }\n            }\n\n            // Mark done queuing jobs\n            *all_jobs_queued.write().unwrap() = true;\n        });\n\n        // Clear percentage progress print\n        print!(\"\\r                \\r\",);\n\n        // Return the rendered image and stats\n        return (image, *collective_stats.read().unwrap());\n    }\n\n    /// Waits for buckets in the job queue to render and renders them when available.\n    fn render_job(\n        &self,\n        job_queue: &MsQueue<BucketJob>,\n        all_jobs_queued: &RwLock<bool>,\n        image: &Image,\n        total_pixels: usize,\n        pixels_rendered: &Mutex<Cell<usize>>,\n        collected_stats: &RwLock<RenderStats>,\n        do_blender_output: bool,\n    ) {\n        let mut stats = RenderStats::new();\n        let mut timer = Timer::new();\n        let mut total_timer = Timer::new();\n\n        let mut paths = Vec::new();\n        let mut rays = RayBatch::new();\n        let mut tracer = Tracer::from_assembly(&self.scene.root);\n        let mut xform_stack = TransformStack::new();\n\n        // Pre-calculate some useful values related to the image plane\n        let cmpx = 1.0 / self.resolution.0 as f32;\n        let cmpy = 1.0 / self.resolution.1 as f32;\n        let min_x = -1.0;\n        let max_x = 1.0;\n        let min_y = -(self.resolution.1 as f32 / self.resolution.0 as f32);\n        let max_y = self.resolution.1 as f32 / self.resolution.0 as f32;\n        let x_extent = max_x - min_x;\n        let y_extent = max_y - min_y;\n\n        // Render\n        'render_loop: loop {\n            paths.clear();\n            rays.clear();\n\n            // Get bucket, or exit if no more jobs left\n            let bucket: BucketJob;\n            loop {\n                if let Some(b) = job_queue.try_pop() {\n                    bucket = b;\n                    break;\n                } else if *all_jobs_queued.read().unwrap() {\n                    break 'render_loop;\n                }\n            }\n\n            timer.tick();\n            // Generate light paths and initial rays\n            for y in bucket.y..(bucket.y + bucket.h) {\n                for x in bucket.x..(bucket.x + bucket.w) {\n                    for si in 0..self.spp {\n                        // Raw sample numbers.\n                        let (d0, d1, d2, d3) = get_sample_4d(si as u32, 0, (x, y), self.seed);\n                        let (d4, _, _, _) = get_sample_4d(si as u32, 1, (x, y), self.seed);\n\n                        // Calculate image plane x and y coordinates\n                        let (img_x, img_y) = {\n                            let filter_x = probit(d3, 2.0 / 6.0) + 0.5;\n                            let filter_y = probit(d4, 2.0 / 6.0) + 0.5;\n                            let samp_x = (filter_x + x as f32) * cmpx;\n                            let samp_y = (filter_y + y as f32) * cmpy;\n                            ((samp_x - 0.5) * x_extent, (0.5 - samp_y) * y_extent)\n                        };\n\n                        // Create the light path and initial ray for this sample\n                        let (path, ray) = LightPath::new(\n                            &self.scene,\n                            self.seed,\n                            (x, y),\n                            (img_x, img_y),\n                            (d1, d2),\n                            d0,\n                            map_0_1_to_wavelength(golden_ratio_sample(\n                                si as u32,\n                                hash_u32((x << 16) ^ y, self.seed),\n                            )),\n                            si as u32,\n                        );\n                        paths.push(path);\n                        rays.push(ray, false);\n                    }\n                }\n            }\n            stats.initial_ray_generation_time += timer.tick() as f64;\n\n            // Trace the paths!\n            let mut pi = paths.len();\n            while pi > 0 {\n                // Test rays against scene\n                let isects = tracer.trace(&mut rays);\n                stats.trace_time += timer.tick() as f64;\n\n                // Determine next rays to shoot based on result\n                let mut new_end = 0;\n                for i in 0..pi {\n                    if paths[i].next(&mut xform_stack, &self.scene, &isects[i], &mut rays, i) {\n                        paths.swap(new_end, i);\n                        rays.swap(new_end, i);\n                        new_end += 1;\n                    }\n                }\n                rays.truncate(new_end);\n                pi = new_end;\n                stats.ray_generation_time += timer.tick() as f64;\n            }\n\n            {\n                // Calculate color based on ray hits and save to image\n                let min = (bucket.x, bucket.y);\n                let max = (bucket.x + bucket.w, bucket.y + bucket.h);\n                let mut img_bucket = image.get_bucket(min, max);\n                for path in &paths {\n                    let path_col = SpectralSample::from_parts(path.color, path.wavelength);\n                    let mut col = img_bucket.get(path.pixel_co.0, path.pixel_co.1);\n                    col += XYZ::from_spectral_sample(&path_col) / self.spp as f32;\n                    img_bucket.set(path.pixel_co.0, path.pixel_co.1, col);\n                }\n                stats.sample_writing_time += timer.tick() as f64;\n\n                // Pre-calculate base64 encoding if needed\n                let base64_enc = if do_blender_output {\n                    use crate::color::xyz_to_rec709_e;\n                    Some(img_bucket.rgba_base64(xyz_to_rec709_e))\n                } else {\n                    None\n                };\n\n                // Print render progress, and image data if doing blender output\n                let guard = pixels_rendered.lock().unwrap();\n                let mut pr = (*guard).get();\n                let percentage_old = pr as f64 / total_pixels as f64 * 100.0;\n\n                pr += bucket.w as usize * bucket.h as usize;\n                (*guard).set(pr);\n                let percentage_new = pr as f64 / total_pixels as f64 * 100.0;\n\n                let old_string = format!(\"{:.2}%\", percentage_old);\n                let new_string = format!(\"{:.2}%\", percentage_new);\n\n                if let Some(bucket_data) = base64_enc {\n                    // If doing Blender output\n                    println!(\"DIV\");\n                    println!(\"{}\", new_string);\n                    println!(\"{} {} {} {}\", min.0, min.1, max.0, max.1);\n                    println!(\"{}\", bucket_data);\n                    println!(\"BUCKET_END\");\n                    println!(\"DIV\");\n                } else {\n                    // If doing console output\n                    if new_string != old_string {\n                        print!(\"\\r{}\", new_string);\n                    }\n                }\n                let _ = io::stdout().flush();\n            }\n        }\n\n        stats.total_time += total_timer.tick() as f64;\n        stats.ray_count = tracer.rays_traced();\n        ACCEL_NODE_RAY_TESTS.with(|anv| {\n            stats.accel_node_visits = anv.get();\n            anv.set(0);\n        });\n\n        // Collect stats\n        collected_stats.write().unwrap().collect(stats);\n    }\n}\n\n#[derive(Debug)]\nenum LightPathEvent {\n    CameraRay,\n    BounceRay,\n    ShadowRay,\n}\n\n#[derive(Debug)]\npub struct LightPath {\n    event: LightPathEvent,\n    bounce_count: u32,\n\n    sampling_seed: u32,\n    pixel_co: (u32, u32),\n    sample_number: u32, // Which sample in the LDS sequence this is.\n    dim_offset: u32,\n    time: f32,\n    wavelength: f32,\n\n    next_bounce_ray: Option<Ray>,\n    next_attenuation_fac: Vec4,\n\n    closure_sample_pdf: f32,\n    light_attenuation: Vec4,\n    pending_color_addition: Vec4,\n    color: Vec4,\n}\n\n#[allow(clippy::new_ret_no_self)]\nimpl LightPath {\n    fn new(\n        scene: &Scene,\n        sampling_seed: u32,\n        pixel_co: (u32, u32),\n        image_plane_co: (f32, f32),\n        lens_uv: (f32, f32),\n        time: f32,\n        wavelength: f32,\n        sample_number: u32,\n    ) -> (LightPath, Ray) {\n        (\n            LightPath {\n                event: LightPathEvent::CameraRay,\n                bounce_count: 0,\n\n                sampling_seed: sampling_seed ^ 0x40d4682b,\n                pixel_co: pixel_co,\n                sample_number: sample_number,\n                dim_offset: 0,\n                time: time,\n                wavelength: wavelength,\n\n                next_bounce_ray: None,\n                next_attenuation_fac: Vec4::splat(1.0),\n\n                closure_sample_pdf: 1.0,\n                light_attenuation: Vec4::splat(1.0),\n                pending_color_addition: Vec4::splat(0.0),\n                color: Vec4::splat(0.0),\n            },\n            scene.camera.generate_ray(\n                image_plane_co.0,\n                image_plane_co.1,\n                time,\n                wavelength,\n                lens_uv.0,\n                lens_uv.1,\n            ),\n        )\n    }\n\n    fn next_lds_sequence(&mut self) {\n        self.dim_offset = 0;\n        self.sampling_seed += 1;\n    }\n\n    fn next_lds_samp(&mut self) -> (f32, f32, f32, f32) {\n        let dimension = self.dim_offset;\n        self.dim_offset += 1;\n        get_sample_4d(\n            self.sample_number,\n            dimension,\n            self.pixel_co,\n            self.sampling_seed,\n        )\n    }\n\n    fn next(\n        &mut self,\n        xform_stack: &mut TransformStack,\n        scene: &Scene,\n        isect: &surface::SurfaceIntersection,\n        rays: &mut RayBatch,\n        ray_idx: usize,\n    ) -> bool {\n        match self.event {\n            //--------------------------------------------------------------------\n            // Result of Camera or bounce ray, prepare next bounce and light rays\n            LightPathEvent::CameraRay | LightPathEvent::BounceRay => {\n                if let surface::SurfaceIntersection::Hit {\n                    intersection_data: ref idata,\n                    ref closure,\n                } = *isect\n                {\n                    // Hit something!  Do the stuff\n\n                    // If it's an emission closure, handle specially:\n                    // - Collect light from the emission.\n                    // - Terminate the path.\n                    use crate::shading::surface_closure::SurfaceClosure;\n                    if let SurfaceClosure::Emit(color) = *closure {\n                        let color = color.to_spectral_sample(self.wavelength).e;\n                        if let LightPathEvent::CameraRay = self.event {\n                            self.color += color;\n                        } else {\n                            let mis_pdf =\n                                power_heuristic(self.closure_sample_pdf, idata.sample_pdf);\n                            self.color += color * self.light_attenuation / mis_pdf;\n                        };\n\n                        return false;\n                    }\n\n                    // Roll the previous closure pdf into the attenauation\n                    self.light_attenuation /= self.closure_sample_pdf;\n\n                    // Prepare light ray\n                    self.next_lds_sequence();\n                    let (light_n, d2, d3, d4) = self.next_lds_samp();\n                    let light_uvw = (d2, d3, d4);\n                    xform_stack.clear();\n                    let light_info = scene.sample_lights(\n                        xform_stack,\n                        light_n,\n                        light_uvw,\n                        self.wavelength,\n                        self.time,\n                        isect,\n                    );\n                    let found_light = if light_info.is_none()\n                        || light_info.pdf() <= 0.0\n                        || light_info.selection_pdf() <= 0.0\n                    {\n                        false\n                    } else {\n                        let light_pdf = light_info.pdf();\n                        let light_sel_pdf = light_info.selection_pdf();\n\n                        // Calculate the shadow ray and surface closure stuff\n                        let (attenuation, closure_pdf, shadow_ray) = match light_info {\n                            SceneLightSample::None => unreachable!(),\n\n                            // Distant light\n                            SceneLightSample::Distant { direction, .. } => {\n                                let (attenuation, closure_pdf) = closure.evaluate(\n                                    rays.dir(ray_idx),\n                                    direction,\n                                    idata.nor,\n                                    idata.nor_g,\n                                    self.wavelength,\n                                );\n                                let shadow_ray = {\n                                    // Calculate the shadow ray for testing if the light is\n                                    // in shadow or not.\n                                    let offset_pos = robust_ray_origin(\n                                        idata.pos,\n                                        idata.pos_err,\n                                        idata.nor_g.normalized(),\n                                        direction,\n                                    );\n                                    Ray {\n                                        orig: offset_pos,\n                                        dir: direction,\n                                        time: self.time,\n                                        wavelength: self.wavelength,\n                                        max_t: std::f32::INFINITY,\n                                    }\n                                };\n                                (attenuation, closure_pdf, shadow_ray)\n                            }\n\n                            // Surface light\n                            SceneLightSample::Surface { sample_geo, .. } => {\n                                let dir = sample_geo.0 - idata.pos;\n                                let (attenuation, closure_pdf) = closure.evaluate(\n                                    rays.dir(ray_idx),\n                                    dir,\n                                    idata.nor,\n                                    idata.nor_g,\n                                    self.wavelength,\n                                );\n                                let shadow_ray = {\n                                    // Calculate the shadow ray for testing if the light is\n                                    // in shadow or not.\n                                    let offset_pos = robust_ray_origin(\n                                        idata.pos,\n                                        idata.pos_err,\n                                        idata.nor_g.normalized(),\n                                        dir,\n                                    );\n                                    let offset_end = robust_ray_origin(\n                                        sample_geo.0,\n                                        sample_geo.2,\n                                        sample_geo.1.normalized(),\n                                        -dir,\n                                    );\n                                    Ray {\n                                        orig: offset_pos,\n                                        dir: offset_end - offset_pos,\n                                        time: self.time,\n                                        wavelength: self.wavelength,\n                                        max_t: 1.0,\n                                    }\n                                };\n                                (attenuation, closure_pdf, shadow_ray)\n                            }\n                        };\n\n                        // If there's any possible contribution, set up for a\n                        // light ray.\n                        if attenuation.e.max_element() <= 0.0 {\n                            false\n                        } else {\n                            // Calculate and store the light that will be contributed\n                            // to the film plane if the light is not in shadow.\n                            let light_mis_pdf = power_heuristic(light_pdf, closure_pdf);\n                            self.pending_color_addition =\n                                light_info.color().e * attenuation.e * self.light_attenuation\n                                    / (light_mis_pdf * light_sel_pdf);\n\n                            rays.set_from_ray(&shadow_ray, true, ray_idx);\n\n                            true\n                        }\n                    };\n\n                    // Prepare bounce ray\n                    let do_bounce = if self.bounce_count < 2 {\n                        self.bounce_count += 1;\n\n                        // Sample closure\n                        let (dir, filter, pdf) = {\n                            self.next_lds_sequence();\n                            let (u, v, _, _) = self.next_lds_samp();\n                            closure.sample(\n                                idata.incoming,\n                                idata.nor,\n                                idata.nor_g,\n                                (u, v),\n                                self.wavelength,\n                            )\n                        };\n\n                        // Check if pdf is zero, to avoid NaN's.\n                        if (pdf > 0.0) && (filter.e.max_element() > 0.0) {\n                            // Account for the additional light attenuation from\n                            // this bounce\n                            self.next_attenuation_fac = filter.e;\n                            self.closure_sample_pdf = pdf;\n\n                            // Calculate the ray for this bounce\n                            let offset_pos = robust_ray_origin(\n                                idata.pos,\n                                idata.pos_err,\n                                idata.nor_g.normalized(),\n                                dir,\n                            );\n                            self.next_bounce_ray = Some(Ray {\n                                orig: offset_pos,\n                                dir: dir,\n                                time: self.time,\n                                wavelength: self.wavelength,\n                                max_t: std::f32::INFINITY,\n                            });\n\n                            true\n                        } else {\n                            false\n                        }\n                    } else {\n                        self.next_bounce_ray = None;\n                        false\n                    };\n\n                    // Book keeping for next event\n                    if found_light {\n                        self.event = LightPathEvent::ShadowRay;\n                        return true;\n                    } else if do_bounce {\n                        rays.set_from_ray(&self.next_bounce_ray.unwrap(), false, ray_idx);\n                        self.event = LightPathEvent::BounceRay;\n                        self.light_attenuation *= self.next_attenuation_fac;\n                        return true;\n                    } else {\n                        return false;\n                    }\n                } else {\n                    // Didn't hit anything, so background color\n                    self.color += scene\n                        .world\n                        .background_color\n                        .to_spectral_sample(self.wavelength)\n                        .e\n                        * self.light_attenuation\n                        / self.closure_sample_pdf;\n                    return false;\n                }\n            }\n\n            //--------------------------------------------------------------------\n            // Result of shadow ray from sampling a light\n            LightPathEvent::ShadowRay => {\n                // If the light was not in shadow, add it's light to the film\n                // plane.\n                if let surface::SurfaceIntersection::Miss = *isect {\n                    self.color += self.pending_color_addition;\n                }\n\n                // Set up for the next bounce, if any\n                if let Some(ref nbr) = self.next_bounce_ray {\n                    rays.set_from_ray(nbr, false, ray_idx);\n                    self.light_attenuation *= self.next_attenuation_fac;\n                    self.event = LightPathEvent::BounceRay;\n                    return true;\n                } else {\n                    return false;\n                }\n            }\n        }\n    }\n}\n\n/// Gets a sample, using LDS samples for lower dimensions,\n/// and switching to random samples at higher dimensions where\n/// LDS samples aren't available.\n#[inline(always)]\nfn get_sample_4d(\n    i: u32,\n    dimension_set: u32,\n    pixel_co: (u32, u32),\n    seed: u32,\n) -> (f32, f32, f32, f32) {\n    // A unique seed for every pixel coordinate up to a resolution of\n    // 65536 x 65536.  Also incorperating the seed.\n    let seed = pixel_co.0 ^ (pixel_co.1 << 16) ^ seed.wrapping_mul(0x736caf6f);\n\n    match dimension_set {\n        ds if ds < sobol_burley::NUM_DIMENSION_SETS_4D as u32 => {\n            // Sobol sampling.\n            let n4 = sobol_burley::sample_4d(i, ds, seed);\n            (n4[0], n4[1], n4[2], n4[3])\n        }\n        ds => {\n            // Random sampling.\n            use crate::hash::hash_u32_to_f32;\n            (\n                hash_u32_to_f32((ds * 4 + 0) ^ (i << 16), seed),\n                hash_u32_to_f32((ds * 4 + 1) ^ (i << 16), seed),\n                hash_u32_to_f32((ds * 4 + 2) ^ (i << 16), seed),\n                hash_u32_to_f32((ds * 4 + 3) ^ (i << 16), seed),\n            )\n        }\n    }\n}\n\n/// Golden ratio sampler.\nfn golden_ratio_sample(i: u32, scramble: u32) -> f32 {\n    // NOTE: use this for the wavelength dimension, because\n    // due to the nature of hero wavelength sampling this ends up\n    // being crazily more efficient than pretty much any other sampler,\n    // and reduces variance by a huge amount.\n    let n = i\n        .wrapping_add(hash_u32(scramble, 0))\n        .wrapping_mul(2654435769);\n    n as f32 * (1.0 / (1u64 << 32) as f32)\n}\n\n#[derive(Debug)]\nstruct BucketJob {\n    x: u32,\n    y: u32,\n    w: u32,\n    h: u32,\n}\n"
  },
  {
    "path": "src/sampling/mod.rs",
    "content": "mod monte_carlo;\n\npub use self::monte_carlo::{\n    cosine_sample_hemisphere, spherical_triangle_solid_angle, square_to_circle,\n    triangle_surface_area, uniform_sample_cone, uniform_sample_cone_pdf, uniform_sample_hemisphere,\n    uniform_sample_sphere, uniform_sample_spherical_triangle, uniform_sample_triangle,\n};\n"
  },
  {
    "path": "src/sampling/monte_carlo.rs",
    "content": "#![allow(dead_code)]\n\nuse std::{f32::consts::FRAC_PI_4 as QPI_32, f32::consts::PI as PI_32, f64::consts::PI as PI_64};\n\nuse crate::math::{cross, dot, Point, Vector};\n\n/// Maps the unit square to the unit circle.\n/// NOTE: x and y should be distributed within [-1, 1],\n/// not [0, 1].\npub fn square_to_circle(x: f32, y: f32) -> (f32, f32) {\n    debug_assert!(x >= -1.0 && x <= 1.0 && y >= -1.0 && y <= 1.0);\n\n    if x == 0.0 && y == 0.0 {\n        return (0.0, 0.0);\n    }\n\n    let (radius, angle) = if x > y.abs() {\n        // Quadrant 1\n        (x, QPI_32 * (y / x))\n    } else if y > x.abs() {\n        // Quadrant 2\n        (y, QPI_32 * (2.0 - (x / y)))\n    } else if x < -(y.abs()) {\n        // Quadrant 3\n        (-x, QPI_32 * (4.0 + (y / x)))\n    } else {\n        // Quadrant 4\n        (-y, QPI_32 * (6.0 - (x / y)))\n    };\n\n    (radius * angle.cos(), radius * angle.sin())\n}\n\npub fn cosine_sample_hemisphere(u: f32, v: f32) -> Vector {\n    let (u, v) = square_to_circle((u * 2.0) - 1.0, (v * 2.0) - 1.0);\n    let z = (1.0 - ((u * u) + (v * v))).max(0.0).sqrt();\n    Vector::new(u, v, z)\n}\n\npub fn uniform_sample_hemisphere(u: f32, v: f32) -> Vector {\n    let z = u;\n    let r = (1.0 - (z * z)).max(0.0).sqrt();\n    let phi = 2.0 * PI_32 * v;\n    let x = r * phi.cos();\n    let y = r * phi.sin();\n    Vector::new(x, y, z)\n}\n\npub fn uniform_sample_sphere(u: f32, v: f32) -> Vector {\n    let z = 1.0 - (2.0 * u);\n    let r = (1.0 - (z * z)).max(0.0).sqrt();\n    let phi = 2.0 * PI_32 * v;\n    let x = r * phi.cos();\n    let y = r * phi.sin();\n    Vector::new(x, y, z)\n}\n\n/// Samples a solid angle defined by a cone originating from (0,0,0)\n/// and pointing down the positive z-axis.\n///\n/// `u`, `v`: sampling variables, should each be in the interval [0,1]\n/// `cos_theta_max`: cosine of the max angle from the z-axis, defining\n///                  the outer extent of the cone.\npub fn uniform_sample_cone(u: f32, v: f32, cos_theta_max: f64) -> Vector {\n    let cos_theta = (1.0 - u as f64) + (u as f64 * cos_theta_max);\n    let sin_theta = (1.0 - (cos_theta * cos_theta)).sqrt();\n    let phi = v as f64 * 2.0 * PI_64;\n    Vector::new(\n        (phi.cos() * sin_theta) as f32,\n        (phi.sin() * sin_theta) as f32,\n        cos_theta as f32,\n    )\n}\n\npub fn uniform_sample_cone_pdf(cos_theta_max: f64) -> f64 {\n    // 1.0 / solid angle\n    1.0 / (2.0 * PI_64 * (1.0 - cos_theta_max))\n}\n\n/// Generates a uniform sample on a triangle given two uniform random\n/// variables i and j in [0, 1].\npub fn uniform_sample_triangle(va: Vector, vb: Vector, vc: Vector, i: f32, j: f32) -> Vector {\n    let isqrt = i.sqrt();\n    let a = 1.0 - isqrt;\n    let b = isqrt * (1.0 - j);\n    let c = j * isqrt;\n\n    (va * a) + (vb * b) + (vc * c)\n}\n\n/// Calculates the surface area of a triangle.\npub fn triangle_surface_area(p0: Point, p1: Point, p2: Point) -> f32 {\n    0.5 * cross(p1 - p0, p2 - p0).length()\n}\n\n/// Calculates the projected solid angle of a spherical triangle.\n///\n/// A, B, and C are the points of the triangle on a unit sphere.\npub fn spherical_triangle_solid_angle(va: Vector, vb: Vector, vc: Vector) -> f32 {\n    // Calculate sines and cosines of the spherical triangle's edge lengths\n    let cos_a: f64 = dot(vb, vc).max(-1.0).min(1.0) as f64;\n    let cos_b: f64 = dot(vc, va).max(-1.0).min(1.0) as f64;\n    let cos_c: f64 = dot(va, vb).max(-1.0).min(1.0) as f64;\n    let sin_a: f64 = (1.0 - (cos_a * cos_a)).sqrt();\n    let sin_b: f64 = (1.0 - (cos_b * cos_b)).sqrt();\n    let sin_c: f64 = (1.0 - (cos_c * cos_c)).sqrt();\n\n    // If two of the vertices are coincident, area is zero.\n    // Return early to avoid a divide by zero below.\n    if cos_a == 1.0 || cos_b == 1.0 || cos_c == 1.0 {\n        return 0.0;\n    }\n\n    // Calculate the cosine of the angles at the vertices\n    let cos_va = ((cos_a - (cos_b * cos_c)) / (sin_b * sin_c))\n        .max(-1.0)\n        .min(1.0);\n    let cos_vb = ((cos_b - (cos_c * cos_a)) / (sin_c * sin_a))\n        .max(-1.0)\n        .min(1.0);\n    let cos_vc = ((cos_c - (cos_a * cos_b)) / (sin_a * sin_b))\n        .max(-1.0)\n        .min(1.0);\n\n    // Calculate the angles themselves, in radians\n    let ang_va = cos_va.acos();\n    let ang_vb = cos_vb.acos();\n    let ang_vc = cos_vc.acos();\n\n    // Calculate and return the solid angle of the triangle\n    (ang_va + ang_vb + ang_vc - PI_64) as f32\n}\n\n/// Generates a uniform sample on a spherical triangle given two uniform\n/// random variables i and j in [0, 1].\npub fn uniform_sample_spherical_triangle(\n    va: Vector,\n    vb: Vector,\n    vc: Vector,\n    i: f32,\n    j: f32,\n) -> Vector {\n    // Calculate sines and cosines of the spherical triangle's edge lengths\n    let cos_a: f64 = dot(vb, vc).max(-1.0).min(1.0) as f64;\n    let cos_b: f64 = dot(vc, va).max(-1.0).min(1.0) as f64;\n    let cos_c: f64 = dot(va, vb).max(-1.0).min(1.0) as f64;\n    let sin_a: f64 = (1.0 - (cos_a * cos_a)).sqrt();\n    let sin_b: f64 = (1.0 - (cos_b * cos_b)).sqrt();\n    let sin_c: f64 = (1.0 - (cos_c * cos_c)).sqrt();\n\n    // If two of the vertices are coincident, area is zero.\n    // Return early to avoid a divide by zero below.\n    if cos_a == 1.0 || cos_b == 1.0 || cos_c == 1.0 {\n        // TODO: do something more intelligent here, in the case that it's\n        // an infinitely thin line.\n        return va;\n    }\n\n    // Calculate the cosine of the angles at the vertices\n    let cos_va = ((cos_a - (cos_b * cos_c)) / (sin_b * sin_c))\n        .max(-1.0)\n        .min(1.0);\n    let cos_vb = ((cos_b - (cos_c * cos_a)) / (sin_c * sin_a))\n        .max(-1.0)\n        .min(1.0);\n    let cos_vc = ((cos_c - (cos_a * cos_b)) / (sin_a * sin_b))\n        .max(-1.0)\n        .min(1.0);\n\n    // Calculate sine for A\n    let sin_va = (1.0 - (cos_va * cos_va)).sqrt();\n\n    // Calculate the angles themselves, in radians\n    let ang_va = cos_va.acos();\n    let ang_vb = cos_vb.acos();\n    let ang_vc = cos_vc.acos();\n\n    // Calculate the area of the spherical triangle\n    let area = ang_va + ang_vb + ang_vc - PI_64;\n\n    // The rest of this is from the paper \"Stratified Sampling of Spherical\n    // Triangles\" by James Arvo.\n    let area_2 = area * i as f64;\n\n    let s = (area_2 - ang_va).sin();\n    let t = (area_2 - ang_va).cos();\n    let u = t - cos_va;\n    let v = s + (sin_va * cos_c);\n\n    let q_top = (((v * t) - (u * s)) * cos_va) - v;\n    let q_bottom = ((v * s) + (u * t)) * sin_va;\n    let q = q_top / q_bottom;\n\n    let vc_2 =\n        (va * q as f32) + ((vc - (va * dot(vc, va))).normalized() * (1.0 - (q * q)).sqrt() as f32);\n\n    let z = 1.0 - (j * (1.0 - dot(vc_2, vb)));\n\n    (vb * z) + ((vc_2 - (vb * dot(vc_2, vb))).normalized() * (1.0 - (z * z)).sqrt())\n}\n"
  },
  {
    "path": "src/scene/assembly.rs",
    "content": "use std::collections::HashMap;\n\nuse kioku::Arena;\n\nuse crate::{\n    accel::BVH4,\n    accel::{LightAccel, LightTree},\n    bbox::{transform_bbox_slice_from, BBox},\n    boundable::Boundable,\n    color::SpectralSample,\n    lerp::lerp_slice,\n    light::SurfaceLight,\n    math::{Normal, Point, Transform},\n    shading::SurfaceShader,\n    surface::{Surface, SurfaceIntersection},\n    transform_stack::TransformStack,\n};\n\n#[derive(Copy, Clone, Debug)]\npub struct Assembly<'a> {\n    // Instance list\n    pub instances: &'a [Instance],\n    pub light_instances: &'a [Instance],\n    pub xforms: &'a [Transform],\n\n    // Surface shader list\n    pub surface_shaders: &'a [&'a dyn SurfaceShader],\n\n    // Object list\n    pub objects: &'a [Object<'a>],\n\n    // Assembly list\n    pub assemblies: &'a [Assembly<'a>],\n\n    // Object accel\n    pub object_accel: BVH4<'a>,\n\n    // Light accel\n    pub light_accel: LightTree<'a>,\n}\n\n// TODO: actually fix this clippy warning, rather than `allow`ing it.\n#[allow(clippy::type_complexity)]\nimpl<'a> Assembly<'a> {\n    // Returns (light_color, (sample_point, normal, point_err), pdf, selection_pdf)\n    pub fn sample_lights(\n        &self,\n        xform_stack: &mut TransformStack,\n        n: f32,\n        uvw: (f32, f32, f32),\n        wavelength: f32,\n        time: f32,\n        intr: &SurfaceIntersection,\n    ) -> Option<(SpectralSample, (Point, Normal, f32), f32, f32)> {\n        if let SurfaceIntersection::Hit {\n            intersection_data: idata,\n            closure,\n        } = *intr\n        {\n            let sel_xform = if !xform_stack.top().is_empty() {\n                lerp_slice(xform_stack.top(), time)\n            } else {\n                Transform::new()\n            };\n            if let Some((light_i, sel_pdf, whittled_n)) = self.light_accel.select(\n                idata.incoming * sel_xform,\n                idata.pos * sel_xform,\n                idata.nor * sel_xform,\n                idata.nor_g * sel_xform,\n                &closure,\n                time,\n                n,\n            ) {\n                let inst = self.light_instances[light_i];\n                match inst.instance_type {\n                    InstanceType::Object => {\n                        match self.objects[inst.data_index] {\n                            Object::SurfaceLight(light) => {\n                                // Get the world-to-object space transform of the light\n                                let xform = if let Some((a, b)) = inst.transform_indices {\n                                    let pxforms = xform_stack.top();\n                                    let xform = lerp_slice(&self.xforms[a..b], time);\n                                    if !pxforms.is_empty() {\n                                        lerp_slice(pxforms, time) * xform\n                                    } else {\n                                        xform\n                                    }\n                                } else {\n                                    let pxforms = xform_stack.top();\n                                    if !pxforms.is_empty() {\n                                        lerp_slice(pxforms, time)\n                                    } else {\n                                        Transform::new()\n                                    }\n                                };\n\n                                // Sample the light\n                                let (color, sample_geo, pdf) = light.sample_from_point(\n                                    &xform, idata.pos, uvw.0, uvw.1, wavelength, time,\n                                );\n                                return Some((color, sample_geo, pdf, sel_pdf));\n                            }\n\n                            _ => unimplemented!(),\n                        }\n                    }\n\n                    InstanceType::Assembly => {\n                        // Push the world-to-object space transforms of the assembly onto\n                        // the transform stack.\n                        if let Some((a, b)) = inst.transform_indices {\n                            xform_stack.push(&self.xforms[a..b]);\n                        }\n\n                        // Sample sub-assembly lights\n                        let sample = self.assemblies[inst.data_index].sample_lights(\n                            xform_stack,\n                            whittled_n,\n                            uvw,\n                            wavelength,\n                            time,\n                            intr,\n                        );\n\n                        // Pop the assembly's transforms off the transform stack.\n                        if inst.transform_indices.is_some() {\n                            xform_stack.pop();\n                        }\n\n                        // Return sample\n                        return sample.map(|(ss, v, pdf, spdf)| (ss, v, pdf, spdf * sel_pdf));\n                    }\n                }\n            } else {\n                None\n            }\n        } else {\n            None\n        }\n    }\n}\n\nimpl<'a> Boundable for Assembly<'a> {\n    fn bounds(&self) -> &[BBox] {\n        self.object_accel.bounds()\n    }\n}\n\n#[derive(Debug)]\npub struct AssemblyBuilder<'a> {\n    arena: &'a Arena,\n\n    // Instance list\n    instances: Vec<Instance>,\n    xforms: Vec<Transform>,\n\n    // Shader list\n    surface_shaders: Vec<&'a dyn SurfaceShader>,\n    surface_shader_map: HashMap<String, usize>, // map Name -> Index\n\n    // Object list\n    objects: Vec<Object<'a>>,\n    object_map: HashMap<String, usize>, // map Name -> Index\n\n    // Assembly list\n    assemblies: Vec<Assembly<'a>>,\n    assembly_map: HashMap<String, usize>, // map Name -> Index\n}\n\nimpl<'a> AssemblyBuilder<'a> {\n    pub fn new(arena: &'a Arena) -> AssemblyBuilder<'a> {\n        AssemblyBuilder {\n            arena: arena,\n            instances: Vec::new(),\n            xforms: Vec::new(),\n            surface_shaders: Vec::new(),\n            surface_shader_map: HashMap::new(),\n            objects: Vec::new(),\n            object_map: HashMap::new(),\n            assemblies: Vec::new(),\n            assembly_map: HashMap::new(),\n        }\n    }\n\n    pub fn add_surface_shader(&mut self, name: &str, shader: &'a dyn SurfaceShader) {\n        // Make sure the name hasn't already been used.\n        if self.surface_shader_map.contains_key(name) {\n            panic!(\"Attempted to add surface shader to assembly with a name that already exists.\");\n        }\n\n        // Add shader\n        self.surface_shader_map\n            .insert(name.to_string(), self.surface_shaders.len());\n        self.surface_shaders.push(shader);\n    }\n\n    pub fn add_object(&mut self, name: &str, obj: Object<'a>) {\n        // Make sure the name hasn't already been used.\n        if self.name_exists(name) {\n            panic!(\"Attempted to add object to assembly with a name that already exists.\");\n        }\n\n        // Add object\n        self.object_map.insert(name.to_string(), self.objects.len());\n        self.objects.push(obj);\n    }\n\n    pub fn add_assembly(&mut self, name: &str, asmb: Assembly<'a>) {\n        // Make sure the name hasn't already been used.\n        if self.name_exists(name) {\n            panic!(\n                \"Attempted to add assembly to another assembly with a name that already \\\n                 exists.\"\n            );\n        }\n\n        // Add assembly\n        self.assembly_map\n            .insert(name.to_string(), self.assemblies.len());\n        self.assemblies.push(asmb);\n    }\n\n    pub fn add_instance(\n        &mut self,\n        name: &str,\n        surface_shader_name: Option<&str>,\n        xforms: Option<&[Transform]>,\n    ) {\n        // Make sure name exists\n        if !self.name_exists(name) {\n            panic!(\"Attempted to add instance with a name that doesn't exist.\");\n        }\n\n        // Map zero-length transforms to None\n        let xforms = if let Some(xf) = xforms {\n            if !xf.is_empty() {\n                Some(xf)\n            } else {\n                None\n            }\n        } else {\n            None\n        };\n\n        // Create instance\n        let instance = if self.object_map.contains_key(name) {\n            Instance {\n                instance_type: InstanceType::Object,\n                data_index: self.object_map[name],\n                surface_shader_index: surface_shader_name.map(|name| {\n                    *self\n                        .surface_shader_map\n                        .get(name)\n                        .unwrap_or_else(|| panic!(\"Unknown surface shader '{}'.\", name))\n                }),\n                id: self.instances.len(),\n                transform_indices: xforms\n                    .map(|xf| (self.xforms.len(), self.xforms.len() + xf.len())),\n            }\n        } else {\n            Instance {\n                instance_type: InstanceType::Assembly,\n                data_index: self.assembly_map[name],\n                surface_shader_index: surface_shader_name.map(|name| {\n                    *self\n                        .surface_shader_map\n                        .get(name)\n                        .unwrap_or_else(|| panic!(\"Unknown surface shader '{}'.\", name))\n                }),\n                id: self.instances.len(),\n                transform_indices: xforms\n                    .map(|xf| (self.xforms.len(), self.xforms.len() + xf.len())),\n            }\n        };\n\n        self.instances.push(instance);\n\n        // Store transforms\n        if let Some(xf) = xforms {\n            self.xforms.extend(xf);\n        }\n    }\n\n    pub fn name_exists(&self, name: &str) -> bool {\n        self.object_map.contains_key(name) || self.assembly_map.contains_key(name)\n    }\n\n    pub fn build(mut self) -> Assembly<'a> {\n        // Calculate instance bounds, used for building object accel and light accel.\n        let (bis, bbs) = self.instance_bounds();\n\n        // Build object accel\n        let object_accel = BVH4::from_objects(self.arena, &mut self.instances[..], 1, |inst| {\n            &bbs[bis[inst.id]..bis[inst.id + 1]]\n        });\n\n        // Get list of instances that are for light sources or assemblies that contain light\n        // sources.\n        let mut light_instances: Vec<_> = self\n            .instances\n            .iter()\n            .filter(|inst| match inst.instance_type {\n                InstanceType::Object => {\n                    if let Object::SurfaceLight(_) = self.objects[inst.data_index] {\n                        true\n                    } else {\n                        false\n                    }\n                }\n\n                InstanceType::Assembly => {\n                    self.assemblies[inst.data_index]\n                        .light_accel\n                        .approximate_energy()\n                        > 0.0\n                }\n            })\n            .cloned()\n            .collect();\n\n        // Build light accel\n        let light_accel = LightTree::from_objects(self.arena, &mut light_instances[..], |inst| {\n            let bounds = &bbs[bis[inst.id]..bis[inst.id + 1]];\n            let energy = match inst.instance_type {\n                InstanceType::Object => {\n                    if let Object::SurfaceLight(light) = self.objects[inst.data_index] {\n                        light.approximate_energy()\n                    } else {\n                        0.0\n                    }\n                }\n\n                InstanceType::Assembly => self.assemblies[inst.data_index]\n                    .light_accel\n                    .approximate_energy(),\n            };\n            (bounds, energy)\n        });\n\n        Assembly {\n            instances: self.arena.copy_slice(&self.instances),\n            light_instances: self.arena.copy_slice(&light_instances),\n            xforms: self.arena.copy_slice(&self.xforms),\n            surface_shaders: self.arena.copy_slice(&self.surface_shaders),\n            objects: self.arena.copy_slice(&self.objects),\n            assemblies: self.arena.copy_slice(&self.assemblies),\n            object_accel: object_accel,\n            light_accel: light_accel,\n        }\n    }\n\n    /// Returns a pair of vectors with the bounds of all instances.\n    /// This is used for building the assembly's BVH4.\n    fn instance_bounds(&self) -> (Vec<usize>, Vec<BBox>) {\n        let mut indices = vec![0];\n        let mut bounds = Vec::new();\n\n        for inst in &self.instances {\n            let mut bbs = Vec::new();\n            let mut bbs2 = Vec::new();\n\n            // Get bounding boxes\n            match inst.instance_type {\n                InstanceType::Object => {\n                    // Push bounds onto bbs\n                    let obj = &self.objects[inst.data_index];\n                    match *obj {\n                        Object::Surface(s) => bbs.extend(s.bounds()),\n                        Object::SurfaceLight(l) => bbs.extend(l.bounds()),\n                    }\n                }\n\n                InstanceType::Assembly => {\n                    // Push bounds onto bbs\n                    let asmb = &self.assemblies[inst.data_index];\n                    bbs.extend(asmb.bounds());\n                }\n            }\n\n            // Transform the bounding boxes, if necessary\n            if let Some((xstart, xend)) = inst.transform_indices {\n                let xf = &self.xforms[xstart..xend];\n                transform_bbox_slice_from(&bbs, xf, &mut bbs2);\n            } else {\n                bbs2.clear();\n                bbs2.extend(bbs);\n            }\n\n            // Push transformed bounds onto vec\n            bounds.extend(bbs2);\n            indices.push(bounds.len());\n        }\n\n        (indices, bounds)\n    }\n}\n\n#[derive(Copy, Clone, Debug)]\npub enum Object<'a> {\n    Surface(&'a dyn Surface),\n    SurfaceLight(&'a dyn SurfaceLight),\n}\n\n#[derive(Debug, Copy, Clone)]\npub struct Instance {\n    pub instance_type: InstanceType,\n    pub data_index: usize,\n    pub surface_shader_index: Option<usize>,\n    pub id: usize,\n    pub transform_indices: Option<(usize, usize)>,\n}\n\n#[derive(Debug, Copy, Clone)]\npub enum InstanceType {\n    Object,\n    Assembly,\n}\n"
  },
  {
    "path": "src/scene/mod.rs",
    "content": "mod assembly;\nmod world;\n\nuse crate::{\n    accel::LightAccel,\n    algorithm::weighted_choice,\n    camera::Camera,\n    color::SpectralSample,\n    math::{Normal, Point, Vector},\n    surface::SurfaceIntersection,\n    transform_stack::TransformStack,\n};\n\npub use self::{\n    assembly::{Assembly, AssemblyBuilder, InstanceType, Object},\n    world::World,\n};\n\n#[derive(Debug)]\npub struct Scene<'a> {\n    pub name: Option<String>,\n    pub camera: Camera<'a>,\n    pub world: World<'a>,\n    pub root: Assembly<'a>,\n}\n\nimpl<'a> Scene<'a> {\n    pub fn sample_lights(\n        &self,\n        xform_stack: &mut TransformStack,\n        n: f32,\n        uvw: (f32, f32, f32),\n        wavelength: f32,\n        time: f32,\n        intr: &SurfaceIntersection,\n    ) -> SceneLightSample {\n        // TODO: this just selects between world lights and local lights\n        // with a 50/50 chance.  We should do something more sophisticated\n        // than this, accounting for the estimated impact of the lights\n        // on the point being lit.\n\n        // Calculate relative probabilities of traversing into world lights\n        // or local lights.\n        let wl_energy = if self\n            .world\n            .lights\n            .iter()\n            .fold(0.0, |energy, light| energy + light.approximate_energy())\n            <= 0.0\n        {\n            0.0\n        } else {\n            1.0\n        };\n        let ll_energy = if self.root.light_accel.approximate_energy() <= 0.0 {\n            0.0\n        } else {\n            1.0\n        };\n        let tot_energy = wl_energy + ll_energy;\n\n        // Decide either world or local lights, and select and sample a light.\n        if tot_energy <= 0.0 {\n            return SceneLightSample::None;\n        } else {\n            let wl_prob = wl_energy / tot_energy;\n\n            if n < wl_prob {\n                // World lights\n                let n = n / wl_prob;\n                let (i, p) = weighted_choice(self.world.lights, n, |l| l.approximate_energy());\n                let (ss, sv, pdf) =\n                    self.world.lights[i].sample_from_point(uvw.0, uvw.1, wavelength, time);\n                return SceneLightSample::Distant {\n                    color: ss,\n                    direction: sv,\n                    pdf: pdf,\n                    selection_pdf: p * wl_prob,\n                };\n            } else {\n                // Local lights\n                let n = (n - wl_prob) / (1.0 - wl_prob);\n\n                if let Some((ss, sgeo, pdf, spdf)) =\n                    self.root\n                        .sample_lights(xform_stack, n, uvw, wavelength, time, intr)\n                {\n                    return SceneLightSample::Surface {\n                        color: ss,\n                        sample_geo: sgeo,\n                        pdf: pdf,\n                        selection_pdf: spdf * (1.0 - wl_prob),\n                    };\n                } else {\n                    return SceneLightSample::None;\n                }\n            }\n        }\n    }\n}\n\n#[derive(Debug, Copy, Clone)]\npub enum SceneLightSample {\n    None,\n    Distant {\n        color: SpectralSample,\n        direction: Vector,\n        pdf: f32,\n        selection_pdf: f32,\n    },\n    Surface {\n        color: SpectralSample,\n        sample_geo: (Point, Normal, f32),\n        pdf: f32,\n        selection_pdf: f32,\n    },\n}\n\nimpl SceneLightSample {\n    pub fn is_none(&self) -> bool {\n        if let SceneLightSample::None = *self {\n            true\n        } else {\n            false\n        }\n    }\n\n    pub fn color(&self) -> SpectralSample {\n        match *self {\n            SceneLightSample::None => panic!(),\n            SceneLightSample::Distant { color, .. } => color,\n            SceneLightSample::Surface { color, .. } => color,\n        }\n    }\n\n    pub fn pdf(&self) -> f32 {\n        match *self {\n            SceneLightSample::None => panic!(),\n            SceneLightSample::Distant { pdf, .. } => pdf,\n            SceneLightSample::Surface { pdf, .. } => pdf,\n        }\n    }\n\n    pub fn selection_pdf(&self) -> f32 {\n        match *self {\n            SceneLightSample::None => panic!(),\n            SceneLightSample::Distant { selection_pdf, .. } => selection_pdf,\n            SceneLightSample::Surface { selection_pdf, .. } => selection_pdf,\n        }\n    }\n}\n"
  },
  {
    "path": "src/scene/world.rs",
    "content": "use crate::{color::Color, light::WorldLightSource};\n\n#[derive(Debug)]\npub struct World<'a> {\n    pub background_color: Color,\n    pub lights: &'a [&'a dyn WorldLightSource],\n}\n"
  },
  {
    "path": "src/shading/mod.rs",
    "content": "pub mod surface_closure;\n\nuse std::fmt::Debug;\n\nuse crate::{color::Color, surface::SurfaceIntersectionData};\n\npub use self::surface_closure::SurfaceClosure;\n\n/// Trait for surface shaders.\npub trait SurfaceShader: Debug + Sync {\n    /// Takes the result of a surface intersection and returns the surface\n    /// closure to be evaluated at that intersection point.\n    fn shade(&self, data: &SurfaceIntersectionData, time: f32) -> SurfaceClosure;\n}\n\n/// Clearly we must eat this brownie before the world ends, lest it\n/// go uneaten before the world ends.  But to do so we must trek\n/// far--much like in Lord of the Rings--to fetch the golden fork with\n/// which to eat the brownie.  Only this fork can be used to eat this\n/// brownie, for any who try to eat it with a normal fork shall\n/// perish immediately and without honor.  But guarding the fork are\n/// three large donuts, which must all be eaten in sixty seconds or\n/// less to continue on.  It's called the donut challenge.  But these\n/// are no ordinary donuts.  To call them large is actually doing\n/// them a great injustice, for they are each the size of a small\n/// building.\n#[derive(Debug, Copy, Clone)]\npub enum SimpleSurfaceShader {\n    Emit {\n        color: Color,\n    },\n    Lambert {\n        color: Color,\n    },\n    GGX {\n        color: Color,\n        roughness: f32,\n        fresnel: f32,\n    },\n}\n\nimpl SurfaceShader for SimpleSurfaceShader {\n    fn shade(&self, data: &SurfaceIntersectionData, time: f32) -> SurfaceClosure {\n        let _ = (data, time); // Silence \"unused\" compiler warning\n\n        match *self {\n            SimpleSurfaceShader::Emit { color } => SurfaceClosure::Emit(color),\n\n            SimpleSurfaceShader::Lambert { color } => SurfaceClosure::Lambert(color),\n\n            SimpleSurfaceShader::GGX {\n                color,\n                roughness,\n                fresnel,\n            } => SurfaceClosure::GGX {\n                color: color,\n                roughness: roughness,\n                fresnel: fresnel,\n            },\n        }\n    }\n}\n"
  },
  {
    "path": "src/shading/surface_closure.rs",
    "content": "#![allow(dead_code)]\n\nuse std::f32::consts::PI as PI_32;\n\nuse glam::Vec4;\n\nuse crate::{\n    color::{Color, SpectralSample},\n    lerp::{lerp, Lerp},\n    math::{dot, zup_to_vec, Normal, Vector},\n    sampling::cosine_sample_hemisphere,\n};\n\nconst INV_PI: f32 = 1.0 / PI_32;\nconst H_PI: f32 = PI_32 / 2.0;\n\n/// A surface closure, specifying a BSDF for a point on a surface.\n#[derive(Debug, Copy, Clone)]\npub enum SurfaceClosure {\n    // Normal surface closures.\n    Lambert(Color),\n    GGX {\n        color: Color,\n        roughness: f32,\n        fresnel: f32, // [0.0, 1.0] determines how much fresnel reflection comes into play\n    },\n\n    // Special closures that need special handling by the renderer.\n    Emit(Color),\n}\n\nuse self::SurfaceClosure::*;\n\n/// Note when implementing new BSDFs: both the the color filter and pdf returned from\n/// `sample()` and `evaluate()` should be identical for the same parameters and outgoing\n/// light direction.\nimpl SurfaceClosure {\n    /// Returns whether the closure has a delta distribution or not.\n    pub fn is_delta(&self) -> bool {\n        match *self {\n            Lambert(_) => false,\n            GGX { roughness, .. } => roughness == 0.0,\n            Emit(_) => false,\n        }\n    }\n\n    /// Given an incoming ray and sample values, generates an outgoing ray and\n    /// color filter.\n    ///\n    /// inc:        Incoming light direction.\n    /// nor:        The shading surface normal at the surface point.\n    /// nor_g:      The geometric surface normal at the surface point.\n    /// uv:         The sampling values.\n    /// wavelength: Hero wavelength to generate the color filter for.\n    ///\n    /// Returns a tuple with the generated outgoing light direction, color filter, and pdf.\n    pub fn sample(\n        &self,\n        inc: Vector,\n        nor: Normal,\n        nor_g: Normal,\n        uv: (f32, f32),\n        wavelength: f32,\n    ) -> (Vector, SpectralSample, f32) {\n        match *self {\n            Lambert(color) => lambert_closure::sample(color, inc, nor, nor_g, uv, wavelength),\n\n            GGX {\n                color,\n                roughness,\n                fresnel,\n            } => ggx_closure::sample(color, roughness, fresnel, inc, nor, nor_g, uv, wavelength),\n\n            Emit(color) => emit_closure::sample(color, inc, nor, nor_g, uv, wavelength),\n        }\n    }\n\n    /// Evaluates the closure for the given incoming and outgoing rays.\n    ///\n    /// inc:        The incoming light direction.\n    /// out:        The outgoing light direction.\n    /// nor:        The shading surface normal at the surface point.\n    /// nor_g:      The geometric surface normal at the surface point.\n    /// wavelength: Hero wavelength to generate the color filter for.\n    ///\n    /// Returns the resulting filter color and pdf of if this had been generated\n    /// by `sample()`.\n    pub fn evaluate(\n        &self,\n        inc: Vector,\n        out: Vector,\n        nor: Normal,\n        nor_g: Normal,\n        wavelength: f32,\n    ) -> (SpectralSample, f32) {\n        match *self {\n            Lambert(color) => lambert_closure::evaluate(color, inc, out, nor, nor_g, wavelength),\n\n            GGX {\n                color,\n                roughness,\n                fresnel,\n            } => ggx_closure::evaluate(color, roughness, fresnel, inc, out, nor, nor_g, wavelength),\n\n            Emit(color) => emit_closure::evaluate(color, inc, out, nor, nor_g, wavelength),\n        }\n    }\n\n    /// Returns an estimate of the sum total energy that evaluate() would return\n    /// when integrated over a spherical light source with a center at relative\n    /// position 'to_light_center' and squared radius 'light_radius_squared'.\n    /// This is used for importance sampling, so does not need to be exact,\n    /// but it does need to be non-zero anywhere that an exact solution would\n    /// be non-zero.\n    pub fn estimate_eval_over_sphere_light(\n        &self,\n        inc: Vector,\n        to_light_center: Vector,\n        light_radius_squared: f32,\n        nor: Normal,\n        nor_g: Normal,\n    ) -> f32 {\n        match *self {\n            Lambert(color) => lambert_closure::estimate_eval_over_sphere_light(\n                color,\n                inc,\n                to_light_center,\n                light_radius_squared,\n                nor,\n                nor_g,\n            ),\n            GGX {\n                color,\n                roughness,\n                fresnel,\n            } => ggx_closure::estimate_eval_over_sphere_light(\n                color,\n                roughness,\n                fresnel,\n                inc,\n                to_light_center,\n                light_radius_squared,\n                nor,\n                nor_g,\n            ),\n            Emit(color) => emit_closure::estimate_eval_over_sphere_light(\n                color,\n                inc,\n                to_light_center,\n                light_radius_squared,\n                nor,\n                nor_g,\n            ),\n        }\n    }\n\n    /// Returns the post-compression size of this closure.\n    pub fn compressed_size(&self) -> usize {\n        1 + match *self {\n            Lambert(color) => color.compressed_size(),\n            GGX { color, .. } => {\n                2 // Roughness\n                + 2 // Fresnel\n                + color.compressed_size() // Color\n            }\n            Emit(color) => color.compressed_size(),\n        }\n    }\n\n    /// Writes the compressed form of this closure to `out_data`.\n    ///\n    /// `out_data` must be at least `compressed_size()` bytes long, otherwise\n    /// this method will panic.\n    ///\n    /// Returns the number of bytes written.\n    pub fn write_compressed(&self, out_data: &mut [u8]) -> usize {\n        match *self {\n            Lambert(color) => {\n                out_data[0] = 0; // Discriminant\n                color.write_compressed(&mut out_data[1..]);\n            }\n            GGX {\n                color,\n                roughness,\n                fresnel,\n            } => {\n                out_data[0] = 1; // Discriminant\n\n                // Roughness and fresnel (we write these first because they are\n                // constant-size, whereas the color is variable-size, so this\n                // makes things a little easier).\n                let rgh =\n                    ((roughness.max(0.0).min(1.0) * std::u16::MAX as f32) as u16).to_le_bytes();\n                let frs = ((fresnel.max(0.0).min(1.0) * std::u16::MAX as f32) as u16).to_le_bytes();\n                out_data[1] = rgh[0];\n                out_data[2] = rgh[1];\n                out_data[3] = frs[0];\n                out_data[4] = frs[1];\n\n                // Color\n                color.write_compressed(&mut out_data[5..]); // Color\n            }\n            Emit(color) => {\n                out_data[0] = 2; // Discriminant\n                color.write_compressed(&mut out_data[1..]);\n            }\n        }\n        self.compressed_size()\n    }\n\n    /// Constructs a SurfaceClosure from compressed closure data, and also\n    /// returns the number of bytes consumed from `in_data`.\n    pub fn from_compressed(in_data: &[u8]) -> (SurfaceClosure, usize) {\n        match in_data[0] {\n            0 => {\n                // Lambert\n                let (col, size) = Color::from_compressed(&in_data[1..]);\n                (SurfaceClosure::Lambert(col), 1 + size)\n            }\n\n            1 => {\n                // GGX\n                let mut rgh = [0u8; 2];\n                let mut frs = [0u8; 2];\n                rgh[0] = in_data[1];\n                rgh[1] = in_data[2];\n                frs[0] = in_data[3];\n                frs[1] = in_data[4];\n                let rgh = u16::from_le_bytes(rgh) as f32 * (1.0 / std::u16::MAX as f32);\n                let frs = u16::from_le_bytes(frs) as f32 * (1.0 / std::u16::MAX as f32);\n                let (col, size) = Color::from_compressed(&in_data[5..]);\n                (\n                    SurfaceClosure::GGX {\n                        color: col,\n                        roughness: rgh,\n                        fresnel: frs,\n                    },\n                    5 + size,\n                )\n            }\n\n            2 => {\n                // Emit\n                let (col, size) = Color::from_compressed(&in_data[1..]);\n                (SurfaceClosure::Emit(col), 1 + size)\n            }\n\n            _ => unreachable!(),\n        }\n    }\n}\n\nimpl Lerp for SurfaceClosure {\n    fn lerp(self, other: SurfaceClosure, alpha: f32) -> SurfaceClosure {\n        match (self, other) {\n            (Lambert(col1), Lambert(col2)) => Lambert(lerp(col1, col2, alpha)),\n            (\n                GGX {\n                    color: col1,\n                    roughness: rgh1,\n                    fresnel: frs1,\n                },\n                GGX {\n                    color: col2,\n                    roughness: rgh2,\n                    fresnel: frs2,\n                },\n            ) => GGX {\n                color: lerp(col1, col2, alpha),\n                roughness: lerp(rgh1, rgh2, alpha),\n                fresnel: lerp(frs1, frs2, alpha),\n            },\n            (Emit(col1), Emit(col2)) => Emit(lerp(col1, col2, alpha)),\n\n            _ => panic!(\"Cannot lerp between different surface closure types.\"),\n        }\n    }\n}\n\n/// Lambert closure code.\nmod lambert_closure {\n    use super::*;\n\n    pub fn sample(\n        color: Color,\n        inc: Vector,\n        nor: Normal,\n        nor_g: Normal,\n        uv: (f32, f32),\n        wavelength: f32,\n    ) -> (Vector, SpectralSample, f32) {\n        let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 {\n            (nor.normalized().into_vector(), nor_g.into_vector())\n        } else {\n            (-nor.normalized().into_vector(), -nor_g.into_vector())\n        };\n\n        // Generate a random ray direction in the hemisphere\n        // of the shading surface normal.\n        let dir = cosine_sample_hemisphere(uv.0, uv.1);\n        let pdf = dir.z() * INV_PI;\n        let out = zup_to_vec(dir, nn);\n\n        // Make sure it's not on the wrong side of the geometric normal.\n        if dot(flipped_nor_g, out) >= 0.0 {\n            (out, color.to_spectral_sample(wavelength) * pdf, pdf)\n        } else {\n            (out, SpectralSample::new(0.0), 0.0)\n        }\n    }\n\n    pub fn evaluate(\n        color: Color,\n        inc: Vector,\n        out: Vector,\n        nor: Normal,\n        nor_g: Normal,\n        wavelength: f32,\n    ) -> (SpectralSample, f32) {\n        let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 {\n            (nor.normalized().into_vector(), nor_g.into_vector())\n        } else {\n            (-nor.normalized().into_vector(), -nor_g.into_vector())\n        };\n\n        if dot(flipped_nor_g, out) >= 0.0 {\n            let fac = dot(nn, out.normalized()).max(0.0) * INV_PI;\n            (color.to_spectral_sample(wavelength) * fac, fac)\n        } else {\n            (SpectralSample::new(0.0), 0.0)\n        }\n    }\n\n    pub fn estimate_eval_over_sphere_light(\n        _color: Color,\n        inc: Vector,\n        to_light_center: Vector,\n        light_radius_squared: f32,\n        nor: Normal,\n        nor_g: Normal,\n    ) -> f32 {\n        let _ = nor_g; // Not using this, silence warning\n\n        // Analytically calculates lambert shading from a uniform light source\n        // subtending a circular solid angle.\n        // Only works for solid angle subtending equal to or less than a hemisphere.\n        //\n        // Formula taken from \"Area Light Sources for Real-Time Graphics\"\n        // by John M. Snyder\n        fn sphere_lambert(nlcos: f32, rcos: f32) -> f32 {\n            assert!(nlcos >= -1.0 && nlcos <= 1.0);\n            assert!(rcos >= 0.0 && rcos <= 1.0);\n\n            let nlsin: f32 = (1.0 - (nlcos * nlcos)).sqrt();\n            let rsin2: f32 = 1.0 - (rcos * rcos);\n            let rsin: f32 = rsin2.sqrt();\n            let ysin: f32 = rcos / nlsin;\n            let ycos2: f32 = 1.0 - (ysin * ysin);\n            let ycos: f32 = ycos2.sqrt();\n\n            let g: f32 = (-2.0 * nlsin * rcos * ycos) + H_PI - ysin.asin() + (ysin * ycos);\n            let h: f32 = nlcos * ((ycos * (rsin2 - ycos2).sqrt()) + (rsin2 * (ycos / rsin).asin()));\n\n            let nl: f32 = nlcos.acos();\n            let r: f32 = rcos.acos();\n\n            if nl < (H_PI - r) {\n                nlcos * rsin2\n            } else if nl < H_PI {\n                (nlcos * rsin2) + g - h\n            } else if nl < (H_PI + r) {\n                (g + h) * INV_PI\n            } else {\n                0.0\n            }\n        }\n\n        let dist2 = to_light_center.length2();\n        if dist2 <= light_radius_squared {\n            return (light_radius_squared / dist2).min(4.0);\n        } else {\n            let sin_theta_max2 = (light_radius_squared / dist2).min(1.0);\n            let cos_theta_max = (1.0 - sin_theta_max2).sqrt();\n\n            let v = to_light_center.normalized();\n            let nn = if dot(nor_g.into_vector(), inc) <= 0.0 {\n                nor.normalized()\n            } else {\n                -nor.normalized()\n            }\n            .into_vector();\n\n            let cos_nv = dot(nn, v).max(-1.0).min(1.0);\n\n            // Alt implementation from the SPI paper.\n            // Worse sampling, but here for reference.\n            // {\n            //     let nl_ang = cos_nv.acos();\n            //     let rad_ang = cos_theta_max.acos();\n            //     let min_ang = (nl_ang - rad_ang).max(0.0);\n            //     let lamb = min_ang.cos().max(0.0);\n\n            //     return lamb / dist2;\n            // }\n\n            return sphere_lambert(cos_nv, cos_theta_max);\n        }\n    }\n}\n\nmod ggx_closure {\n    use super::*;\n\n    // Makes sure values are in a valid range\n    pub fn validate(roughness: f32, fresnel: f32) {\n        debug_assert!(fresnel >= 0.0 && fresnel <= 1.0);\n        debug_assert!(roughness >= 0.0 && roughness <= 1.0);\n    }\n\n    pub fn sample(\n        col: Color,\n        roughness: f32,\n        fresnel: f32,\n        inc: Vector,\n        nor: Normal,\n        nor_g: Normal,\n        uv: (f32, f32),\n        wavelength: f32,\n    ) -> (Vector, SpectralSample, f32) {\n        // Get normalized surface normal\n        let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 {\n            (nor.normalized().into_vector(), nor_g.into_vector())\n        } else {\n            (-nor.normalized().into_vector(), -nor_g.into_vector())\n        };\n\n        // Generate a random ray direction in the hemisphere\n        // of the surface.\n        let theta_cos = half_theta_sample(uv.0, roughness);\n        let theta_sin = (1.0 - (theta_cos * theta_cos)).sqrt();\n        let angle = uv.1 * PI_32 * 2.0;\n        let mut half_dir = Vector::new(angle.cos() * theta_sin, angle.sin() * theta_sin, theta_cos);\n        half_dir = zup_to_vec(half_dir, nn).normalized();\n\n        let out = inc - (half_dir * 2.0 * dot(inc, half_dir));\n\n        // Make sure it's not on the wrong side of the geometric normal.\n        if dot(flipped_nor_g, out) >= 0.0 {\n            let (filter, pdf) = evaluate(col, roughness, fresnel, inc, out, nor, nor_g, wavelength);\n            (out, filter, pdf)\n        } else {\n            (out, SpectralSample::new(0.0), 0.0)\n        }\n    }\n\n    pub fn evaluate(\n        col: Color,\n        roughness: f32,\n        fresnel: f32,\n        inc: Vector,\n        out: Vector,\n        nor: Normal,\n        nor_g: Normal,\n        wavelength: f32,\n    ) -> (SpectralSample, f32) {\n        // Calculate needed vectors, normalized\n        let aa = -inc.normalized(); // Vector pointing to where \"in\" came from\n        let bb = out.normalized(); // Out\n        let hh = (aa + bb).normalized(); // Half-way between aa and bb\n\n        // Surface normal\n        let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 {\n            (nor.normalized().into_vector(), nor_g.into_vector())\n        } else {\n            (-nor.normalized().into_vector(), -nor_g.into_vector())\n        };\n\n        // Make sure everything's on the correct side of the surface\n        if dot(nn, aa) < 0.0 || dot(nn, bb) < 0.0 || dot(flipped_nor_g, bb) < 0.0 {\n            return (SpectralSample::new(0.0), 0.0);\n        }\n\n        // Calculate needed dot products\n        let na = dot(nn, aa).clamp(-1.0, 1.0);\n        let nb = dot(nn, bb).clamp(-1.0, 1.0);\n        let ha = dot(hh, aa).clamp(-1.0, 1.0);\n        let hb = dot(hh, bb).clamp(-1.0, 1.0);\n        let nh = dot(nn, hh).clamp(-1.0, 1.0);\n\n        // Calculate F - Fresnel\n        let col_f = {\n            let spectrum_sample = col.to_spectral_sample(wavelength);\n            let rev_fresnel = 1.0 - fresnel;\n            let c0 = lerp(\n                schlick_fresnel_from_fac(spectrum_sample.e[0], hb),\n                spectrum_sample.e[0],\n                rev_fresnel,\n            );\n            let c1 = lerp(\n                schlick_fresnel_from_fac(spectrum_sample.e[1], hb),\n                spectrum_sample.e[1],\n                rev_fresnel,\n            );\n            let c2 = lerp(\n                schlick_fresnel_from_fac(spectrum_sample.e[2], hb),\n                spectrum_sample.e[2],\n                rev_fresnel,\n            );\n            let c3 = lerp(\n                schlick_fresnel_from_fac(spectrum_sample.e[3], hb),\n                spectrum_sample.e[3],\n                rev_fresnel,\n            );\n\n            SpectralSample::from_parts(Vec4::new(c0, c1, c2, c3), wavelength)\n        };\n\n        // Calculate everything else\n        if roughness == 0.0 {\n            // If sharp mirror, just return col * fresnel factor\n            return (col_f, 0.0);\n        } else {\n            // Calculate D - Distribution\n            let dist = ggx_d(nh, roughness) / na;\n\n            // Calculate G1 and G2- Geometric microfacet shadowing\n            let g1 = ggx_g(ha, na, roughness);\n            let g2 = ggx_g(hb, nb, roughness);\n\n            // Final result\n            (col_f * (dist * g1 * g2) * INV_PI, dist * INV_PI)\n        }\n    }\n\n    pub fn estimate_eval_over_sphere_light(\n        _col: Color,\n        roughness: f32,\n        _fresnel: f32,\n        inc: Vector,\n        to_light_center: Vector,\n        light_radius_squared: f32,\n        nor: Normal,\n        nor_g: Normal,\n    ) -> f32 {\n        // TODO: all of the stuff in this function is horribly hacky.\n        // Find a proper way to approximate the light contribution from a\n        // solid angle.\n\n        let _ = nor_g; // Not using this, silence warning\n\n        let dist2 = to_light_center.length2();\n        let sin_theta_max2 = (light_radius_squared / dist2).min(1.0);\n        let cos_theta_max = (1.0 - sin_theta_max2).sqrt();\n\n        assert!(cos_theta_max >= -1.0);\n        assert!(cos_theta_max <= 1.0);\n\n        // Surface normal\n        let nn = if dot(nor.into_vector(), inc) < 0.0 {\n            nor.normalized()\n        } else {\n            -nor.normalized() // If back-facing, flip normal\n        }\n        .into_vector();\n\n        let aa = -inc.normalized(); // Vector pointing to where \"in\" came from\n        let bb = to_light_center.normalized(); // Out\n\n        // Brute-force method\n        //let mut fac = 0.0;\n        //const N: usize = 256;\n        //for i in 0..N {\n        //    let uu = Halton::sample(0, i);\n        //    let vv = Halton::sample(1, i);\n        //    let mut samp = uniform_sample_cone(uu, vv, cos_theta_max);\n        //    samp = zup_to_vec(samp, bb).normalized();\n        //    if dot(nn, samp) > 0.0 {\n        //        let hh = (aa+samp).normalized();\n        //        fac += ggx_d(dot(nn, hh), roughness);\n        //    }\n        //}\n        //fac /= N * N;\n\n        // Approximate method\n        let theta = cos_theta_max.acos();\n        let hh = (aa + bb).normalized();\n        let nh = dot(nn, hh).clamp(-1.0, 1.0);\n        let fac = ggx_d(nh, (1.0f32).min(roughness.sqrt() + (2.0 * theta / PI_32)));\n\n        fac * (1.0f32).min(1.0 - cos_theta_max) * INV_PI\n    }\n\n    //----------------------------------------------------\n\n    // Returns the cosine of the half-angle that should be sampled, given\n    // a random variable in [0,1]\n    fn half_theta_sample(u: f32, rough: f32) -> f32 {\n        let rough2 = rough * rough;\n\n        // Calculate top half of equation\n        let top = 1.0 - u;\n\n        // Calculate bottom half of equation\n        let bottom = 1.0 + ((rough2 - 1.0) * u);\n\n        (top / bottom).sqrt()\n    }\n\n    /// The GGX microfacet distribution function.\n    ///\n    /// nh: cosine of the angle between the surface normal and the microfacet normal.\n    fn ggx_d(nh: f32, rough: f32) -> f32 {\n        if nh <= 0.0 {\n            return 0.0;\n        }\n\n        let rough2 = rough * rough;\n        let tmp = 1.0 + ((rough2 - 1.0) * (nh * nh));\n        rough2 / (PI_32 * tmp * tmp)\n    }\n\n    /// The GGX Smith shadow-masking function.\n    ///\n    /// vh: cosine of the angle between the view vector and the microfacet normal.\n    /// vn: cosine of the angle between the view vector and surface normal.\n    fn ggx_g(vh: f32, vn: f32, rough: f32) -> f32 {\n        if (vh * vn) <= 0.0 {\n            0.0\n        } else {\n            2.0 / (1.0 + (1.0 + rough * rough * (1.0 - vn * vn) / (vn * vn)).sqrt())\n        }\n    }\n}\n\n/// Emit closure code.\n///\n/// NOTE: this needs to be handled specially by the integrator!  It does not\n/// behave like a standard closure!\nmod emit_closure {\n    use super::*;\n\n    pub fn sample(\n        color: Color,\n        inc: Vector,\n        nor: Normal,\n        nor_g: Normal,\n        uv: (f32, f32),\n        wavelength: f32,\n    ) -> (Vector, SpectralSample, f32) {\n        let _ = (inc, nor, nor_g, uv); // Not using these, silence warning\n\n        (\n            Vector::new(0.0, 0.0, 0.0),\n            color.to_spectral_sample(wavelength),\n            1.0,\n        )\n    }\n\n    pub fn evaluate(\n        color: Color,\n        inc: Vector,\n        out: Vector,\n        nor: Normal,\n        nor_g: Normal,\n        wavelength: f32,\n    ) -> (SpectralSample, f32) {\n        let _ = (inc, out, nor, nor_g); // Not using these, silence warning\n\n        (color.to_spectral_sample(wavelength), 1.0)\n    }\n\n    pub fn estimate_eval_over_sphere_light(\n        _color: Color,\n        _inc: Vector,\n        _to_light_center: Vector,\n        _light_radius_squared: f32,\n        _nor: Normal,\n        _nor_g: Normal,\n    ) -> f32 {\n        // TODO: what to do here?\n        unimplemented!()\n    }\n}\n\n//=============================================================================\n\n/// Utility function that calculates the fresnel reflection factor of a given\n/// incoming ray against a surface with the given normal-reflectance factor.\n///\n/// `frensel_fac`: The ratio of light reflected back if the ray were to\n///                hit the surface head-on (perpendicular to the surface).\n/// `c`: The cosine of the angle between the incoming light and the\n///      surface's normal.  Probably calculated e.g. with a normalized\n///      dot product.\n#[allow(dead_code)]\nfn dielectric_fresnel_from_fac(fresnel_fac: f32, c: f32) -> f32 {\n    let tmp1 = fresnel_fac.sqrt() - 1.0;\n\n    // Protect against divide by zero.\n    if tmp1.abs() < 0.000_001 {\n        return 1.0;\n    }\n\n    // Find the ior ratio\n    let tmp2 = (-2.0 / tmp1) - 1.0;\n    let ior_ratio = tmp2 * tmp2;\n\n    // Calculate fresnel factor\n    dielectric_fresnel(ior_ratio, c)\n}\n\n/// Schlick's approximation version of `dielectric_fresnel_from_fac()` above.\n#[allow(dead_code)]\nfn schlick_fresnel_from_fac(frensel_fac: f32, c: f32) -> f32 {\n    let c1 = 1.0 - c;\n    let c2 = c1 * c1;\n    frensel_fac + ((1.0 - frensel_fac) * c1 * c2 * c2)\n}\n\n/// Utility function that calculates the fresnel reflection factor of a given\n/// incoming ray against a surface with the given ior outside/inside ratio.\n///\n/// `ior_ratio`: The ratio of the outside material ior (probably 1.0 for air)\n///              over the inside ior.\n/// `c`: The cosine of the angle between the incoming light and the\n///      surface's normal.  Probably calculated e.g. with a normalized\n///      dot product.\n#[allow(dead_code)]\nfn dielectric_fresnel(ior_ratio: f32, c: f32) -> f32 {\n    let g = (ior_ratio - 1.0 + (c * c)).sqrt();\n\n    let f1 = g - c;\n    let f2 = g + c;\n    let f3 = (f1 * f1) / (f2 * f2);\n\n    let f4 = (c * f2) - 1.0;\n    let f5 = (c * f1) + 1.0;\n    let f6 = 1.0 + ((f4 * f4) / (f5 * f5));\n\n    0.5 * f3 * f6\n}\n\n/// Schlick's approximation of the fresnel reflection factor.\n///\n/// Same interface as `dielectric_fresnel()`, above.\n#[allow(dead_code)]\nfn schlick_fresnel(ior_ratio: f32, c: f32) -> f32 {\n    let f1 = (1.0 - ior_ratio) / (1.0 + ior_ratio);\n    let f2 = f1 * f1;\n    let c1 = 1.0 - c;\n    let c2 = c1 * c1;\n\n    f2 + ((1.0 - f2) * c1 * c2 * c2)\n}\n"
  },
  {
    "path": "src/surface/bilinear_patch.rs",
    "content": "use super::{point_order, PointOrder, Splitable, MAX_EDGE_DICE};\nuse crate::{\n    lerp::{lerp, lerp_slice},\n    math::Point,\n};\n\n#[derive(Debug, Copy, Clone)]\npub struct BilinearPatch<'a> {\n    // The control points are stored in clockwise order, like this:\n    //  u ----->\n    // v  0  1\n    // |  3  2\n    // \\/\n    control_points: &'a [[Point; 4]],\n\n    // Indicates if any of the edges *must* be split, for example if there\n    // are adjacent patches that were split for non-dicing reasons.\n    //\n    // Matching the ascii graph above, the edges are:\n    //\n    //      0\n    //   -------\n    // 3 |     | 1\n    //   -------\n    //      2\n    must_split: [bool; 4],\n}\n\nfn bilerp_point(patch: [Point; 4], uv: (f32, f32)) -> Point {\n    let a = lerp(patch[0], patch[1], uv.0);\n    let b = lerp(patch[3], patch[2], uv.0);\n    lerp(a, b, uv.1)\n}\n\n#[derive(Debug, Copy, Clone)]\npub struct BilinearSubPatch<'a> {\n    original: &'a BilinearPatch<'a>,\n    clip: [(f32, f32); 4],\n    must_split: [bool; 4],\n}\n\nimpl<'a> Splitable for BilinearSubPatch<'a> {\n    fn split<F>(&self, metric: F) -> Option<(Self, Self)>\n    where\n        F: Fn(Point, Point) -> f32,\n    {\n        // Get the points of the sub-patch at time 0.5.\n        let patch = lerp_slice(self.original.control_points, 0.5);\n        let points = [\n            bilerp_point(patch, self.clip[0]),\n            bilerp_point(patch, self.clip[1]),\n            bilerp_point(patch, self.clip[2]),\n            bilerp_point(patch, self.clip[3]),\n        ];\n\n        // Calculate edge metrics.\n        let edge_metric = [\n            metric(points[0], points[1]),\n            metric(points[1], points[2]),\n            metric(points[2], points[3]),\n            metric(points[3], points[0]),\n        ];\n\n        // Find an edge to split, if any.\n        let split_edge_index = {\n            // Find the \"longest\" edge in terms of the metric.\n            let (edge_i, m) = edge_metric\n                .iter()\n                .enumerate()\n                .max_by(|a, b| {\n                    if a.1 > b.1 {\n                        std::cmp::Ordering::Greater\n                    } else {\n                        std::cmp::Ordering::Less\n                    }\n                })\n                .unwrap();\n\n            // Return an edge to split, if a split is needed.\n            if *m > MAX_EDGE_DICE as f32 {\n                // Split needed because of over-long edge.\n                Some(edge_i)\n            } else {\n                // Return the the first edge with \"must_split\" set, if any.\n                // Otherwise returns `None`.\n                self.must_split\n                    .iter()\n                    .enumerate()\n                    .find(|a| *a.1)\n                    .map(|a| a.0)\n            }\n        };\n\n        // Do the split if needed\n        if let Some(i) = split_edge_index {\n            let edge_1 = (i, (i + 1) % 4);\n            let edge_2 = ((i + 2) % 4, (i + 3) % 4);\n            let new_must_split = {\n                let mut new_must_split = self.must_split;\n                new_must_split[edge_1.0] = false;\n                new_must_split[edge_2.0] = false;\n                new_must_split\n            };\n\n            let midpoint_1 = lerp(self.clip[edge_1.0], self.clip[edge_1.1], 0.5);\n            let midpoint_2 = {\n                let alpha =\n                    if self.must_split[edge_2.0] || edge_metric[edge_2.0] > MAX_EDGE_DICE as f32 {\n                        0.5\n                    } else {\n                        let edge_2_dice_rate = edge_metric[edge_2.0].ceil();\n                        (edge_2_dice_rate * 0.5).floor() / edge_2_dice_rate\n                    };\n\n                match point_order(points[edge_2.0], points[edge_2.1]) {\n                    PointOrder::AsIs => lerp(self.clip[edge_2.0], self.clip[edge_2.1], alpha),\n                    PointOrder::Flip => lerp(self.clip[edge_2.1], self.clip[edge_2.0], alpha),\n                }\n            };\n\n            // Build the new sub-patches\n            let mut patch_1 = BilinearSubPatch {\n                original: self.original,\n                clip: self.clip,\n                must_split: new_must_split,\n            };\n            let mut patch_2 = patch_1;\n            patch_1.clip[edge_1.1] = midpoint_1;\n            patch_1.clip[edge_2.0] = midpoint_2;\n            patch_2.clip[edge_1.0] = midpoint_1;\n            patch_2.clip[edge_2.1] = midpoint_2;\n\n            Some((patch_1, patch_2))\n        } else {\n            // No split\n            None\n        }\n    }\n}\n"
  },
  {
    "path": "src/surface/micropoly_batch.rs",
    "content": "#![allow(dead_code)]\n\nuse std::collections::HashMap;\n\nuse kioku::Arena;\n\nuse crate::{\n    accel::BVH4,\n    bbox::BBox,\n    boundable::Boundable,\n    lerp::lerp_slice,\n    math::{cross, dot, Normal, Point, Transform},\n    ray::{RayBatch, RayStack},\n    shading::SurfaceClosure,\n};\n\nuse super::{triangle, SurfaceIntersection, SurfaceIntersectionData};\n\nconst MAX_LEAF_TRIANGLE_COUNT: usize = 3;\n\n/// This is the core surface primitive for rendering: all surfaces are\n/// ultimately processed into pre-shaded micropolygon batches for rendering.\n///\n/// It is essentially a triangle soup that shares the same surface shader.\n/// The parameters of that shader can vary over the triangles, but its\n/// structure cannot.\n#[derive(Copy, Clone, Debug)]\npub struct MicropolyBatch<'a> {\n    // Vertices and associated normals.  Time samples for the same vertex are\n    // laid out next to each other in a contiguous slice.\n    time_sample_count: usize,\n    vertices: &'a [Point],\n    normals: &'a [Normal],\n\n    // Per-vertex shading data.\n    compressed_vertex_closure_size: usize, // Size in bites of a single compressed closure\n    vertex_closure_time_sample_count: usize,\n    compressed_vertex_closures: &'a [u8], // Packed compressed closures\n\n    // Micro-triangle indices.  Each element of the tuple specifies the index\n    // of a vertex, which indexes into all of the arrays above.\n    indices: &'a [(u32, u32, u32)],\n\n    // Acceleration structure for fast ray intersection testing.\n    accel: BVH4<'a>,\n}\n\nimpl<'a> MicropolyBatch<'a> {\n    pub fn from_verts_and_indices<'b>(\n        arena: &'b Arena,\n        verts: &[Vec<Point>],\n        vert_normals: &[Vec<Normal>],\n        tri_indices: &[(usize, usize, usize)],\n    ) -> MicropolyBatch<'b> {\n        let vert_count = verts[0].len();\n        let time_sample_count = verts.len();\n\n        // Copy verts over to a contiguous area of memory, reorganizing them\n        // so that each vertices' time samples are contiguous in memory.\n        let vertices = {\n            let vertices = arena.alloc_array_uninit(vert_count * time_sample_count);\n\n            for vi in 0..vert_count {\n                for ti in 0..time_sample_count {\n                    unsafe {\n                        *vertices[(vi * time_sample_count) + ti].as_mut_ptr() = verts[ti][vi];\n                    }\n                }\n            }\n\n            unsafe { std::mem::transmute(vertices) }\n        };\n\n        // Copy vertex normals, if any, organizing them the same as vertices\n        // above.\n        let normals = {\n            let normals = arena.alloc_array_uninit(vert_count * time_sample_count);\n\n            for vi in 0..vert_count {\n                for ti in 0..time_sample_count {\n                    unsafe {\n                        *normals[(vi * time_sample_count) + ti].as_mut_ptr() = vert_normals[ti][vi];\n                    }\n                }\n            }\n\n            unsafe { std::mem::transmute(&normals[..]) }\n        };\n\n        // Copy triangle vertex indices over, appending the triangle index itself to the tuple\n        let indices: &mut [(u32, u32, u32)] = {\n            let indices = arena.alloc_array_uninit(tri_indices.len());\n            for (i, tri_i) in tri_indices.iter().enumerate() {\n                unsafe {\n                    *indices[i].as_mut_ptr() = (tri_i.0 as u32, tri_i.2 as u32, tri_i.1 as u32);\n                }\n            }\n            unsafe { std::mem::transmute(indices) }\n        };\n\n        // Create bounds array for use during BVH construction\n        let (bounds, bounds_map) = {\n            let mut bounds = Vec::with_capacity(indices.len() * time_sample_count);\n            let mut bounds_map = HashMap::new();\n\n            for tri in tri_indices {\n                let start = bounds.len();\n                for ti in 0..time_sample_count {\n                    let p0 = verts[ti][tri.0];\n                    let p1 = verts[ti][tri.1];\n                    let p2 = verts[ti][tri.2];\n                    let minimum = p0.min(p1.min(p2));\n                    let maximum = p0.max(p1.max(p2));\n                    bounds.push(BBox::from_points(minimum, maximum));\n                }\n                let end = bounds.len();\n                bounds_map.insert((tri.0 as u32, tri.1 as u32, tri.2 as u32), (start, end));\n            }\n            (bounds, bounds_map)\n        };\n\n        // Build BVH\n        let accel = BVH4::from_objects(arena, &mut indices[..], MAX_LEAF_TRIANGLE_COUNT, |tri| {\n            let (start, end) = bounds_map[tri];\n            &bounds[start..end]\n        });\n\n        MicropolyBatch {\n            time_sample_count: time_sample_count,\n            vertices: vertices,\n            normals: normals,\n            compressed_vertex_closure_size: 0,\n            vertex_closure_time_sample_count: 1,\n            compressed_vertex_closures: &[],\n            indices: indices,\n            accel: accel,\n        }\n    }\n}\n\nimpl<'a> Boundable for MicropolyBatch<'a> {\n    fn bounds(&self) -> &[BBox] {\n        self.accel.bounds()\n    }\n}\n\nimpl<'a> MicropolyBatch<'a> {\n    fn intersect_rays(\n        &self,\n        rays: &mut RayBatch,\n        ray_stack: &mut RayStack,\n        isects: &mut [SurfaceIntersection],\n        space: &[Transform],\n    ) {\n        // Precalculate transform for non-motion blur cases\n        let static_mat_space = if space.len() == 1 {\n            lerp_slice(space, 0.0).inverse()\n        } else {\n            Transform::new()\n        };\n\n        self.accel\n            .traverse(rays, ray_stack, |idx_range, rays, ray_stack| {\n                let tri_count = idx_range.end - idx_range.start;\n\n                // Build the triangle cache if we can!\n                let is_cached = ray_stack.ray_count_in_next_task() >= tri_count\n                    && self.time_sample_count == 1\n                    && space.len() <= 1;\n                let mut tri_cache = [std::mem::MaybeUninit::uninit(); MAX_LEAF_TRIANGLE_COUNT];\n                if is_cached {\n                    for tri_idx in idx_range.clone() {\n                        let i = tri_idx - idx_range.start;\n                        let tri_indices = self.indices[tri_idx];\n\n                        // For static triangles with static transforms, cache them.\n                        unsafe {\n                            *tri_cache[i].as_mut_ptr() = (\n                                self.vertices[tri_indices.0 as usize],\n                                self.vertices[tri_indices.1 as usize],\n                                self.vertices[tri_indices.2 as usize],\n                            );\n                            if !space.is_empty() {\n                                (*tri_cache[i].as_mut_ptr()).0 =\n                                    (*tri_cache[i].as_mut_ptr()).0 * static_mat_space;\n                                (*tri_cache[i].as_mut_ptr()).1 =\n                                    (*tri_cache[i].as_mut_ptr()).1 * static_mat_space;\n                                (*tri_cache[i].as_mut_ptr()).2 =\n                                    (*tri_cache[i].as_mut_ptr()).2 * static_mat_space;\n                            }\n                        }\n                    }\n                }\n\n                // Test each ray against the triangles.\n                ray_stack.do_next_task(|ray_idx| {\n                    let ray_idx = ray_idx as usize;\n\n                    if rays.is_done(ray_idx) {\n                        return;\n                    }\n\n                    let ray_time = rays.time(ray_idx);\n\n                    // Calculate the ray space, if necessary.\n                    let mat_space = if space.len() > 1 {\n                        // Per-ray transform, for motion blur\n                        lerp_slice(space, ray_time).inverse()\n                    } else {\n                        static_mat_space\n                    };\n\n                    // Iterate through the triangles and test the ray against them.\n                    let mut non_shadow_hit = false;\n                    let mut hit_tri = std::mem::MaybeUninit::uninit();\n                    let mut hit_tri_indices = std::mem::MaybeUninit::uninit();\n                    let mut hit_tri_data = std::mem::MaybeUninit::uninit();\n                    let ray_pre = triangle::RayTriPrecompute::new(rays.dir(ray_idx));\n                    for tri_idx in idx_range.clone() {\n                        let tri_indices = self.indices[tri_idx];\n\n                        // Get triangle if necessary\n                        let tri = if is_cached {\n                            let i = tri_idx - idx_range.start;\n                            unsafe { tri_cache[i].assume_init() }\n                        } else {\n                            let mut tri = if self.time_sample_count == 1 {\n                                // No deformation motion blur, so fast-path it.\n                                (\n                                    self.vertices[tri_indices.0 as usize],\n                                    self.vertices[tri_indices.1 as usize],\n                                    self.vertices[tri_indices.2 as usize],\n                                )\n                            } else {\n                                // Deformation motion blur, need to interpolate.\n                                let p0_slice = &self.vertices[(tri_indices.0 as usize\n                                    * self.time_sample_count)\n                                    ..((tri_indices.0 as usize + 1) * self.time_sample_count)];\n                                let p1_slice = &self.vertices[(tri_indices.1 as usize\n                                    * self.time_sample_count)\n                                    ..((tri_indices.1 as usize + 1) * self.time_sample_count)];\n                                let p2_slice = &self.vertices[(tri_indices.2 as usize\n                                    * self.time_sample_count)\n                                    ..((tri_indices.2 as usize + 1) * self.time_sample_count)];\n\n                                let p0 = lerp_slice(p0_slice, ray_time);\n                                let p1 = lerp_slice(p1_slice, ray_time);\n                                let p2 = lerp_slice(p2_slice, ray_time);\n\n                                (p0, p1, p2)\n                            };\n\n                            if !space.is_empty() {\n                                tri.0 = tri.0 * mat_space;\n                                tri.1 = tri.1 * mat_space;\n                                tri.2 = tri.2 * mat_space;\n                            }\n\n                            tri\n                        };\n\n                        // Test ray against triangle\n                        if let Some((t, b0, b1, b2)) = triangle::intersect_ray(\n                            rays.orig(ray_idx),\n                            ray_pre,\n                            rays.max_t(ray_idx),\n                            tri,\n                        ) {\n                            if rays.is_occlusion(ray_idx) {\n                                isects[ray_idx] = SurfaceIntersection::Occlude;\n                                rays.mark_done(ray_idx);\n                                break;\n                            } else {\n                                non_shadow_hit = true;\n                                rays.set_max_t(ray_idx, t);\n                                unsafe {\n                                    *hit_tri.as_mut_ptr() = tri;\n                                    *hit_tri_indices.as_mut_ptr() = tri_indices;\n                                    *hit_tri_data.as_mut_ptr() = (t, b0, b1, b2);\n                                }\n                            }\n                        }\n                    }\n\n                    // Calculate intersection data if necessary.\n                    if non_shadow_hit {\n                        let hit_tri = unsafe { hit_tri.assume_init() };\n                        let hit_tri_indices = unsafe { hit_tri_indices.assume_init() };\n                        let (t, b0, b1, b2) = unsafe { hit_tri_data.assume_init() };\n\n                        // Calculate intersection point and error magnitudes\n                        let (pos, pos_err) = triangle::surface_point(hit_tri, (b0, b1, b2));\n\n                        // Calculate geometric surface normal\n                        let geo_normal =\n                            cross(hit_tri.0 - hit_tri.1, hit_tri.0 - hit_tri.2).into_normal();\n\n                        // Calculate interpolated surface normal\n                        let shading_normal = {\n                            let n0_slice = &self.normals[(hit_tri_indices.0 as usize\n                                * self.time_sample_count)\n                                ..((hit_tri_indices.0 as usize + 1) * self.time_sample_count)];\n                            let n1_slice = &self.normals[(hit_tri_indices.1 as usize\n                                * self.time_sample_count)\n                                ..((hit_tri_indices.1 as usize + 1) * self.time_sample_count)];\n                            let n2_slice = &self.normals[(hit_tri_indices.2 as usize\n                                * self.time_sample_count)\n                                ..((hit_tri_indices.2 as usize + 1) * self.time_sample_count)];\n\n                            let n0 = lerp_slice(n0_slice, ray_time).normalized();\n                            let n1 = lerp_slice(n1_slice, ray_time).normalized();\n                            let n2 = lerp_slice(n2_slice, ray_time).normalized();\n\n                            let s_nor = ((n0 * b0) + (n1 * b1) + (n2 * b2)) * mat_space;\n                            if dot(s_nor, geo_normal) >= 0.0 {\n                                s_nor\n                            } else {\n                                -s_nor\n                            }\n                        };\n\n                        // Calculate interpolated surface closure.\n                        // TODO: actually interpolate.\n                        let closure = {\n                            let start_byte = hit_tri_indices.0 as usize\n                                * self.compressed_vertex_closure_size\n                                * self.vertex_closure_time_sample_count;\n                            let end_byte = start_byte + self.compressed_vertex_closure_size;\n                            let (closure, _) = SurfaceClosure::from_compressed(\n                                &self.compressed_vertex_closures[start_byte..end_byte],\n                            );\n                            closure\n                        };\n\n                        let intersection_data = SurfaceIntersectionData {\n                            incoming: rays.dir(ray_idx),\n                            t: t,\n                            pos: pos,\n                            pos_err: pos_err,\n                            nor: shading_normal,\n                            nor_g: geo_normal,\n                            local_space: mat_space,\n                            sample_pdf: 0.0,\n                        };\n\n                        // Fill in intersection data\n                        isects[ray_idx] = SurfaceIntersection::Hit {\n                            intersection_data: intersection_data,\n                            closure: closure,\n                        };\n                    }\n                });\n                ray_stack.pop_task();\n            });\n    }\n}\n"
  },
  {
    "path": "src/surface/mod.rs",
    "content": "#![allow(dead_code)]\n\n// pub mod micropoly_batch;\npub mod bilinear_patch;\npub mod micropoly_batch;\npub mod triangle;\npub mod triangle_mesh;\n\nuse std::fmt::Debug;\n\nuse crate::{\n    boundable::Boundable,\n    math::{Normal, Point, Transform, Vector},\n    ray::{RayBatch, RayStack},\n    shading::surface_closure::SurfaceClosure,\n    shading::SurfaceShader,\n};\n\nconst MAX_EDGE_DICE: u32 = 128;\n\npub trait Surface: Boundable + Debug + Sync {\n    fn intersect_rays(\n        &self,\n        rays: &mut RayBatch,\n        ray_stack: &mut RayStack,\n        isects: &mut [SurfaceIntersection],\n        shader: &dyn SurfaceShader,\n        space: &[Transform],\n    );\n}\n\npub trait Splitable: Copy {\n    /// Splits the surface into two pieces if necessary.\n    fn split<F>(&self, metric: F) -> Option<(Self, Self)>\n    where\n        F: Fn(Point, Point) -> f32;\n}\n\n#[derive(Debug, Copy, Clone)]\npub enum PointOrder {\n    AsIs,\n    Flip,\n}\n\npub fn point_order(p1: Point, p2: Point) -> PointOrder {\n    let max_diff = {\n        let v = p2 - p1;\n        let v_abs = v.abs();\n\n        let mut diff = v.x();\n        let mut diff_abs = v_abs.x();\n        if v_abs.y() > diff_abs {\n            diff = v.y();\n            diff_abs = v_abs.y();\n        }\n        if v_abs.z() > diff_abs {\n            diff = v.z();\n        }\n\n        diff\n    };\n\n    if max_diff <= 0.0 {\n        PointOrder::AsIs\n    } else {\n        PointOrder::Flip\n    }\n}\n\n#[derive(Debug, Copy, Clone)]\n#[allow(clippy::large_enum_variant)]\npub enum SurfaceIntersection {\n    Miss,\n    Occlude,\n    Hit {\n        intersection_data: SurfaceIntersectionData,\n        closure: SurfaceClosure,\n    },\n}\n\n#[derive(Debug, Copy, Clone)]\npub struct SurfaceIntersectionData {\n    pub incoming: Vector, // Direction of the incoming ray\n    pub pos: Point,       // Position of the intersection\n    pub pos_err: f32,     // Error magnitude of the intersection position.  Imagine\n    // a cube centered around `pos` with dimensions of `2 * pos_err`.\n    pub nor: Normal,            // Shading normal\n    pub nor_g: Normal,          // True geometric normal\n    pub local_space: Transform, // Matrix from global space to local space\n    pub t: f32,                 // Ray t-value at the intersection point\n    pub sample_pdf: f32,        // The PDF of getting this point by explicitly sampling the surface\n}\n"
  },
  {
    "path": "src/surface/triangle.rs",
    "content": "#![allow(dead_code)]\n\nuse crate::{\n    fp_utils::fp_gamma,\n    math::{Point, Vector},\n};\n\n#[derive(Debug, Copy, Clone)]\npub struct RayTriPrecompute {\n    i: (usize, usize, usize),\n    s: (f32, f32, f32),\n}\n\nimpl RayTriPrecompute {\n    pub fn new(ray_dir: Vector) -> RayTriPrecompute {\n        // Calculate the permuted dimension indices for the new ray space.\n        let (xi, yi, zi) = {\n            let xabs = ray_dir.x().abs();\n            let yabs = ray_dir.y().abs();\n            let zabs = ray_dir.z().abs();\n\n            if xabs > yabs && xabs > zabs {\n                (1, 2, 0)\n            } else if yabs > zabs {\n                (2, 0, 1)\n            } else {\n                (0, 1, 2)\n            }\n        };\n\n        let dir_x = ray_dir.get_n(xi);\n        let dir_y = ray_dir.get_n(yi);\n        let dir_z = ray_dir.get_n(zi);\n\n        // Calculate shear constants.\n        let sx = dir_x / dir_z;\n        let sy = dir_y / dir_z;\n        let sz = 1.0 / dir_z;\n\n        RayTriPrecompute {\n            i: (xi, yi, zi),\n            s: (sx, sy, sz),\n        }\n    }\n}\n\n/// Intersects `ray` with `tri`, returning `Some((t, b0, b1, b2))`, or `None`\n/// if no intersection.\n///\n/// Returned values:\n///\n/// * `t` is the ray t at the hit point.\n/// * `b0`, `b1`, and `b2` are the barycentric coordinates of the triangle at\n///   the hit point.\n///\n/// Uses the ray-triangle test from the paper \"Watertight Ray/Triangle\n/// Intersection\" by Woop et al.\npub fn intersect_ray(\n    ray_orig: Point,\n    ray_pre: RayTriPrecompute,\n    ray_max_t: f32,\n    tri: (Point, Point, Point),\n) -> Option<(f32, f32, f32, f32)> {\n    // Calculate vertices in ray space.\n    let p0 = tri.0 - ray_orig;\n    let p1 = tri.1 - ray_orig;\n    let p2 = tri.2 - ray_orig;\n\n    let p0x = p0.get_n(ray_pre.i.0) - (ray_pre.s.0 * p0.get_n(ray_pre.i.2));\n    let p0y = p0.get_n(ray_pre.i.1) - (ray_pre.s.1 * p0.get_n(ray_pre.i.2));\n    let p1x = p1.get_n(ray_pre.i.0) - (ray_pre.s.0 * p1.get_n(ray_pre.i.2));\n    let p1y = p1.get_n(ray_pre.i.1) - (ray_pre.s.1 * p1.get_n(ray_pre.i.2));\n    let p2x = p2.get_n(ray_pre.i.0) - (ray_pre.s.0 * p2.get_n(ray_pre.i.2));\n    let p2y = p2.get_n(ray_pre.i.1) - (ray_pre.s.1 * p2.get_n(ray_pre.i.2));\n\n    // Calculate scaled barycentric coordinates.\n    let mut e0 = (p1x * p2y) - (p1y * p2x);\n    let mut e1 = (p2x * p0y) - (p2y * p0x);\n    let mut e2 = (p0x * p1y) - (p0y * p1x);\n\n    // Fallback to test against edges using double precision.\n    if e0 == 0.0 || e1 == 0.0 || e2 == 0.0 {\n        e0 = ((p1x as f64 * p2y as f64) - (p1y as f64 * p2x as f64)) as f32;\n        e1 = ((p2x as f64 * p0y as f64) - (p2y as f64 * p0x as f64)) as f32;\n        e2 = ((p0x as f64 * p1y as f64) - (p0y as f64 * p1x as f64)) as f32;\n    }\n\n    // Check if the ray hit the triangle.\n    if (e0 < 0.0 || e1 < 0.0 || e2 < 0.0) && (e0 > 0.0 || e1 > 0.0 || e2 > 0.0) {\n        return None;\n    }\n\n    // Determinant\n    let det = e0 + e1 + e2;\n    if det == 0.0 {\n        return None;\n    }\n\n    // Calculate t of hitpoint.\n    let p0z = ray_pre.s.2 * p0.get_n(ray_pre.i.2);\n    let p1z = ray_pre.s.2 * p1.get_n(ray_pre.i.2);\n    let p2z = ray_pre.s.2 * p2.get_n(ray_pre.i.2);\n    let t_scaled = (e0 * p0z) + (e1 * p1z) + (e2 * p2z);\n\n    // Check if the hitpoint t is within ray min/max t.\n    if (det > 0.0 && (t_scaled <= 0.0 || t_scaled > (ray_max_t * det)))\n        || (det < 0.0 && (t_scaled >= 0.0 || t_scaled < (ray_max_t * det)))\n    {\n        return None;\n    }\n\n    // Calculate t and the hitpoint barycentric coordinates.\n    let inv_det = 1.0 / det;\n    let b0 = e0 * inv_det;\n    let b1 = e1 * inv_det;\n    let b2 = e2 * inv_det;\n    let t = t_scaled * inv_det;\n\n    // Check error bounds on t for very close hit points.\n    // The technique used here is from \"Physically Based Rendering: From Theory\n    // to Implementation\" third edition by Pharr et al.\n    {\n        // Calculate delta z\n        let max_zt = max_abs_3(p0z, p1z, p2z);\n        let dz = fp_gamma(3) * max_zt;\n\n        // Calculate delta x and y\n        let max_xt = max_abs_3(p0x, p1x, p2x);\n        let max_yt = max_abs_3(p0y, p1y, p2y);\n        let dx = fp_gamma(5) * (max_xt + max_zt);\n        let dy = fp_gamma(5) * (max_yt + max_zt);\n\n        // Calculate delta e\n        let de = 2.0 * ((fp_gamma(2) * max_xt * max_yt) + (dy * max_xt + dx * max_yt));\n\n        // Calculate delta t\n        let max_e = max_abs_3(e0, e1, e2);\n        let dt =\n            3.0 * ((fp_gamma(3) * max_e * max_zt) + (de * max_zt + dz * max_e)) * inv_det.abs();\n\n        // Finally, do the check\n        if t <= dt {\n            return None;\n        }\n    }\n\n    // Return t and barycentric coordinates\n    Some((t, b0, b1, b2))\n}\n\n/// Calculates a point on a triangle's surface at the given barycentric\n/// coordinates.\n///\n/// Returns the point and the error magnitude of the point.\npub fn surface_point(tri: (Point, Point, Point), bary: (f32, f32, f32)) -> (Point, f32) {\n    let pos = ((tri.0.into_vector() * bary.0)\n        + (tri.1.into_vector() * bary.1)\n        + (tri.2.into_vector() * bary.2))\n        .into_point();\n\n    let pos_err = (((tri.0.into_vector().abs() * bary.0)\n        + (tri.1.into_vector().abs() * bary.1)\n        + (tri.2.into_vector().abs() * bary.2))\n        * fp_gamma(7))\n    .co\n    .max_element();\n\n    (pos, pos_err)\n}\n\nfn max_abs_3(a: f32, b: f32, c: f32) -> f32 {\n    let a = a.abs();\n    let b = b.abs();\n    let c = c.abs();\n\n    if a > b && a > c {\n        a\n    } else if b > c {\n        b\n    } else {\n        c\n    }\n}\n"
  },
  {
    "path": "src/surface/triangle_mesh.rs",
    "content": "#![allow(dead_code)]\n\nuse kioku::Arena;\n\nuse crate::{\n    accel::BVH4,\n    bbox::BBox,\n    boundable::Boundable,\n    lerp::lerp_slice,\n    math::{cross, dot, Normal, Point, Transform},\n    ray::{RayBatch, RayStack},\n    shading::SurfaceShader,\n};\n\nuse super::{triangle, Surface, SurfaceIntersection, SurfaceIntersectionData};\n\nconst MAX_LEAF_TRIANGLE_COUNT: usize = 3;\n\n#[derive(Copy, Clone, Debug)]\npub struct TriangleMesh<'a> {\n    time_sample_count: usize,\n    vertices: &'a [Point], // Vertices, with the time samples for each vertex stored contiguously\n    normals: Option<&'a [Normal]>, // Vertex normals, organized the same as `vertices`\n    indices: &'a [(u32, u32, u32, u32)], // (v0_idx, v1_idx, v2_idx, original_tri_idx)\n    accel: BVH4<'a>,\n}\n\nimpl<'a> TriangleMesh<'a> {\n    pub fn from_verts_and_indices<'b>(\n        arena: &'b Arena,\n        verts: &[Vec<Point>],\n        vert_normals: &Option<Vec<Vec<Normal>>>,\n        tri_indices: &[(usize, usize, usize)],\n    ) -> TriangleMesh<'b> {\n        let vert_count = verts[0].len();\n        let time_sample_count = verts.len();\n\n        // Copy verts over to a contiguous area of memory, reorganizing them\n        // so that each vertices' time samples are contiguous in memory.\n        let vertices = {\n            let vertices = arena.alloc_array_uninit(vert_count * time_sample_count);\n\n            for vi in 0..vert_count {\n                for ti in 0..time_sample_count {\n                    unsafe {\n                        *vertices[(vi * time_sample_count) + ti].as_mut_ptr() = verts[ti][vi];\n                    }\n                }\n            }\n\n            unsafe { std::mem::transmute(vertices) }\n        };\n\n        // Copy vertex normals, if any, organizing them the same as vertices\n        // above.\n        let normals = match vert_normals {\n            Some(ref vnors) => {\n                let normals = arena.alloc_array_uninit(vert_count * time_sample_count);\n\n                for vi in 0..vert_count {\n                    for ti in 0..time_sample_count {\n                        unsafe {\n                            *normals[(vi * time_sample_count) + ti].as_mut_ptr() = vnors[ti][vi];\n                        }\n                    }\n                }\n\n                unsafe { Some(std::mem::transmute(&normals[..])) }\n            }\n\n            None => None,\n        };\n\n        // Copy triangle vertex indices over, appending the triangle index itself to the tuple\n        let indices: &mut [(u32, u32, u32, u32)] = {\n            let indices = arena.alloc_array_uninit(tri_indices.len());\n            for (i, tri_i) in tri_indices.iter().enumerate() {\n                unsafe {\n                    *indices[i].as_mut_ptr() =\n                        (tri_i.0 as u32, tri_i.2 as u32, tri_i.1 as u32, i as u32);\n                }\n            }\n            unsafe { std::mem::transmute(indices) }\n        };\n\n        // Create bounds array for use during BVH construction\n        let bounds = {\n            let mut bounds = Vec::with_capacity(indices.len() * time_sample_count);\n            for tri in tri_indices {\n                for ti in 0..time_sample_count {\n                    let p0 = verts[ti][tri.0];\n                    let p1 = verts[ti][tri.1];\n                    let p2 = verts[ti][tri.2];\n                    let minimum = p0.min(p1.min(p2));\n                    let maximum = p0.max(p1.max(p2));\n                    bounds.push(BBox::from_points(minimum, maximum));\n                }\n            }\n            bounds\n        };\n\n        // Build BVH\n        let accel = BVH4::from_objects(arena, &mut indices[..], MAX_LEAF_TRIANGLE_COUNT, |tri| {\n            &bounds\n                [(tri.3 as usize * time_sample_count)..((tri.3 as usize + 1) * time_sample_count)]\n        });\n\n        TriangleMesh {\n            time_sample_count: time_sample_count,\n            vertices: vertices,\n            normals: normals,\n            indices: indices,\n            accel: accel,\n        }\n    }\n}\n\nimpl<'a> Boundable for TriangleMesh<'a> {\n    fn bounds(&self) -> &[BBox] {\n        self.accel.bounds()\n    }\n}\n\nimpl<'a> Surface for TriangleMesh<'a> {\n    fn intersect_rays(\n        &self,\n        rays: &mut RayBatch,\n        ray_stack: &mut RayStack,\n        isects: &mut [SurfaceIntersection],\n        shader: &dyn SurfaceShader,\n        space: &[Transform],\n    ) {\n        // Precalculate transform for non-motion blur cases\n        let static_mat_space = if space.len() == 1 {\n            lerp_slice(space, 0.0).inverse()\n        } else {\n            Transform::new()\n        };\n\n        self.accel\n            .traverse(rays, ray_stack, |idx_range, rays, ray_stack| {\n                let tri_count = idx_range.end - idx_range.start;\n\n                // Build the triangle cache if we can!\n                let is_cached = ray_stack.ray_count_in_next_task() >= tri_count\n                    && self.time_sample_count == 1\n                    && space.len() <= 1;\n                let mut tri_cache = [std::mem::MaybeUninit::uninit(); MAX_LEAF_TRIANGLE_COUNT];\n                if is_cached {\n                    for tri_idx in idx_range.clone() {\n                        let i = tri_idx - idx_range.start;\n                        let tri_indices = self.indices[tri_idx];\n\n                        // For static triangles with static transforms, cache them.\n                        unsafe {\n                            *tri_cache[i].as_mut_ptr() = (\n                                self.vertices[tri_indices.0 as usize],\n                                self.vertices[tri_indices.1 as usize],\n                                self.vertices[tri_indices.2 as usize],\n                            );\n                            if !space.is_empty() {\n                                (*tri_cache[i].as_mut_ptr()).0 =\n                                    (*tri_cache[i].as_mut_ptr()).0 * static_mat_space;\n                                (*tri_cache[i].as_mut_ptr()).1 =\n                                    (*tri_cache[i].as_mut_ptr()).1 * static_mat_space;\n                                (*tri_cache[i].as_mut_ptr()).2 =\n                                    (*tri_cache[i].as_mut_ptr()).2 * static_mat_space;\n                            }\n                        }\n                    }\n                }\n\n                // Test each ray against the triangles.\n                ray_stack.do_next_task(|ray_idx| {\n                    let ray_idx = ray_idx as usize;\n\n                    if rays.is_done(ray_idx) {\n                        return;\n                    }\n\n                    let ray_time = rays.time(ray_idx);\n\n                    // Calculate the ray space, if necessary.\n                    let mat_space = if space.len() > 1 {\n                        // Per-ray transform, for motion blur\n                        lerp_slice(space, ray_time).inverse()\n                    } else {\n                        static_mat_space\n                    };\n\n                    // Iterate through the triangles and test the ray against them.\n                    let mut non_shadow_hit = false;\n                    let mut hit_tri = std::mem::MaybeUninit::uninit();\n                    let mut hit_tri_indices = std::mem::MaybeUninit::uninit();\n                    let mut hit_tri_data = std::mem::MaybeUninit::uninit();\n                    let ray_pre = triangle::RayTriPrecompute::new(rays.dir(ray_idx));\n                    for tri_idx in idx_range.clone() {\n                        let tri_indices = self.indices[tri_idx];\n\n                        // Get triangle if necessary\n                        let tri = if is_cached {\n                            let i = tri_idx - idx_range.start;\n                            unsafe { tri_cache[i].assume_init() }\n                        } else {\n                            let mut tri = if self.time_sample_count == 1 {\n                                // No deformation motion blur, so fast-path it.\n                                (\n                                    self.vertices[tri_indices.0 as usize],\n                                    self.vertices[tri_indices.1 as usize],\n                                    self.vertices[tri_indices.2 as usize],\n                                )\n                            } else {\n                                // Deformation motion blur, need to interpolate.\n                                let p0_slice = &self.vertices[(tri_indices.0 as usize\n                                    * self.time_sample_count)\n                                    ..((tri_indices.0 as usize + 1) * self.time_sample_count)];\n                                let p1_slice = &self.vertices[(tri_indices.1 as usize\n                                    * self.time_sample_count)\n                                    ..((tri_indices.1 as usize + 1) * self.time_sample_count)];\n                                let p2_slice = &self.vertices[(tri_indices.2 as usize\n                                    * self.time_sample_count)\n                                    ..((tri_indices.2 as usize + 1) * self.time_sample_count)];\n\n                                let p0 = lerp_slice(p0_slice, ray_time);\n                                let p1 = lerp_slice(p1_slice, ray_time);\n                                let p2 = lerp_slice(p2_slice, ray_time);\n\n                                (p0, p1, p2)\n                            };\n\n                            if !space.is_empty() {\n                                tri.0 = tri.0 * mat_space;\n                                tri.1 = tri.1 * mat_space;\n                                tri.2 = tri.2 * mat_space;\n                            }\n\n                            tri\n                        };\n\n                        // Test ray against triangle\n                        if let Some((t, b0, b1, b2)) = triangle::intersect_ray(\n                            rays.orig(ray_idx),\n                            ray_pre,\n                            rays.max_t(ray_idx),\n                            tri,\n                        ) {\n                            if rays.is_occlusion(ray_idx) {\n                                isects[ray_idx] = SurfaceIntersection::Occlude;\n                                rays.mark_done(ray_idx);\n                                break;\n                            } else {\n                                non_shadow_hit = true;\n                                rays.set_max_t(ray_idx, t);\n                                unsafe {\n                                    *hit_tri.as_mut_ptr() = tri;\n                                    *hit_tri_indices.as_mut_ptr() = tri_indices;\n                                    *hit_tri_data.as_mut_ptr() = (t, b0, b1, b2);\n                                }\n                            }\n                        }\n                    }\n\n                    // Calculate intersection data if necessary.\n                    if non_shadow_hit {\n                        let hit_tri = unsafe { hit_tri.assume_init() };\n                        let (t, b0, b1, b2) = unsafe { hit_tri_data.assume_init() };\n\n                        // Calculate intersection point and error magnitudes\n                        let (pos, pos_err) = triangle::surface_point(hit_tri, (b0, b1, b2));\n\n                        // Calculate geometric surface normal\n                        let geo_normal =\n                            cross(hit_tri.0 - hit_tri.1, hit_tri.0 - hit_tri.2).into_normal();\n\n                        // Calculate interpolated surface normal, if any\n                        let shading_normal = if let Some(normals) = self.normals {\n                            let hit_tri_indices = unsafe { hit_tri_indices.assume_init() };\n                            let n0_slice = &normals[(hit_tri_indices.0 as usize\n                                * self.time_sample_count)\n                                ..((hit_tri_indices.0 as usize + 1) * self.time_sample_count)];\n                            let n1_slice = &normals[(hit_tri_indices.1 as usize\n                                * self.time_sample_count)\n                                ..((hit_tri_indices.1 as usize + 1) * self.time_sample_count)];\n                            let n2_slice = &normals[(hit_tri_indices.2 as usize\n                                * self.time_sample_count)\n                                ..((hit_tri_indices.2 as usize + 1) * self.time_sample_count)];\n\n                            let n0 = lerp_slice(n0_slice, ray_time).normalized();\n                            let n1 = lerp_slice(n1_slice, ray_time).normalized();\n                            let n2 = lerp_slice(n2_slice, ray_time).normalized();\n\n                            let s_nor = ((n0 * b0) + (n1 * b1) + (n2 * b2)) * mat_space;\n                            if dot(s_nor, geo_normal) >= 0.0 {\n                                s_nor\n                            } else {\n                                -s_nor\n                            }\n                        } else {\n                            geo_normal\n                        };\n\n                        let intersection_data = SurfaceIntersectionData {\n                            incoming: rays.dir(ray_idx),\n                            t: t,\n                            pos: pos,\n                            pos_err: pos_err,\n                            nor: shading_normal,\n                            nor_g: geo_normal,\n                            local_space: mat_space,\n                            sample_pdf: 0.0,\n                        };\n\n                        // Fill in intersection data\n                        isects[ray_idx] = SurfaceIntersection::Hit {\n                            intersection_data: intersection_data,\n                            closure: shader.shade(&intersection_data, ray_time),\n                        };\n                    }\n                });\n                ray_stack.pop_task();\n            });\n    }\n}\n"
  },
  {
    "path": "src/timer.rs",
    "content": "#![allow(dead_code)]\n\nuse std::{thread, time::Duration};\n\n#[derive(Copy, Clone)]\npub struct Timer {\n    last_time: u64,\n}\n\nimpl Timer {\n    pub fn new() -> Timer {\n        Timer {\n            last_time: time::precise_time_ns(),\n        }\n    }\n\n    /// Marks a new tick time and returns the time elapsed in seconds since\n    /// the last call to tick().\n    pub fn tick(&mut self) -> f32 {\n        let n = time::precise_time_ns();\n        let dt = n - self.last_time;\n        self.last_time = n;\n\n        dt as f32 / 1_000_000_000.0\n    }\n\n    /// Returns the time elapsed in seconds since the last call to tick().\n    pub fn elapsed(self) -> f32 {\n        let dt = time::precise_time_ns() - self.last_time;\n        dt as f32 / 1_000_000_000.0\n    }\n\n    /// Sleeps the current thread until n seconds after the last tick.\n    pub fn sleep_until(self, n: f32) {\n        let dt = time::precise_time_ns() - self.last_time;\n        let target_dt = ((n as f64) * 1_000_000_000.0) as u64;\n        if dt < target_dt {\n            let delay = target_dt - dt;\n            let seconds = delay / 1_000_000_000;\n            let nanoseconds = delay % 1_000_000_000;\n            thread::sleep(Duration::new(seconds, nanoseconds as u32));\n        }\n    }\n}\n"
  },
  {
    "path": "src/tracer.rs",
    "content": "use std::iter;\n\nuse crate::{\n    accel::ray_code,\n    color::{rec709_to_xyz, Color},\n    lerp::lerp_slice,\n    math::Transform,\n    ray::{RayBatch, RayStack},\n    scene::{Assembly, InstanceType, Object},\n    shading::{SimpleSurfaceShader, SurfaceShader},\n    surface::SurfaceIntersection,\n    transform_stack::TransformStack,\n};\n\npub struct Tracer<'a> {\n    ray_trace_count: u64,\n    ray_stack: RayStack,\n    inner: TracerInner<'a>,\n}\n\nimpl<'a> Tracer<'a> {\n    pub fn from_assembly(assembly: &'a Assembly) -> Tracer<'a> {\n        Tracer {\n            ray_trace_count: 0,\n            ray_stack: RayStack::new(),\n            inner: TracerInner {\n                root: assembly,\n                xform_stack: TransformStack::new(),\n                isects: Vec::new(),\n            },\n        }\n    }\n\n    pub fn trace<'b>(&'b mut self, rays: &mut RayBatch) -> &'b [SurfaceIntersection] {\n        self.ray_trace_count += rays.len() as u64;\n        self.inner.trace(rays, &mut self.ray_stack)\n    }\n\n    pub fn rays_traced(&self) -> u64 {\n        self.ray_trace_count\n    }\n}\n\nstruct TracerInner<'a> {\n    root: &'a Assembly<'a>,\n    xform_stack: TransformStack,\n    isects: Vec<SurfaceIntersection>,\n}\n\nimpl<'a> TracerInner<'a> {\n    fn trace<'b>(\n        &'b mut self,\n        rays: &mut RayBatch,\n        ray_stack: &mut RayStack,\n    ) -> &'b [SurfaceIntersection] {\n        ray_stack.clear();\n\n        // Ready the isects\n        self.isects.clear();\n        self.isects.reserve(rays.len());\n        self.isects\n            .extend(iter::repeat(SurfaceIntersection::Miss).take(rays.len()));\n\n        // Prep the accel part of the rays.\n        {\n            let ident = Transform::new();\n            for i in 0..rays.len() {\n                rays.update_local(i, &ident);\n            }\n        }\n\n        // Divide the rays into 8 different lanes by direction.\n        ray_stack.ensure_lane_count(8);\n        for i in 0..rays.len() {\n            ray_stack.push_ray_index(i, ray_code(rays.dir(i)));\n        }\n        ray_stack.push_lanes_to_tasks(&[0, 1, 2, 3, 4, 5, 6, 7]);\n\n        // Trace each of the 8 lanes separately.\n        while !ray_stack.is_empty() {\n            self.trace_assembly(self.root, rays, ray_stack);\n        }\n\n        &self.isects\n    }\n\n    fn trace_assembly<'b>(\n        &'b mut self,\n        assembly: &Assembly,\n        rays: &mut RayBatch,\n        ray_stack: &mut RayStack,\n    ) {\n        assembly\n            .object_accel\n            .traverse(rays, ray_stack, |idx_range, rays, ray_stack| {\n                let inst = &assembly.instances[idx_range.start];\n\n                // Transform rays if needed\n                if let Some((xstart, xend)) = inst.transform_indices {\n                    // Push transforms to stack\n                    self.xform_stack.push(&assembly.xforms[xstart..xend]);\n\n                    // Do transforms\n                    // TODO: re-divide rays based on direction (maybe?).\n                    let xforms = self.xform_stack.top();\n                    ray_stack.do_next_task(|ray_idx| {\n                        let t = rays.time(ray_idx);\n                        rays.update_local(ray_idx, &lerp_slice(xforms, t));\n                    });\n                    ray_stack.duplicate_next_task();\n                }\n\n                // Trace rays\n                match inst.instance_type {\n                    InstanceType::Object => {\n                        self.trace_object(\n                            &assembly.objects[inst.data_index],\n                            inst.surface_shader_index\n                                .map(|i| assembly.surface_shaders[i]),\n                            rays,\n                            ray_stack,\n                        );\n                    }\n\n                    InstanceType::Assembly => {\n                        self.trace_assembly(&assembly.assemblies[inst.data_index], rays, ray_stack);\n                    }\n                }\n\n                // Un-transform rays if needed\n                if inst.transform_indices.is_some() {\n                    // Pop transforms off stack\n                    self.xform_stack.pop();\n\n                    // Undo transforms\n                    let xforms = self.xform_stack.top();\n                    if !xforms.is_empty() {\n                        ray_stack.pop_do_next_task(|ray_idx| {\n                            let t = rays.time(ray_idx);\n                            rays.update_local(ray_idx, &lerp_slice(xforms, t));\n                        });\n                    } else {\n                        let ident = Transform::new();\n                        ray_stack.pop_do_next_task(|ray_idx| {\n                            rays.update_local(ray_idx, &ident);\n                        });\n                    }\n                }\n            });\n    }\n\n    fn trace_object<'b>(\n        &'b mut self,\n        obj: &Object,\n        surface_shader: Option<&dyn SurfaceShader>,\n        rays: &mut RayBatch,\n        ray_stack: &mut RayStack,\n    ) {\n        match *obj {\n            Object::Surface(surface) => {\n                let unassigned_shader = SimpleSurfaceShader::Emit {\n                    color: Color::new_xyz(rec709_to_xyz((1.0, 0.0, 1.0))),\n                };\n                let shader = surface_shader.unwrap_or(&unassigned_shader);\n\n                surface.intersect_rays(\n                    rays,\n                    ray_stack,\n                    &mut self.isects,\n                    shader,\n                    self.xform_stack.top(),\n                );\n            }\n\n            Object::SurfaceLight(surface) => {\n                // Lights don't use shaders\n                let bogus_shader = SimpleSurfaceShader::Emit {\n                    color: Color::new_xyz(rec709_to_xyz((1.0, 0.0, 1.0))),\n                };\n\n                surface.intersect_rays(\n                    rays,\n                    ray_stack,\n                    &mut self.isects,\n                    &bogus_shader,\n                    self.xform_stack.top(),\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/transform_stack.rs",
    "content": "use std::{\n    cmp,\n    mem::{transmute, MaybeUninit},\n};\n\nuse crate::{algorithm::merge_slices_to, math::Transform};\n\npub struct TransformStack {\n    stack: Vec<MaybeUninit<Transform>>,\n    stack_indices: Vec<usize>,\n}\n\nimpl TransformStack {\n    pub fn new() -> TransformStack {\n        let mut ts = TransformStack {\n            stack: Vec::new(),\n            stack_indices: Vec::new(),\n        };\n\n        ts.stack_indices.push(0);\n        ts.stack_indices.push(0);\n\n        ts\n    }\n\n    pub fn clear(&mut self) {\n        self.stack.clear();\n        self.stack_indices.clear();\n        self.stack_indices.push(0);\n        self.stack_indices.push(0);\n    }\n\n    pub fn push(&mut self, xforms: &[Transform]) {\n        assert!(!xforms.is_empty());\n\n        if self.stack.is_empty() {\n            let xforms: &[MaybeUninit<Transform>] = unsafe { transmute(xforms) };\n            self.stack.extend(xforms);\n        } else {\n            let sil = self.stack_indices.len();\n            let i1 = self.stack_indices[sil - 2];\n            let i2 = self.stack_indices[sil - 1];\n            // Reserve stack space for the new transforms.\n            // Note this leaves exposed uninitialized memory.  The subsequent call to\n            // merge_slices_to() fills that memory in.\n            {\n                let maxlen = cmp::max(xforms.len(), i2 - i1);\n                self.stack.reserve(maxlen);\n                let l = self.stack.len();\n                unsafe { self.stack.set_len(l + maxlen) };\n            }\n            let (xfs1, xfs2) = self.stack.split_at_mut(i2);\n            merge_slices_to(\n                unsafe { transmute(&xfs1[i1..i2]) },\n                xforms,\n                xfs2,\n                |xf1, xf2| *xf1 * *xf2,\n            );\n        }\n\n        self.stack_indices.push(self.stack.len());\n    }\n\n    pub fn pop(&mut self) {\n        assert!(self.stack_indices.len() > 2);\n\n        let sl = self.stack.len();\n        let sil = self.stack_indices.len();\n        let i1 = self.stack_indices[sil - 2];\n        let i2 = self.stack_indices[sil - 1];\n\n        self.stack.truncate(sl - (i2 - i1));\n        self.stack_indices.pop();\n    }\n\n    pub fn top(&self) -> &[Transform] {\n        let sil = self.stack_indices.len();\n        let i1 = self.stack_indices[sil - 2];\n        let i2 = self.stack_indices[sil - 1];\n\n        unsafe { transmute(&self.stack[i1..i2]) }\n    }\n}\n"
  },
  {
    "path": "sub_crates/bvh_order/Cargo.toml",
    "content": "[package]\nname = \"bvh_order\"\nversion = \"0.1.0\"\nauthors = [\"Nathan Vegdahl <cessen@cessen.com>\"]\nedition = \"2018\"\nlicense = \"MIT, Apache 2.0\"\nbuild = \"build.rs\"\n\n[lib]\nname = \"bvh_order\"\npath = \"src/lib.rs\"\n"
  },
  {
    "path": "sub_crates/bvh_order/LICENSE.md",
    "content": "Copyright (c) 2020 Nathan Vegdahl\n\nThis project is licensed under either of\n\n* MIT license (licenses/MIT.txt or http://opensource.org/licenses/MIT)\n* Apache License, Version 2.0, (licenses/Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0)\n\nat your option.\n"
  },
  {
    "path": "sub_crates/bvh_order/build.rs",
    "content": "// Generate table for traversal order of quad BVHs.\n\nuse std::{env, fs::File, io::Write, path::Path};\n\nfn main() {\n    // Build the traversal table.\n    let mut traversal_table = [\n        Vec::new(),\n        Vec::new(),\n        Vec::new(),\n        Vec::new(),\n        Vec::new(),\n        Vec::new(),\n        Vec::new(),\n        Vec::new(),\n    ];\n    for raydir in 0..8 {\n        let ray = [raydir & 1, (raydir >> 1) & 1, (raydir >> 2) & 1];\n\n        for s2 in 0..3 {\n            for s1 in 0..3 {\n                for s0 in 0..3 {\n                    let mut perm = [0, 1, 2, 3];\n                    if ray[s1] == 1 {\n                        perm.swap(0, 1);\n                    }\n                    if ray[s2] == 1 {\n                        perm.swap(2, 3);\n                    }\n                    if ray[s0] == 1 {\n                        perm.swap(0, 2);\n                        perm.swap(1, 3);\n                    }\n                    traversal_table[raydir]\n                        .push(perm[0] + (perm[1] << 2) + (perm[2] << 4) + (perm[3] << 6));\n                }\n            }\n        }\n\n        for s1 in 0..3 {\n            for s0 in 0..3 {\n                let mut perm = [0, 1, 2];\n                if ray[s1] == 1 {\n                    perm.swap(0, 1);\n                }\n                if ray[s0] == 1 {\n                    perm.swap(0, 1);\n                    perm.swap(0, 2);\n                }\n                traversal_table[raydir].push(perm[0] + (perm[1] << 2) + (perm[2] << 4));\n            }\n        }\n\n        for s1 in 0..3 {\n            for s0 in 0..3 {\n                let mut perm = [0, 1, 2];\n                if ray[s1] == 1 {\n                    perm.swap(1, 2);\n                }\n                if ray[s0] == 1 {\n                    perm.swap(0, 2);\n                    perm.swap(0, 1);\n                }\n                traversal_table[raydir].push(perm[0] + (perm[1] << 2) + (perm[2] << 4));\n            }\n        }\n\n        for s0 in 0..3 {\n            let mut perm = [0, 1];\n            if ray[s0] == 1 {\n                perm.swap(0, 1);\n            }\n            traversal_table[raydir].push(perm[0] + (perm[1] << 2));\n        }\n    }\n\n    // Write traversal table to Rust file\n    let out_dir = env::var(\"OUT_DIR\").unwrap();\n    let dest_path = Path::new(&out_dir).join(\"table_inc.rs\");\n    let mut f = File::create(&dest_path).unwrap();\n\n    f.write_all(\"pub static TRAVERSAL_TABLE: [[u8; 48]; 8] = [\".as_bytes())\n        .unwrap();\n\n    for sub_table in traversal_table.iter() {\n        f.write_all(\"\\n    [\".as_bytes()).unwrap();\n        for (i, n) in sub_table.iter().enumerate() {\n            if i == 27 || i == 36 || i == 45 {\n                f.write_all(\"\\n     \".as_bytes()).unwrap();\n            }\n            f.write_all(format!(\"{}\", n).as_bytes()).unwrap();\n            if i != 47 {\n                f.write_all(\", \".as_bytes()).unwrap();\n            }\n        }\n        f.write_all(\"],\".as_bytes()).unwrap();\n    }\n\n    f.write_all(\"\\n];\".as_bytes()).unwrap();\n}\n"
  },
  {
    "path": "sub_crates/bvh_order/src/lib.rs",
    "content": "#![allow(dead_code)]\n\n// Include TRAVERSAL_TABLE generated by the build.rs script\ninclude!(concat!(env!(\"OUT_DIR\"), \"/table_inc.rs\"));\n\n/// Represents the split axes of the BVH2 node(s) that a BVH4 node was created\n/// from.\n///\n/// * `Full` is four nodes from three splits: top, left, and right.\n/// * `Left` is three nodes from two splits: top and left.\n/// * `Right` is three nodes from two splits: top and right.\n/// * `TopOnly` is two nodes from one split (in other words, the BVH4 node is\n///   identical to the single BVH2 node that it was created from).\n///\n/// The left node of a split is the node whose coordinate on the top split-axis\n/// is lower.  For example, if the top split is on the x axis, then `left.x <= right.x`.\n///\n/// The values representing each axis are x = 0, y = 1, and z = 2.\n#[derive(Debug, Copy, Clone)]\npub enum SplitAxes {\n    Full((u8, u8, u8)), // top, left, right\n    Left((u8, u8)),     // top, left\n    Right((u8, u8)),    // top, right\n    TopOnly(u8),        // top\n}\n\n/// Calculates the traversal code for a BVH4 node based on the splits and\n/// topology of the BVH2 node(s) it was created from.\n#[inline(always)]\npub fn calc_traversal_code(split: SplitAxes) -> u8 {\n    match split {\n        SplitAxes::Full((top, left, right)) => top + (left * 3) + (right * 9),\n        SplitAxes::Left((top, left)) => top + (left * 3) + 27,\n        SplitAxes::Right((top, right)) => top + (right * 3) + (27 + 9),\n        SplitAxes::TopOnly(top) => top + (27 + 9 + 9),\n    }\n}\n"
  },
  {
    "path": "sub_crates/color/Cargo.toml",
    "content": "[package]\nname = \"color\"\nversion = \"0.1.0\"\nauthors = [\"Nathan Vegdahl <cessen@cessen.com>\"]\nedition = \"2018\"\nlicense = \"MIT, Apache 2.0\"\nbuild = \"build.rs\"\n\n[lib]\nname = \"color\"\npath = \"src/lib.rs\"\n"
  },
  {
    "path": "sub_crates/color/LICENSE.md",
    "content": "Copyright (c) 2020 Nathan Vegdahl\n\nThis project is licensed under either of\n\n* MIT license (licenses/MIT.txt or http://opensource.org/licenses/MIT)\n* Apache License, Version 2.0, (licenses/Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0)\n\nat your option.\n"
  },
  {
    "path": "sub_crates/color/build.rs",
    "content": "use std::{env, fs::File, io::Write, path::Path};\n\n#[derive(Copy, Clone)]\nstruct Chromaticities {\n    r: (f64, f64),\n    g: (f64, f64),\n    b: (f64, f64),\n    w: (f64, f64),\n}\n\nfn main() {\n    let out_dir = env::var(\"OUT_DIR\").unwrap();\n\n    // Rec709\n    {\n        let chroma = Chromaticities {\n            r: (0.640, 0.330),\n            g: (0.300, 0.600),\n            b: (0.150, 0.060),\n            w: (0.3127, 0.3290),\n        };\n\n        let dest_path = Path::new(&out_dir).join(\"rec709_inc.rs\");\n        let mut f = File::create(&dest_path).unwrap();\n        write_conversion_functions(\"rec709\", chroma, &mut f);\n    }\n\n    // Rec2020\n    {\n        let chroma = Chromaticities {\n            r: (0.708, 0.292),\n            g: (0.170, 0.797),\n            b: (0.131, 0.046),\n            w: (0.3127, 0.3290),\n        };\n\n        let dest_path = Path::new(&out_dir).join(\"rec2020_inc.rs\");\n        let mut f = File::create(&dest_path).unwrap();\n        write_conversion_functions(\"rec2020\", chroma, &mut f);\n    }\n\n    // ACES AP0\n    {\n        let chroma = Chromaticities {\n            r: (0.73470, 0.26530),\n            g: (0.00000, 1.00000),\n            b: (0.00010, -0.07700),\n            w: (0.32168, 0.33767),\n        };\n\n        let dest_path = Path::new(&out_dir).join(\"aces_ap0_inc.rs\");\n        let mut f = File::create(&dest_path).unwrap();\n        write_conversion_functions(\"aces_ap0\", chroma, &mut f);\n    }\n\n    // ACES AP1\n    {\n        let chroma = Chromaticities {\n            r: (0.713, 0.293),\n            g: (0.165, 0.830),\n            b: (0.128, 0.044),\n            w: (0.32168, 0.33767),\n        };\n\n        let dest_path = Path::new(&out_dir).join(\"aces_ap1_inc.rs\");\n        let mut f = File::create(&dest_path).unwrap();\n        write_conversion_functions(\"aces_ap1\", chroma, &mut f);\n    }\n}\n\n/// Generates conversion functions for the given rgb to xyz transform matrix.\nfn write_conversion_functions(space_name: &str, chroma: Chromaticities, f: &mut File) {\n    let to_xyz = rgb_to_xyz(chroma, 1.0);\n\n    f.write_all(\n        format!(\n            r#\"\n#[inline]\npub fn {}_to_xyz(rgb: (f32, f32, f32)) -> (f32, f32, f32) {{\n    (\n        (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}),\n        (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}),\n        (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}),\n    )\n}}\n        \"#,\n            space_name,\n            to_xyz[0][0],\n            to_xyz[0][1],\n            to_xyz[0][2],\n            to_xyz[1][0],\n            to_xyz[1][1],\n            to_xyz[1][2],\n            to_xyz[2][0],\n            to_xyz[2][1],\n            to_xyz[2][2]\n        )\n        .as_bytes(),\n    )\n    .unwrap();\n\n    let inv = inverse(to_xyz);\n    f.write_all(\n        format!(\n            r#\"\n#[inline]\npub fn xyz_to_{}(xyz: (f32, f32, f32)) -> (f32, f32, f32) {{\n    (\n        (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}),\n        (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}),\n        (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}),\n    )\n}}\n        \"#,\n            space_name,\n            inv[0][0],\n            inv[0][1],\n            inv[0][2],\n            inv[1][0],\n            inv[1][1],\n            inv[1][2],\n            inv[2][0],\n            inv[2][1],\n            inv[2][2]\n        )\n        .as_bytes(),\n    )\n    .unwrap();\n\n    let e_chroma = {\n        let mut e_chroma = chroma;\n        e_chroma.w = (1.0 / 3.0, 1.0 / 3.0);\n        e_chroma\n    };\n    let e_to_xyz = rgb_to_xyz(e_chroma, 1.0);\n    f.write_all(\n        format!(\n            r#\"\n#[inline]\npub fn {}_e_to_xyz(rgb: (f32, f32, f32)) -> (f32, f32, f32) {{\n    (\n        (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}),\n        (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}),\n        (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}),\n    )\n}}\n        \"#,\n            space_name,\n            e_to_xyz[0][0],\n            e_to_xyz[0][1],\n            e_to_xyz[0][2],\n            e_to_xyz[1][0],\n            e_to_xyz[1][1],\n            e_to_xyz[1][2],\n            e_to_xyz[2][0],\n            e_to_xyz[2][1],\n            e_to_xyz[2][2]\n        )\n        .as_bytes(),\n    )\n    .unwrap();\n\n    let inv_e = inverse(e_to_xyz);\n    f.write_all(\n        format!(\n            r#\"\n#[inline]\npub fn xyz_to_{}_e(xyz: (f32, f32, f32)) -> (f32, f32, f32) {{\n    (\n        (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}),\n        (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}),\n        (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}),\n    )\n}}\n        \"#,\n            space_name,\n            inv_e[0][0],\n            inv_e[0][1],\n            inv_e[0][2],\n            inv_e[1][0],\n            inv_e[1][1],\n            inv_e[1][2],\n            inv_e[2][0],\n            inv_e[2][1],\n            inv_e[2][2]\n        )\n        .as_bytes(),\n    )\n    .unwrap();\n}\n\n/// Port of the RGBtoXYZ function from the ACES CTL reference implementation.\n/// See lib/IlmCtlMath/CtlColorSpace.cpp in the CTL reference implementation.\n///\n/// This takes the chromaticities of an RGB colorspace and generates a\n/// transform matrix from that space to XYZ.\n///\n/// * `chroma` is the chromaticities.\n/// * `y` is the XYZ \"Y\" value that should map to RGB (1,1,1)\nfn rgb_to_xyz(chroma: Chromaticities, y: f64) -> [[f64; 3]; 3] {\n    // X and Z values of RGB value (1, 1, 1), or \"white\"\n    let x = chroma.w.0 * y / chroma.w.1;\n    let z = (1.0 - chroma.w.0 - chroma.w.1) * y / chroma.w.1;\n\n    // Scale factors for matrix rows\n    let d = chroma.r.0 * (chroma.b.1 - chroma.g.1)\n        + chroma.b.0 * (chroma.g.1 - chroma.r.1)\n        + chroma.g.0 * (chroma.r.1 - chroma.b.1);\n\n    let sr = (x * (chroma.b.1 - chroma.g.1)\n        - chroma.g.0 * (y * (chroma.b.1 - 1.0) + chroma.b.1 * (x + z))\n        + chroma.b.0 * (y * (chroma.g.1 - 1.0) + chroma.g.1 * (x + z)))\n        / d;\n\n    let sg = (x * (chroma.r.1 - chroma.b.1)\n        + chroma.r.0 * (y * (chroma.b.1 - 1.0) + chroma.b.1 * (x + z))\n        - chroma.b.0 * (y * (chroma.r.1 - 1.0) + chroma.r.1 * (x + z)))\n        / d;\n\n    let sb = (x * (chroma.g.1 - chroma.r.1)\n        - chroma.r.0 * (y * (chroma.g.1 - 1.0) + chroma.g.1 * (x + z))\n        + chroma.g.0 * (y * (chroma.r.1 - 1.0) + chroma.r.1 * (x + z)))\n        / d;\n\n    // Assemble the matrix\n    let mut mat = [[0.0; 3]; 3];\n\n    mat[0][0] = sr * chroma.r.0;\n    mat[0][1] = sg * chroma.g.0;\n    mat[0][2] = sb * chroma.b.0;\n\n    mat[1][0] = sr * chroma.r.1;\n    mat[1][1] = sg * chroma.g.1;\n    mat[1][2] = sb * chroma.b.1;\n\n    mat[2][0] = sr * (1.0 - chroma.r.0 - chroma.r.1);\n    mat[2][1] = sg * (1.0 - chroma.g.0 - chroma.g.1);\n    mat[2][2] = sb * (1.0 - chroma.b.0 - chroma.b.1);\n\n    mat\n}\n\n/// Calculates the inverse of the given 3x3 matrix.\n///\n/// Ported to Rust from `gjInverse()` in IlmBase's Imath/ImathMatrix.h\nfn inverse(m: [[f64; 3]; 3]) -> [[f64; 3]; 3] {\n    let mut s = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];\n    let mut t = m;\n\n    // Forward elimination\n    for i in 0..2 {\n        let mut pivot = i;\n        let mut pivotsize = t[i][i];\n\n        if pivotsize < 0.0 {\n            pivotsize = -pivotsize;\n        }\n\n        for j in (i + 1)..3 {\n            let mut tmp = t[j][i];\n\n            if tmp < 0.0 {\n                tmp = -tmp;\n            }\n\n            if tmp > pivotsize {\n                pivot = j;\n                pivotsize = tmp;\n            }\n        }\n\n        if pivotsize == 0.0 {\n            panic!(\"Cannot invert singular matrix.\");\n        }\n\n        if pivot != i {\n            for j in 0..3 {\n                let mut tmp = t[i][j];\n                t[i][j] = t[pivot][j];\n                t[pivot][j] = tmp;\n\n                tmp = s[i][j];\n                s[i][j] = s[pivot][j];\n                s[pivot][j] = tmp;\n            }\n        }\n\n        for j in (i + 1)..3 {\n            let f = t[j][i] / t[i][i];\n\n            for k in 0..3 {\n                t[j][k] -= f * t[i][k];\n                s[j][k] -= f * s[i][k];\n            }\n        }\n    }\n\n    // Backward substitution\n    for i in (0..3).rev() {\n        let f = t[i][i];\n\n        if t[i][i] == 0.0 {\n            panic!(\"Cannot invert singular matrix.\");\n        }\n\n        for j in 0..3 {\n            t[i][j] /= f;\n            s[i][j] /= f;\n        }\n\n        for j in 0..i {\n            let f = t[j][i];\n\n            for k in 0..3 {\n                t[j][k] -= f * t[i][k];\n                s[j][k] -= f * s[i][k];\n            }\n        }\n    }\n\n    s\n}\n"
  },
  {
    "path": "sub_crates/color/src/lib.rs",
    "content": "#![allow(clippy::excessive_precision)]\n#![allow(clippy::unreadable_literal)]\n\n#[allow(non_camel_case_types)]\n#[derive(Copy, Clone)]\npub enum Space {\n    XYZ,\n    ACES_AP0,\n    ACES_AP1,\n    Rec709,\n    Rec2020,\n}\n\n// Generated conversion functions between XYZ and various RGB colorspaces\ninclude!(concat!(env!(\"OUT_DIR\"), \"/rec709_inc.rs\"));\ninclude!(concat!(env!(\"OUT_DIR\"), \"/rec2020_inc.rs\"));\ninclude!(concat!(env!(\"OUT_DIR\"), \"/aces_ap0_inc.rs\"));\ninclude!(concat!(env!(\"OUT_DIR\"), \"/aces_ap1_inc.rs\"));\n"
  },
  {
    "path": "sub_crates/compact/Cargo.toml",
    "content": "[package]\nname = \"compact\"\nversion = \"0.1.0\"\nauthors = [\"Nathan Vegdahl <cessen@cessen.com>\"]\nedition = \"2018\"\nlicense = \"MIT, Apache 2.0\"\n\n[lib]\nname = \"compact\"\npath = \"src/lib.rs\"\n\n[dev-dependencies]\nproptest = \"0.8\"\nbencher = \"0.1.5\"\nrand = \"0.6\"\n\n[[bench]]\nname = \"bench\"\nharness = false"
  },
  {
    "path": "sub_crates/compact/LICENSE.md",
    "content": "Copyright (c) 2020 Nathan Vegdahl\n\nThis project is licensed under either of\n\n* MIT license (licenses/MIT.txt or http://opensource.org/licenses/MIT)\n* Apache License, Version 2.0, (licenses/Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0)\n\nat your option.\n"
  },
  {
    "path": "sub_crates/compact/benches/bench.rs",
    "content": "use bencher::{benchmark_group, benchmark_main, black_box, Bencher};\nuse compact::{\n    fluv::fluv32,\n    shared_exp::{signed48, unsigned32, unsigned40},\n    unit_vec::oct32,\n};\nuse rand::{rngs::SmallRng, FromEntropy, Rng};\n\n//----\n\nfn unsigned32_encode_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let x = rng.gen::<f32>();\n        let y = rng.gen::<f32>();\n        let z = rng.gen::<f32>();\n        for _ in 0..1000 {\n            black_box(unsigned32::encode(black_box((x, y, z))));\n        }\n    });\n}\n\nfn unsigned32_decode_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let v = rng.gen::<u32>();\n        for _ in 0..1000 {\n            black_box(unsigned32::decode(black_box(v)));\n        }\n    });\n}\n\nfn unsigned40_encode_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let x = rng.gen::<f32>();\n        let y = rng.gen::<f32>();\n        let z = rng.gen::<f32>();\n        for _ in 0..1000 {\n            black_box(unsigned40::encode(black_box((x, y, z))));\n        }\n    });\n}\n\nfn unsigned40_decode_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let v = [\n            rng.gen::<u8>(),\n            rng.gen::<u8>(),\n            rng.gen::<u8>(),\n            rng.gen::<u8>(),\n            rng.gen::<u8>(),\n        ];\n        for _ in 0..1000 {\n            black_box(unsigned40::decode(black_box(v)));\n        }\n    });\n}\n\nfn signed48_encode_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let x = rng.gen::<f32>() - 0.5;\n        let y = rng.gen::<f32>() - 0.5;\n        let z = rng.gen::<f32>() - 0.5;\n        for _ in 0..1000 {\n            black_box(signed48::encode(black_box((x, y, z))));\n        }\n    });\n}\n\nfn signed48_decode_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let v = [\n            rng.gen::<u8>(),\n            rng.gen::<u8>(),\n            rng.gen::<u8>(),\n            rng.gen::<u8>(),\n            rng.gen::<u8>(),\n            rng.gen::<u8>(),\n        ];\n        for _ in 0..1000 {\n            black_box(signed48::decode(black_box(v)));\n        }\n    });\n}\n\nfn fluv32_encode_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let x = rng.gen::<f32>();\n        let y = rng.gen::<f32>();\n        let z = rng.gen::<f32>();\n        for _ in 0..1000 {\n            black_box(fluv32::encode(black_box((x, y, z))));\n        }\n    });\n}\n\nfn fluv32_decode_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let v = rng.gen::<u32>();\n        for _ in 0..1000 {\n            black_box(fluv32::decode(black_box(v)));\n        }\n    });\n}\n\nfn fluv32_decode_yuv_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let v = rng.gen::<u32>();\n        for _ in 0..1000 {\n            black_box(fluv32::decode_yuv(black_box(v)));\n        }\n    });\n}\n\nfn oct32_encode_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let x = rng.gen::<f32>() - 0.5;\n        let y = rng.gen::<f32>() - 0.5;\n        let z = rng.gen::<f32>() - 0.5;\n        for _ in 0..1000 {\n            black_box(oct32::encode(black_box((x, y, z))));\n        }\n    });\n}\n\nfn oct32_encode_precise_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let x = rng.gen::<f32>() - 0.5;\n        let y = rng.gen::<f32>() - 0.5;\n        let z = rng.gen::<f32>() - 0.5;\n        for _ in 0..1000 {\n            black_box(oct32::encode_precise(black_box((x, y, z))));\n        }\n    });\n}\n\nfn oct32_decode_1000_values(bench: &mut Bencher) {\n    let mut rng = SmallRng::from_entropy();\n    bench.iter(|| {\n        let v = rng.gen::<u32>();\n        for _ in 0..1000 {\n            black_box(oct32::decode(black_box(v)));\n        }\n    });\n}\n\n//----\n\nbenchmark_group!(\n    benches,\n    unsigned32_encode_1000_values,\n    unsigned32_decode_1000_values,\n    unsigned40_encode_1000_values,\n    unsigned40_decode_1000_values,\n    signed48_encode_1000_values,\n    signed48_decode_1000_values,\n    fluv32_encode_1000_values,\n    fluv32_decode_1000_values,\n    fluv32_decode_yuv_1000_values,\n    oct32_encode_1000_values,\n    oct32_encode_precise_1000_values,\n    oct32_decode_1000_values,\n);\nbenchmark_main!(benches);\n"
  },
  {
    "path": "sub_crates/compact/src/fluv/fluv32.rs",
    "content": "//! Encoding/decoding for the 32-bit FLuv32 color format.\n//!\n//! This encoding is based on, but is slightly different than, the 32-bit\n//! LogLuv format from the paper \"Overcoming Gamut and Dynamic Range\n//! Limitations in Digital Images\" by Greg Ward:\n//!\n//! * It uses the same uv chroma storage approach, but with *very* slightly\n//!   tweaked scales to allow perfect representation of E.\n//! * It uses a floating point rather than log encoding to store luminance,\n//!   mainly for the sake of faster decoding.\n//! * It omits the sign bit of LogLuv, foregoing negative luminance\n//!   capabilities.\n//!\n//! Aside from that, this format has the same chroma precision, very slightly\n//! improved luminance precision, and the same 127-stops of dynamic range as\n//! LogLuv.\n//!\n//! Like the LogLuv format, this is an absolute rather than relative color\n//! encoding, and as such takes CIE XYZ triplets as input.  It is *not*\n//! designed to take arbitrary floating point triplets, and will perform poorly\n//! if e.g. passed RGB values.\n//!\n//! The bit layout is (from most to least significant bit):\n//!\n//! * 7 bits: luminance exponent (bias 63)\n//! * 9 bits: luminance mantissa (implied leading 1, for 10 bits precision)\n//! * 8 bits: u'\n//! * 8 bits: v'\n//!\n//! ## Luminance details\n//!\n//! Like typical floating point, the luminance mantissa is treated as having an\n//! implicit leading 1, giving it an extra bit of precision.\n//!\n//! The luminance exponent is stored in 7 bits with a bias of 63.  The smallest\n//! exponent indicates a value of zero, and a valid encoding should also set\n//! the mantissa to zero in that case (denormal numbers are not supported).\n//! The largest exponent is given no special treatment (no infinities, no NaN).\n//!\n//! All together, this gives Fluv32 a worst-case precision that's slightly\n//! better than Logluv, and a luminance range of roughly `10^-19` to `10^19`,\n//! essentially the same as Logluv.\n//!\n//! Quoting Greg Ward about luminance ranges:\n//!\n//! > The sun is about `10^8 cd/m^2`, and the underside of a rock on a moonless\n//! > night is probably around `10^-6` or so [...]\n//!\n//! So Fluv32's luminance range is *massively* larger than needed for any\n//! day-to-day phenomena.  The only things that exceed it are stellar events\n//! such as supernovae, images of which are unliklely to be used with physical\n//! units in most practical graphics applications.\n\n#![allow(clippy::cast_lossless)]\n\nconst EXP_BIAS: i32 = 63;\nconst BIAS_OFFSET: u32 = 127 - EXP_BIAS as u32;\n\n/// The scale factor of the quantized U component.\npub const U_SCALE: f32 = 817.0 / 2.0;\n\n/// The scale factor of the quantized V component.\npub const V_SCALE: f32 = 1235.0 / 3.0;\n\n/// Largest representable Y component.\npub const Y_MAX: f32 = ((1u128 << (128 - EXP_BIAS)) - (1u128 << (128 - EXP_BIAS - 10))) as f32;\n\n/// Smallest representable non-zero Y component.\npub const Y_MIN: f32 = 1.0 / (1u128 << (EXP_BIAS - 1)) as f32;\n\n/// Difference between 1.0 and the next largest representable Y value.\npub const Y_EPSILON: f32 = 1.0 / 512.0;\n\n/// Encodes from CIE XYZ to 32-bit FloatLuv.\n#[inline]\npub fn encode(xyz: (f32, f32, f32)) -> u32 {\n    debug_assert!(\n        xyz.0 >= 0.0\n            && xyz.1 >= 0.0\n            && xyz.2 >= 0.0\n            && !xyz.0.is_nan()\n            && !xyz.1.is_nan()\n            && !xyz.2.is_nan(),\n        \"trifloat::fluv32::encode(): encoding to fluv32 only \\\n         works correctly for positive, non-NaN numbers, but the numbers passed \\\n         were: ({}, {}, {})\",\n        xyz.0,\n        xyz.1,\n        xyz.2\n    );\n\n    // Calculates the 16-bit encoding of the UV values for the given XYZ input.\n    #[inline(always)]\n    fn encode_uv(xyz: (f32, f32, f32)) -> u32 {\n        let s = xyz.0 + (15.0 * xyz.1) + (3.0 * xyz.2);\n\n        // The `+ 0.5` is for rounding, and is not part of the normal equation.\n        // The minimum value of 1.0 for v is to avoid a possible divide by zero\n        // when decoding.  A value less than 1.0 is outside the real colors,\n        // so we don't need to store it anyway.\n        let u = (((4.0 * U_SCALE) * xyz.0 / s) + 0.5).max(0.0).min(255.0);\n        let v = (((9.0 * V_SCALE) * xyz.1 / s) + 0.5).max(1.0).min(255.0);\n\n        ((u as u32) << 8) | (v as u32)\n    }\n\n    let y_bits = xyz.1.to_bits() & 0x7fffffff;\n\n    if y_bits < ((BIAS_OFFSET + 1) << 23) {\n        // Special case: black.\n        encode_uv((1.0, 1.0, 1.0))\n    } else if y_bits >= ((BIAS_OFFSET + 128) << 23) {\n        if xyz.1.is_infinite() {\n            // Special case: infinity.  In this case, we don't have any\n            // reasonable basis for calculating chroma, so just return\n            // the brightest white.\n            0xffff0000 | encode_uv((1.0, 1.0, 1.0))\n        } else {\n            // Special case: non-infinite, but brighter luma than can be\n            // represented.  Return the correct chroma, and the brightest luma.\n            0xffff0000 | encode_uv(xyz)\n        }\n    } else {\n        // Common case.\n        (((y_bits - (BIAS_OFFSET << 23)) << 2) & 0xffff0000) | encode_uv(xyz)\n    }\n}\n\n/// Decodes from 32-bit FloatLuv to CIE XYZ.\n#[inline]\npub fn decode(fluv32: u32) -> (f32, f32, f32) {\n    // Unpack values.\n    let (y, u, v) = decode_yuv(fluv32);\n    let u = u as f32;\n    let v = v as f32;\n\n    // Calculate x and z.\n    // This is re-worked from the original equations, to allow a bunch of stuff\n    // to cancel out and avoid operations.  It makes the underlying equations a\n    // bit non-obvious.\n    // We also roll the U/V_SCALE application into the final x and z formulas,\n    // since some of that cancels out as well, and all of it can be avoided at\n    // runtime that way.\n    const VU_RATIO: f32 = V_SCALE / U_SCALE;\n    let tmp = y / v;\n    let x = tmp * ((2.25 * VU_RATIO) * u); // y * (9u / 4v)\n    let z = tmp * ((3.0 * V_SCALE) - ((0.75 * VU_RATIO) * u) - (5.0 * v)); // y * ((12 - 3u - 20v) / 4v)\n\n    (x, y, z.max(0.0))\n}\n\n/// Decodes from 32-bit FloatLuv to Yuv.\n///\n/// The Y component is the luminance, and is simply the Y from CIE XYZ.\n///\n/// The u and v components are the CIE LUV u' and v' chromaticity coordinates,\n/// but returned as `u8`s, and scaled by `U_SCALE` and `V_SCALE` respectively\n/// to fit the range 0-255.\n#[inline]\npub fn decode_yuv(fluv32: u32) -> (f32, u8, u8) {\n    let y = f32::from_bits(((fluv32 & 0xffff0000) >> 2) + (BIAS_OFFSET << 23));\n    let u = (fluv32 >> 8) as u8;\n    let v = fluv32 as u8;\n\n    if fluv32 <= 0xffff {\n        (0.0, u, v)\n    } else {\n        (y, u, v)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn round_trip(floats: (f32, f32, f32)) -> (f32, f32, f32) {\n        decode(encode(floats))\n    }\n\n    #[test]\n    fn all_zeros() {\n        let fs = (0.0f32, 0.0f32, 0.0f32);\n\n        let tri = encode(fs);\n        let fs2 = decode(tri);\n\n        assert_eq!(0x000056c3, tri);\n        assert_eq!(fs, fs2);\n    }\n\n    #[test]\n    fn all_ones() {\n        let fs = (1.0f32, 1.0f32, 1.0f32);\n\n        let tri = encode(fs);\n        let fs2 = decode(tri);\n\n        assert!((fs.0 - fs2.0).abs() < 0.0000001);\n        assert_eq!(fs.1, fs2.1);\n        assert!((fs.2 - fs2.2).abs() < 0.0000001);\n        assert_eq!(0x7e0056c3, tri);\n    }\n\n    #[test]\n    fn powers_of_two() {\n        let mut n = 0.25;\n        for _ in 0..20 {\n            let a = (n as f32, n as f32, n as f32);\n            let b = round_trip(a);\n\n            let rd0 = 2.0 * (a.0 - b.0).abs() / (a.0 + b.0);\n            let rd2 = 2.0 * (a.2 - b.2).abs() / (a.2 + b.2);\n\n            assert_eq!(a.1, b.1);\n            assert!(rd0 < 0.01);\n            assert!(rd2 < 0.01);\n\n            n *= 2.0;\n        }\n    }\n\n    #[test]\n    fn accuracy_01() {\n        let mut n = 1.0;\n        for _ in 0..512 {\n            let a = (n as f32, n as f32, n as f32);\n            let b = round_trip(a);\n\n            let rd0 = 2.0 * (a.0 - b.0).abs() / (a.0 + b.0);\n            let rd2 = 2.0 * (a.2 - b.2).abs() / (a.2 + b.2);\n\n            assert_eq!(a.1, b.1);\n            assert!(rd0 < 0.01);\n            assert!(rd2 < 0.01);\n\n            n += 1.0 / 512.0;\n        }\n    }\n\n    #[test]\n    #[should_panic]\n    fn accuracy_02() {\n        let mut n = 1.0;\n        for _ in 0..1024 {\n            let a = (n as f32, n as f32, n as f32);\n            let b = round_trip(a);\n            assert_eq!(a.1, b.1);\n            n += 1.0 / 1024.0;\n        }\n    }\n\n    #[test]\n    fn integers() {\n        for n in 1..=512 {\n            let a = (n as f32, n as f32, n as f32);\n            let b = round_trip(a);\n\n            let rd0 = 2.0 * (a.0 - b.0).abs() / (a.0 + b.0);\n            let rd2 = 2.0 * (a.2 - b.2).abs() / (a.2 + b.2);\n\n            assert_eq!(a.1, b.1);\n            assert!(rd0 < 0.01);\n            assert!(rd2 < 0.01);\n        }\n    }\n\n    #[test]\n    fn precision_floor() {\n        let fs = (2049.0f32, 2049.0f32, 2049.0f32);\n        assert_eq!(2048.0, round_trip(fs).1);\n    }\n\n    #[test]\n    fn decode_yuv_01() {\n        let fs = (1.0, 1.0, 1.0);\n        let a = encode(fs);\n\n        assert_eq!((1.0, 0x56, 0xc3), decode_yuv(a));\n    }\n\n    #[test]\n    fn saturate_y() {\n        let fs = (1.0e+28, 1.0e+28, 1.0e+28);\n\n        assert_eq!(Y_MAX, round_trip(fs).1);\n        assert_eq!(Y_MAX, decode(0xFFFFFFFF).1);\n    }\n\n    #[test]\n    fn inf_saturate() {\n        use std::f32::INFINITY;\n        let fs = (INFINITY, INFINITY, INFINITY);\n\n        assert_eq!(Y_MAX, round_trip(fs).1);\n        assert_eq!(0xffff56c3, encode(fs));\n    }\n\n    #[test]\n    fn smallest_value_and_underflow() {\n        let fs1 = (Y_MIN, Y_MIN, Y_MIN);\n        let fs2 = (Y_MIN * 0.99, Y_MIN * 0.99, Y_MIN * 0.99);\n\n        dbg!(Y_MIN);\n        assert_eq!(fs1.1, round_trip(fs1).1);\n        assert_eq!(0.0, round_trip(fs2).1);\n        assert_eq!(0x000056c3, encode(fs2));\n    }\n\n    #[test]\n    fn negative_z_impossible() {\n        for y in 0..1024 {\n            let fs = (1.0, 1.0 + (y as f32 / 4096.0), 0.0);\n            let fs2 = round_trip(fs);\n            assert!(fs2.2 >= 0.0);\n        }\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_01() {\n        encode((std::f32::NAN, 0.0, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_02() {\n        encode((0.0, std::f32::NAN, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_03() {\n        encode((0.0, 0.0, std::f32::NAN));\n    }\n\n    #[test]\n    #[should_panic]\n    fn negative_01() {\n        encode((-1.0, 0.0, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn negative_02() {\n        encode((0.0, -1.0, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn negative_03() {\n        encode((0.0, 0.0, -1.0));\n    }\n\n    #[test]\n    fn negative_04() {\n        encode((-0.0, -0.0, -0.0));\n    }\n}\n"
  },
  {
    "path": "sub_crates/compact/src/fluv/mod.rs",
    "content": "//! Fluv, a set of formats for compactly storing HDR XYZ colors.\n//!\n//! At the moment, only a 32-bit variant of the fluv format is provided.\n\npub mod fluv32;\n"
  },
  {
    "path": "sub_crates/compact/src/lib.rs",
    "content": "//! Functions for storing various kinds of data compactly, using domain\n//! knowledge of how that data is used.\n//!\n//! This includes functions for compactly storing e.g. colors and unit vectors.\n\npub mod fluv;\npub mod shared_exp;\npub mod unit_vec;\n"
  },
  {
    "path": "sub_crates/compact/src/shared_exp/mod.rs",
    "content": "//! Shared-exponent float triplet formats.\n\npub mod signed48;\npub mod unsigned32;\npub mod unsigned40;\n\n//===========================================================================\n// Shared functions used by the other modules in this crate.\n\n/// Calculates 2.0^exp using IEEE bit fiddling.\n///\n/// Only works for integer exponents in the range [-126, 127]\n/// due to IEEE 32-bit float limits.\n#[inline(always)]\nfn fiddle_exp2(exp: i32) -> f32 {\n    use std::f32;\n    f32::from_bits(((exp + 127) as u32) << 23)\n}\n\n/// Calculates a floor(log2(n)) using IEEE bit fiddling.\n///\n/// Because of IEEE floating point format, infinity and NaN\n/// floating point values return 128, and subnormal numbers always\n/// return -127.  These particular behaviors are not, of course,\n/// mathemetically correct, but are actually desireable for the\n/// calculations in this library.\n#[inline(always)]\nfn fiddle_log2(n: f32) -> i32 {\n    use std::f32;\n    ((f32::to_bits(n) >> 23) & 0b1111_1111) as i32 - 127\n}\n"
  },
  {
    "path": "sub_crates/compact/src/shared_exp/signed48.rs",
    "content": "//! Encoding/decoding for signed 48-bit trifloat numbers.\n//!\n//! The encoding uses 13 bits of mantissa and 1 sign bit per number, and 6\n//! bits for the shared exponent. The bit layout is: [sign 1, mantissa 1,\n//! sign 2, mantissa 2, sign 3, mantissa 3, exponent].  The exponent is stored\n//! as an unsigned integer with a bias of 26.\n//!\n//! The largest representable number is just under `2^38`, and the smallest\n//! representable positive number is `2^-38`.\n//!\n//! Since the exponent is shared between all three values, the precision\n//! of all three values depends on the largest (in magnitude) of the three.\n//! All integers in the range `[-8192, 8192]` can be represented exactly in the\n//! largest value.\n\n#![allow(clippy::cast_lossless)]\n\nuse super::{fiddle_exp2, fiddle_log2};\n\n/// Largest representable number.\npub const MAX: f32 = ((1u128 << (64 - EXP_BIAS)) - (1 << (64 - EXP_BIAS - 13))) as f32;\n\n/// Smallest representable number.\n///\n/// Note this is not the smallest _magnitude_ number.  This is a negative\n/// number of large magnitude.\npub const MIN: f32 = -MAX;\n\n/// Smallest representable positive number.\n///\n/// This is the number with the smallest possible magnitude (aside from zero).\npub const MIN_POSITIVE: f32 = 1.0 / (1u128 << (EXP_BIAS + 12)) as f32;\n\n/// Difference between 1.0 and the next largest representable number.\npub const EPSILON: f32 = 1.0 / 4096.0;\n\nconst EXP_BIAS: i32 = 26;\n\n/// Encodes three floating point values into a signed 48-bit trifloat.\n///\n/// Input floats that are larger than `MAX` or smaller than `MIN` will saturate\n/// to `MAX` and `MIN` respectively, including +/- infinity.  Values are\n/// converted to trifloat precision by rounding towards zero.\n///\n/// Warning: NaN's are _not_ supported by the trifloat format.  There are\n/// debug-only assertions in place to catch such values in the input floats.\n#[inline]\npub fn encode(floats: (f32, f32, f32)) -> [u8; 6] {\n    u64_to_bytes(encode_64(floats))\n}\n\n/// Decodes a signed 48-bit trifloat into three full floating point numbers.\n///\n/// This operation is lossless and cannot fail.\n#[inline]\npub fn decode(trifloat: [u8; 6]) -> (f32, f32, f32) {\n    decode_64(bytes_to_u64(trifloat))\n}\n\n// Workhorse encode function, which operates on u64.\n#[inline(always)]\nfn encode_64(floats: (f32, f32, f32)) -> u64 {\n    debug_assert!(\n        !floats.0.is_nan() && !floats.1.is_nan() && !floats.2.is_nan(),\n        \"trifloat::signed48::encode(): encoding to signed tri-floats only \\\n         works correctly for non-NaN numbers, but the numbers passed were: \\\n         ({}, {}, {})\",\n        floats.0,\n        floats.1,\n        floats.2\n    );\n\n    let floats_abs = (floats.0.abs(), floats.1.abs(), floats.2.abs());\n\n    let largest_abs = floats_abs.0.max(floats_abs.1.max(floats_abs.2));\n\n    if largest_abs < MIN_POSITIVE {\n        0\n    } else {\n        let e = fiddle_log2(largest_abs).max(-EXP_BIAS).min(63 - EXP_BIAS);\n        let inv_multiplier = fiddle_exp2(-e + 12);\n\n        let x_sign = (floats.0.to_bits() >> 31) as u64;\n        let x = (floats_abs.0 * inv_multiplier).min(8191.0) as u64;\n        let y_sign = (floats.1.to_bits() >> 31) as u64;\n        let y = (floats_abs.1 * inv_multiplier).min(8191.0) as u64;\n        let z_sign = (floats.2.to_bits() >> 31) as u64;\n        let z = (floats_abs.2 * inv_multiplier).min(8191.0) as u64;\n\n        (x_sign << 47)\n            | (x << 34)\n            | (y_sign << 33)\n            | (y << 20)\n            | (z_sign << 19)\n            | (z << 6)\n            | (e + EXP_BIAS) as u64\n    }\n}\n\n// Workhorse decode function, which operates on u64.\n#[inline(always)]\nfn decode_64(trifloat: u64) -> (f32, f32, f32) {\n    // Unpack values.\n    let x = (trifloat >> 34) & 0b111_11111_11111;\n    let y = (trifloat >> 20) & 0b111_11111_11111;\n    let z = (trifloat >> 6) & 0b111_11111_11111;\n\n    let x_sign = ((trifloat >> 16) & 0x8000_0000) as u32;\n    let y_sign = ((trifloat >> 2) & 0x8000_0000) as u32;\n    let z_sign = ((trifloat << 12) & 0x8000_0000) as u32;\n\n    let e = trifloat & 0b111_111;\n\n    let multiplier = fiddle_exp2(e as i32 - EXP_BIAS - 12);\n\n    (\n        f32::from_bits((x as f32 * multiplier).to_bits() | x_sign),\n        f32::from_bits((y as f32 * multiplier).to_bits() | y_sign),\n        f32::from_bits((z as f32 * multiplier).to_bits() | z_sign),\n    )\n}\n\n#[inline(always)]\nfn u64_to_bytes(n: u64) -> [u8; 6] {\n    let a = n.to_ne_bytes();\n    let mut b = [0u8; 6];\n    if cfg!(target_endian = \"big\") {\n        (&mut b[..]).copy_from_slice(&a[2..8]);\n    } else {\n        (&mut b[..]).copy_from_slice(&a[0..6]);\n    }\n    b\n}\n\n#[inline(always)]\nfn bytes_to_u64(a: [u8; 6]) -> u64 {\n    let mut b = [0u8; 8];\n    if cfg!(target_endian = \"big\") {\n        (&mut b[2..8]).copy_from_slice(&a[..]);\n    } else {\n        (&mut b[0..6]).copy_from_slice(&a[..]);\n    }\n    u64::from_ne_bytes(b)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn round_trip(floats: (f32, f32, f32)) -> (f32, f32, f32) {\n        decode(encode(floats))\n    }\n\n    #[test]\n    fn all_zeros() {\n        let fs = (0.0f32, 0.0f32, 0.0f32);\n\n        let tri = encode_64(fs);\n        let fs2 = decode_64(tri);\n\n        assert_eq!(tri, 0);\n        assert_eq!(fs, fs2);\n    }\n\n    #[test]\n    fn powers_of_two() {\n        let fs = (8.0f32, 128.0f32, 0.5f32);\n        assert_eq!(fs, round_trip(fs));\n    }\n\n    #[test]\n    fn signs() {\n        let fs1 = (1.0f32, 1.0f32, 1.0f32);\n        let fs2 = (1.0f32, 1.0f32, -1.0f32);\n        let fs3 = (1.0f32, -1.0f32, 1.0f32);\n        let fs4 = (1.0f32, -1.0f32, -1.0f32);\n        let fs5 = (-1.0f32, 1.0f32, 1.0f32);\n        let fs6 = (-1.0f32, 1.0f32, -1.0f32);\n        let fs7 = (-1.0f32, -1.0f32, 1.0f32);\n        let fs8 = (-1.0f32, -1.0f32, -1.0f32);\n\n        assert_eq!(fs1, round_trip(fs1));\n        assert_eq!(fs2, round_trip(fs2));\n        assert_eq!(fs3, round_trip(fs3));\n        assert_eq!(fs4, round_trip(fs4));\n        assert_eq!(fs5, round_trip(fs5));\n        assert_eq!(fs6, round_trip(fs6));\n        assert_eq!(fs7, round_trip(fs7));\n        assert_eq!(fs8, round_trip(fs8));\n    }\n\n    #[test]\n    fn accuracy() {\n        let mut n = 1.0;\n        for _ in 0..256 {\n            let (x, _, _) = round_trip((n, 0.0, 0.0));\n            assert_eq!(n, x);\n            n += 1.0 / 256.0;\n        }\n    }\n\n    #[test]\n    fn integers() {\n        for n in -8192i32..=8192i32 {\n            let (x, _, _) = round_trip((n as f32, 0.0, 0.0));\n            assert_eq!(n as f32, x);\n        }\n    }\n\n    #[test]\n    fn precision_floor() {\n        let fs = (7.0f32, 8193.0f32, -1.0f32);\n        let fsn = (-7.0f32, -8193.0f32, 1.0f32);\n        assert_eq!((6.0, 8192.0, -0.0), round_trip(fs));\n        assert_eq!((-6.0, -8192.0, 0.0), round_trip(fsn));\n    }\n\n    #[test]\n    fn saturate() {\n        let fs = (\n            99_999_999_999_999.0,\n            99_999_999_999_999.0,\n            99_999_999_999_999.0,\n        );\n        let fsn = (\n            -99_999_999_999_999.0,\n            -99_999_999_999_999.0,\n            -99_999_999_999_999.0,\n        );\n\n        assert_eq!((MAX, MAX, MAX), round_trip(fs));\n        assert_eq!((MIN, MIN, MIN), round_trip(fsn));\n        assert_eq!((MAX, MAX, MAX), decode_64(0x7FFD_FFF7_FFFF));\n        assert_eq!((MIN, MIN, MIN), decode_64(0xFFFF_FFFF_FFFF));\n    }\n\n    #[test]\n    fn inf_saturate() {\n        use std::f32::INFINITY;\n        let fs = (INFINITY, 0.0, 0.0);\n        let fsn = (-INFINITY, 0.0, 0.0);\n\n        assert_eq!((MAX, 0.0, 0.0), round_trip(fs));\n        assert_eq!((MIN, 0.0, 0.0), round_trip(fsn));\n        assert_eq!(0x7FFC0000003F, encode_64(fs));\n        assert_eq!(0xFFFC0000003F, encode_64(fsn));\n    }\n\n    #[test]\n    fn partial_saturate() {\n        let fs = (99_999_999_999_999.0, 4294967296.0, -17179869184.0);\n        let fsn = (-99_999_999_999_999.0, 4294967296.0, -17179869184.0);\n\n        assert_eq!((MAX, 4294967296.0, -17179869184.0), round_trip(fs));\n        assert_eq!((MIN, 4294967296.0, -17179869184.0), round_trip(fsn));\n    }\n\n    #[test]\n    fn smallest_value() {\n        let fs = (MIN_POSITIVE * 1.5, MIN_POSITIVE, MIN_POSITIVE * 0.50);\n        let fsn = (-MIN_POSITIVE * 1.5, -MIN_POSITIVE, -MIN_POSITIVE * 0.50);\n\n        assert_eq!((MIN_POSITIVE, -MIN_POSITIVE, 0.0), decode_64(0x600100000));\n        assert_eq!((MIN_POSITIVE, MIN_POSITIVE, 0.0), round_trip(fs));\n        assert_eq!((-MIN_POSITIVE, -MIN_POSITIVE, -0.0), round_trip(fsn));\n    }\n\n    #[test]\n    fn underflow() {\n        let fs = (MIN_POSITIVE * 0.5, -MIN_POSITIVE * 0.5, MIN_POSITIVE);\n        assert_eq!(0x200000040, encode_64(fs));\n        assert_eq!((0.0, -0.0, MIN_POSITIVE), round_trip(fs));\n    }\n\n    #[test]\n    fn garbage_upper_bits_decode() {\n        let fs1 = (4.0, -623.53, 12.3);\n        let fs2 = (-63456254.2, 5235423.53, 54353.3);\n        let fs3 = (-0.000000634, 0.00000000005, 0.00000000892);\n\n        let n1 = encode_64(fs1);\n        let n2 = encode_64(fs2);\n        let n3 = encode_64(fs3);\n\n        assert_eq!(decode_64(n1), decode_64(n1 | 0xffff_0000_0000_0000));\n        assert_eq!(decode_64(n2), decode_64(n2 | 0xffff_0000_0000_0000));\n        assert_eq!(decode_64(n3), decode_64(n3 | 0xffff_0000_0000_0000));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_01() {\n        encode((std::f32::NAN, 1.0, -1.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_02() {\n        encode((1.0, std::f32::NAN, -1.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_03() {\n        encode((1.0, -1.0, std::f32::NAN));\n    }\n}\n"
  },
  {
    "path": "sub_crates/compact/src/shared_exp/unsigned32.rs",
    "content": "//! Encoding/decoding for unsigned 32-bit trifloat numbers.\n//!\n//! The encoding uses 9 bits of mantissa per number, and 5 bits for the shared\n//! exponent.  The bit layout is [mantissa 1, mantissa 2, mantissa 3, exponent].\n//! The exponent is stored as an unsigned integer with a bias of 11.\n//!\n//! The largest representable number is `2^21 - 4096`, and the smallest\n//! representable non-zero number is `2^-19`.\n//!\n//! Since the exponent is shared between the three values, the precision\n//! of all three values depends on the largest of the three.  All integers\n//! up to 512 can be represented exactly in the largest value.\n\nuse super::{fiddle_exp2, fiddle_log2};\n\n/// Largest representable number.\npub const MAX: f32 = ((1u64 << (32 - EXP_BIAS)) - (1 << (32 - EXP_BIAS - 9))) as f32;\n\n/// Smallest representable non-zero number.\npub const MIN: f32 = 1.0 / (1 << (EXP_BIAS + 8)) as f32;\n\n/// Difference between 1.0 and the next largest representable number.\npub const EPSILON: f32 = 1.0 / 256.0;\n\nconst EXP_BIAS: i32 = 11;\n\n/// Encodes three floating point values into an unsigned 32-bit trifloat.\n///\n/// Input floats larger than `MAX` will saturate to `MAX`, including infinity.\n/// Values are converted to trifloat precision by rounding down.\n///\n/// Warning: negative values and NaN's are _not_ supported by the trifloat\n/// format.  There are debug-only assertions in place to catch such\n/// values in the input floats.\n#[inline]\npub fn encode(floats: (f32, f32, f32)) -> u32 {\n    debug_assert!(\n        floats.0 >= 0.0\n            && floats.1 >= 0.0\n            && floats.2 >= 0.0\n            && !floats.0.is_nan()\n            && !floats.1.is_nan()\n            && !floats.2.is_nan(),\n        \"trifloat::unsigned32::encode(): encoding to unsigned tri-floats only \\\n         works correctly for positive, non-NaN numbers, but the numbers passed \\\n         were: ({}, {}, {})\",\n        floats.0,\n        floats.1,\n        floats.2\n    );\n\n    let largest = floats.0.max(floats.1.max(floats.2));\n\n    if largest < MIN {\n        return 0;\n    } else {\n        let e = fiddle_log2(largest).max(-EXP_BIAS).min(31 - EXP_BIAS);\n        let inv_multiplier = fiddle_exp2(-e + 8);\n        let x = (floats.0 * inv_multiplier).min(511.0) as u32;\n        let y = (floats.1 * inv_multiplier).min(511.0) as u32;\n        let z = (floats.2 * inv_multiplier).min(511.0) as u32;\n\n        (x << (9 + 9 + 5)) | (y << (9 + 5)) | (z << 5) | (e + EXP_BIAS) as u32\n    }\n}\n\n/// Decodes an unsigned 32-bit trifloat into three full floating point numbers.\n///\n/// This operation is lossless and cannot fail.\n#[inline]\npub fn decode(trifloat: u32) -> (f32, f32, f32) {\n    // Unpack values.\n    let x = trifloat >> (9 + 9 + 5);\n    let y = (trifloat >> (9 + 5)) & 0b1_1111_1111;\n    let z = (trifloat >> 5) & 0b1_1111_1111;\n    let e = trifloat & 0b1_1111;\n\n    let multiplier = fiddle_exp2(e as i32 - EXP_BIAS - 8);\n\n    (\n        x as f32 * multiplier,\n        y as f32 * multiplier,\n        z as f32 * multiplier,\n    )\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn round_trip(floats: (f32, f32, f32)) -> (f32, f32, f32) {\n        decode(encode(floats))\n    }\n\n    #[test]\n    fn all_zeros() {\n        let fs = (0.0f32, 0.0f32, 0.0f32);\n\n        let tri = encode(fs);\n        let fs2 = decode(tri);\n\n        assert_eq!(tri, 0u32);\n        assert_eq!(fs, fs2);\n    }\n\n    #[test]\n    fn powers_of_two() {\n        let fs = (8.0f32, 128.0f32, 0.5f32);\n        assert_eq!(fs, round_trip(fs));\n    }\n\n    #[test]\n    fn accuracy_01() {\n        let mut n = 1.0;\n        for _ in 0..256 {\n            let (x, _, _) = round_trip((n, 0.0, 0.0));\n            assert_eq!(n, x);\n            n += 1.0 / 256.0;\n        }\n    }\n\n    #[test]\n    #[should_panic]\n    fn accuracy_02() {\n        let mut n = 1.0;\n        for _ in 0..512 {\n            let (x, _, _) = round_trip((n, 0.0, 0.0));\n            assert_eq!(n, x);\n            n += 1.0 / 512.0;\n        }\n    }\n\n    #[test]\n    fn integers() {\n        for n in 0..=512 {\n            let (x, _, _) = round_trip((n as f32, 0.0, 0.0));\n            assert_eq!(n as f32, x);\n        }\n    }\n\n    #[test]\n    fn precision_floor() {\n        let fs = (7.0f32, 513.0f32, 1.0f32);\n        assert_eq!((6.0, 512.0, 0.0), round_trip(fs));\n    }\n\n    #[test]\n    fn saturate() {\n        let fs = (9999999999.0, 9999999999.0, 9999999999.0);\n\n        assert_eq!((MAX, MAX, MAX), round_trip(fs));\n        assert_eq!((MAX, MAX, MAX), decode(0xFFFFFFFF));\n    }\n\n    #[test]\n    fn inf_saturate() {\n        use std::f32::INFINITY;\n        let fs = (INFINITY, 0.0, 0.0);\n\n        assert_eq!((MAX, 0.0, 0.0), round_trip(fs));\n        assert_eq!(0xFF80001F, encode(fs));\n    }\n\n    #[test]\n    fn partial_saturate() {\n        let fs = (9999999999.0, 4096.0, 262144.0);\n\n        assert_eq!((MAX, 4096.0, 262144.0), round_trip(fs));\n    }\n\n    #[test]\n    fn smallest_value() {\n        let fs = (MIN * 1.5, MIN, MIN * 0.5);\n        assert_eq!((MIN, MIN, 0.0), round_trip(fs));\n        assert_eq!((MIN, MIN, 0.0), decode(0x00_80_40_00));\n    }\n\n    #[test]\n    fn underflow() {\n        let fs = (MIN * 0.99, 0.0, 0.0);\n        assert_eq!(0, encode(fs));\n        assert_eq!((0.0, 0.0, 0.0), round_trip(fs));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_01() {\n        encode((std::f32::NAN, 0.0, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_02() {\n        encode((0.0, std::f32::NAN, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_03() {\n        encode((0.0, 0.0, std::f32::NAN));\n    }\n\n    #[test]\n    #[should_panic]\n    fn negative_01() {\n        encode((-1.0, 0.0, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn negative_02() {\n        encode((0.0, -1.0, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn negative_03() {\n        encode((0.0, 0.0, -1.0));\n    }\n\n    #[test]\n    fn negative_04() {\n        encode((-0.0, -0.0, -0.0));\n    }\n}\n"
  },
  {
    "path": "sub_crates/compact/src/shared_exp/unsigned40.rs",
    "content": "//! Encoding/decoding for unsigned 40-bit trifloat numbers.\n//!\n//! The encoding uses 11 bits of mantissa per number, and 7 bits for the shared\n//! exponent.  The bit layout is [mantissa 1, mantissa 2, mantissa 3, exponent].\n//! The exponent is stored as an unsigned integer with a bias of 32.\n//!\n//! The largest representable number is just under `2^96`, and the smallest\n//! representable non-zero number is `2^-42`.\n//!\n//! Since the exponent is shared between the three values, the precision\n//! of all three values depends on the largest of the three.  All integers\n//! up to 2048 can be represented exactly in the largest value.\n\nuse super::{fiddle_exp2, fiddle_log2};\n\n/// Largest representable number.\npub const MAX: f32 = ((1u128 << (128 - EXP_BIAS)) - (1 << (128 - EXP_BIAS - 11))) as f32;\n\n/// Smallest representable non-zero number.\npub const MIN: f32 = 1.0 / (1u128 << (EXP_BIAS + 10)) as f32;\n\n/// Difference between 1.0 and the next largest representable number.\npub const EPSILON: f32 = 1.0 / 1024.0;\n\nconst EXP_BIAS: i32 = 32;\n\n/// Encodes three floating point values into an unsigned 40-bit trifloat.\n///\n/// Input floats larger than `MAX` will saturate to `MAX`, including infinity.\n/// Values are converted to trifloat precision by rounding down.\n///\n/// Warning: negative values and NaN's are _not_ supported by the trifloat\n/// format.  There are debug-only assertions in place to catch such\n/// values in the input floats.\n#[inline]\npub fn encode(floats: (f32, f32, f32)) -> [u8; 5] {\n    u64_to_bytes(encode_64(floats))\n}\n\n/// Decodes an unsigned 40-bit trifloat into three full floating point numbers.\n///\n/// This operation is lossless and cannot fail.\n#[inline]\npub fn decode(trifloat: [u8; 5]) -> (f32, f32, f32) {\n    decode_64(bytes_to_u64(trifloat))\n}\n\n// Workhorse encode function, which operates on u64.\n#[inline(always)]\nfn encode_64(floats: (f32, f32, f32)) -> u64 {\n    debug_assert!(\n        floats.0 >= 0.0\n            && floats.1 >= 0.0\n            && floats.2 >= 0.0\n            && !floats.0.is_nan()\n            && !floats.1.is_nan()\n            && !floats.2.is_nan(),\n        \"trifloat::unsigned32::encode(): encoding to unsigned tri-floats only \\\n         works correctly for positive, non-NaN numbers, but the numbers passed \\\n         were: ({}, {}, {})\",\n        floats.0,\n        floats.1,\n        floats.2\n    );\n\n    let largest = floats.0.max(floats.1.max(floats.2));\n\n    if largest < MIN {\n        return 0;\n    } else {\n        let e = fiddle_log2(largest).max(-EXP_BIAS).min(127 - EXP_BIAS);\n        let inv_multiplier = fiddle_exp2(-e + 10);\n        let x = (floats.0 * inv_multiplier).min(2047.0) as u64;\n        let y = (floats.1 * inv_multiplier).min(2047.0) as u64;\n        let z = (floats.2 * inv_multiplier).min(2047.0) as u64;\n\n        (x << (11 + 11 + 7)) | (y << (11 + 7)) | (z << 7) | (e + EXP_BIAS) as u64\n    }\n}\n\n// Workhorse decode function, which operates on u64.\n#[inline(always)]\nfn decode_64(trifloat: u64) -> (f32, f32, f32) {\n    // Unpack values.\n    let x = trifloat >> (11 + 11 + 7);\n    let y = (trifloat >> (11 + 7)) & 0b111_1111_1111;\n    let z = (trifloat >> 7) & 0b111_1111_1111;\n    let e = trifloat & 0b111_1111;\n\n    let multiplier = fiddle_exp2(e as i32 - EXP_BIAS - 10);\n\n    (\n        x as f32 * multiplier,\n        y as f32 * multiplier,\n        z as f32 * multiplier,\n    )\n}\n\n#[inline(always)]\nfn u64_to_bytes(n: u64) -> [u8; 5] {\n    let a = n.to_ne_bytes();\n    let mut b = [0u8; 5];\n    if cfg!(target_endian = \"big\") {\n        (&mut b[..]).copy_from_slice(&a[3..8]);\n    } else {\n        (&mut b[..]).copy_from_slice(&a[0..5]);\n    }\n    b\n}\n\n#[inline(always)]\nfn bytes_to_u64(a: [u8; 5]) -> u64 {\n    let mut b = [0u8; 8];\n    if cfg!(target_endian = \"big\") {\n        (&mut b[3..8]).copy_from_slice(&a[..]);\n    } else {\n        (&mut b[0..5]).copy_from_slice(&a[..]);\n    }\n    u64::from_ne_bytes(b)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn round_trip(floats: (f32, f32, f32)) -> (f32, f32, f32) {\n        decode(encode(floats))\n    }\n\n    #[test]\n    fn all_zeros() {\n        let fs = (0.0f32, 0.0f32, 0.0f32);\n\n        let tri = encode_64(fs);\n        let fs2 = decode_64(tri);\n\n        assert_eq!(tri, 0u64);\n        assert_eq!(fs, fs2);\n    }\n\n    #[test]\n    fn powers_of_two() {\n        let fs = (8.0f32, 128.0f32, 0.5f32);\n        assert_eq!(fs, round_trip(fs));\n    }\n\n    #[test]\n    fn accuracy_01() {\n        let mut n = 1.0;\n        for _ in 0..1024 {\n            let (x, _, _) = round_trip((n, 0.0, 0.0));\n            assert_eq!(n, x);\n            n += 1.0 / 1024.0;\n        }\n    }\n\n    #[test]\n    #[should_panic]\n    fn accuracy_02() {\n        let mut n = 1.0;\n        for _ in 0..2048 {\n            let (x, _, _) = round_trip((n, 0.0, 0.0));\n            assert_eq!(n, x);\n            n += 1.0 / 2048.0;\n        }\n    }\n\n    #[test]\n    fn integers() {\n        for n in 0..=2048 {\n            let (x, _, _) = round_trip((n as f32, 0.0, 0.0));\n            assert_eq!(n as f32, x);\n        }\n    }\n\n    #[test]\n    fn precision_floor() {\n        let fs = (7.0f32, 2049.0f32, 1.0f32);\n        assert_eq!((6.0, 2048.0, 0.0), round_trip(fs));\n    }\n\n    #[test]\n    fn saturate() {\n        let fs = (1.0e+30, 1.0e+30, 1.0e+30);\n\n        assert_eq!((MAX, MAX, MAX), round_trip(fs));\n        assert_eq!((MAX, MAX, MAX), decode_64(0xff_ffff_ffff));\n    }\n\n    #[test]\n    fn inf_saturate() {\n        use std::f32::INFINITY;\n        let fs = (INFINITY, 0.0, 0.0);\n\n        assert_eq!((MAX, 0.0, 0.0), round_trip(fs));\n        assert_eq!(0xffe000007f, encode_64(fs));\n    }\n\n    #[test]\n    fn partial_saturate() {\n        let fs = (\n            1.0e+30,\n            (1u128 << (128 - EXP_BIAS - 11)) as f32,\n            (1u128 << (128 - EXP_BIAS - 12)) as f32,\n        );\n\n        assert_eq!(\n            (MAX, (1u128 << (128 - EXP_BIAS - 11)) as f32, 0.0),\n            round_trip(fs)\n        );\n    }\n\n    #[test]\n    fn smallest_value() {\n        let fs = (MIN * 1.5, MIN, MIN * 0.5);\n        assert_eq!((MIN, MIN, 0.0), round_trip(fs));\n        assert_eq!((MIN, MIN, 0.0), decode_64(0x20_04_00_00));\n    }\n\n    #[test]\n    fn underflow() {\n        let fs = (MIN * 0.99, 0.0, 0.0);\n        assert_eq!(0, encode_64(fs));\n        assert_eq!((0.0, 0.0, 0.0), round_trip(fs));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_01() {\n        encode((std::f32::NAN, 0.0, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_02() {\n        encode((0.0, std::f32::NAN, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn nans_03() {\n        encode((0.0, 0.0, std::f32::NAN));\n    }\n\n    #[test]\n    #[should_panic]\n    fn negative_01() {\n        encode((-1.0, 0.0, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn negative_02() {\n        encode((0.0, -1.0, 0.0));\n    }\n\n    #[test]\n    #[should_panic]\n    fn negative_03() {\n        encode((0.0, 0.0, -1.0));\n    }\n\n    #[test]\n    fn negative_04() {\n        encode((-0.0, -0.0, -0.0));\n    }\n}\n"
  },
  {
    "path": "sub_crates/compact/src/unit_vec/mod.rs",
    "content": "//! 3d unit vector formats.\n\npub mod oct32;\n"
  },
  {
    "path": "sub_crates/compact/src/unit_vec/oct32.rs",
    "content": "//! Encoding/decoding for a 32-bit representation of unit 3d vectors.\n//!\n//! Follows the Oct32 encoding specified in the paper \"A Survey\n//! of Efficient Representations for Independent Unit Vectors\" by\n//! Cigolle et al.\n\nconst STEP_SIZE: f32 = 1.0 / STEPS;\nconst STEPS: f32 = ((1 << (16 - 1)) - 1) as f32;\n\n/// Encodes a vector of three floats to the oct32 format.\n///\n/// The input vector does not need to be normalized--only the direction\n/// matters to the encoding process, not the length.\n#[inline]\npub fn encode(vec: (f32, f32, f32)) -> u32 {\n    let (u, v) = vec3_to_oct(vec);\n    ((to_snorm_16(u) as u32) << 16) | to_snorm_16(v) as u32\n}\n\n/// Encodes a vector of three floats to the oct32 format.\n///\n/// This is the same as `encode()` except that it is slower and encodes\n/// with slightly better precision.\npub fn encode_precise(vec: (f32, f32, f32)) -> u32 {\n    #[inline(always)]\n    fn dot_norm(a: (f32, f32, f32), b: (f32, f32, f32)) -> f64 {\n        let l = ((a.0 as f64 * a.0 as f64) + (a.1 as f64 * a.1 as f64) + (a.2 as f64 * a.2 as f64))\n            .sqrt();\n        ((a.0 as f64 * b.0 as f64) + (a.1 as f64 * b.1 as f64) + (a.2 as f64 * b.2 as f64)) / l\n    }\n\n    // Calculate the initial floored version.\n    let s = {\n        let mut s = vec3_to_oct(vec); // Remap to the square.\n        s.0 = (s.0.max(-1.0).min(1.0) * STEPS).floor() * STEP_SIZE;\n        s.1 = (s.1.max(-1.0).min(1.0) * STEPS).floor() * STEP_SIZE;\n        s\n    };\n\n    // Test all combinations of floor and ceil and keep the best.\n    // Note that at +/- 1, this will exit the square, but that\n    // will be a worse encoding and never win.\n    let mut best_rep = s;\n    let mut max_dot = 0.0;\n    for &(i, j) in &[\n        (0.0, 0.0),\n        (0.0, STEP_SIZE),\n        (STEP_SIZE, 0.0),\n        (STEP_SIZE, STEP_SIZE),\n    ] {\n        let candidate = (s.0 + i, s.1 + j);\n        let oct = oct_to_vec3(candidate);\n        let dot = dot_norm(oct, vec);\n        if dot > max_dot {\n            best_rep = candidate;\n            max_dot = dot;\n        }\n    }\n\n    ((to_snorm_16(best_rep.0) as u32) << 16) | to_snorm_16(best_rep.1) as u32\n}\n\n/// Decodes from an oct32 to a vector of three floats.\n///\n/// The returned vector will not generally be normalized.  Code that\n/// needs a normalized vector should normalize the returned vector.\n#[inline]\npub fn decode(n: u32) -> (f32, f32, f32) {\n    oct_to_vec3((from_snorm_16((n >> 16) as u16), from_snorm_16(n as u16)))\n}\n\n#[inline(always)]\nfn vec3_to_oct(vec: (f32, f32, f32)) -> (f32, f32) {\n    let l1_norm = vec.0.abs() + vec.1.abs() + vec.2.abs();\n    let u = vec.0 / l1_norm;\n    let v = vec.1 / l1_norm;\n\n    if vec.2 > 0.0 {\n        (u, v)\n    } else {\n        ((1.0 - v.abs()) * sign(vec.0), (1.0 - u.abs()) * sign(vec.1))\n    }\n}\n\n#[inline(always)]\nfn oct_to_vec3(oct: (f32, f32)) -> (f32, f32, f32) {\n    let vec2 = 1.0 - (oct.0.abs() + oct.1.abs());\n\n    if vec2 < 0.0 {\n        (\n            (1.0 - oct.1.abs()) * sign(oct.0),\n            (1.0 - oct.0.abs()) * sign(oct.1),\n            vec2,\n        )\n    } else {\n        (oct.0, oct.1, vec2)\n    }\n}\n\n#[inline(always)]\nfn to_snorm_16(n: f32) -> u16 {\n    (n * STEPS).round() as i16 as u16\n}\n\n#[inline(always)]\nfn from_snorm_16(n: u16) -> f32 {\n    f32::from(n as i16) * STEP_SIZE\n}\n\n#[inline(always)]\nfn sign(n: f32) -> f32 {\n    if n < 0.0 {\n        -1.0\n    } else {\n        1.0\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn axis_directions() {\n        let px = (1.0, 0.0, 0.0);\n        let px_oct = encode(px);\n        let px_octp = encode_precise(px);\n\n        let nx = (-1.0, 0.0, 0.0);\n        let nx_oct = encode(nx);\n        let nx_octp = encode_precise(nx);\n\n        let py = (0.0, 1.0, 0.0);\n        let py_oct = encode(py);\n        let py_octp = encode_precise(py);\n\n        let ny = (0.0, -1.0, 0.0);\n        let ny_oct = encode(ny);\n        let ny_octp = encode_precise(ny);\n\n        let pz = (0.0, 0.0, 1.0);\n        let pz_oct = encode(pz);\n        let pz_octp = encode_precise(pz);\n\n        let nz = (0.0, 0.0, -1.0);\n        let nz_oct = encode(nz);\n        let nz_octp = encode_precise(nz);\n\n        assert_eq!(px, decode(px_oct));\n        assert_eq!(nx, decode(nx_oct));\n        assert_eq!(py, decode(py_oct));\n        assert_eq!(ny, decode(ny_oct));\n        assert_eq!(pz, decode(pz_oct));\n        assert_eq!(nz, decode(nz_oct));\n\n        assert_eq!(px, decode(px_octp));\n        assert_eq!(nx, decode(nx_octp));\n        assert_eq!(py, decode(py_octp));\n        assert_eq!(ny, decode(ny_octp));\n        assert_eq!(pz, decode(pz_octp));\n        assert_eq!(nz, decode(nz_octp));\n    }\n}\n"
  },
  {
    "path": "sub_crates/compact/tests/proptest_tests.rs",
    "content": "#[macro_use]\nextern crate proptest;\n\nuse compact::unit_vec::oct32::{decode, encode, encode_precise};\nuse proptest::test_runner::Config;\n\n/// Calculates the cosine of the angle between the two vectors,\n/// and checks to see if it's greater than the passed cos.\nfn cos_gt(a: (f32, f32, f32), b: (f32, f32, f32), cos: f64) -> bool {\n    fn normalize(v: (f32, f32, f32)) -> (f64, f64, f64) {\n        let norm =\n            ((v.0 as f64 * v.0 as f64) + (v.1 as f64 * v.1 as f64) + (v.2 as f64 * v.2 as f64))\n                .sqrt();\n        (v.0 as f64 / norm, v.1 as f64 / norm, v.2 as f64 / norm)\n    }\n    let a = normalize(a);\n    let b = normalize(b);\n    let cos2 = (a.0 * b.0) + (a.1 * b.1) + (a.2 * b.2);\n    let r = cos2 > cos as f64;\n\n    if !r {\n        println!(\"cos: {}, left: {:?}, right: {:?}\", cos2, a, b);\n    }\n\n    r\n}\n\n/// Checks if the difference between the two vectors on all axes is\n/// less than delta.  Both vectors are L1-normalized first.\nfn l1_delta_lt(a: (f32, f32, f32), b: (f32, f32, f32), delta: f32) -> bool {\n    fn l1_normalize(v: (f32, f32, f32)) -> (f32, f32, f32) {\n        let l1_norm = v.0.abs() + v.1.abs() + v.2.abs();\n        (v.0 / l1_norm, v.1 / l1_norm, v.2 / l1_norm)\n    }\n\n    let a = l1_normalize(a);\n    let b = l1_normalize(b);\n\n    let rx = (a.0 - b.0).abs() < delta;\n    let ry = (a.1 - b.1).abs() < delta;\n    let rz = (a.2 - b.2).abs() < delta;\n\n    let r = rx && ry && rz;\n\n    if !r {\n        println!(\"left: {:?}, right: {:?}\", a, b);\n    }\n\n    r\n}\n\nproptest! {\n    #![proptest_config(Config::with_cases(4096))]\n\n    #[test]\n    fn oct32_pt_roundtrip_angle_precision(v in (-1.0f32..1.0, -1.0f32..1.0, -1.0f32..1.0)) {\n        let oct = encode(v);\n        let octp = encode_precise(v);\n\n        // Check if the angle between the original and the roundtrip\n        // is less than 0.004 degrees\n        assert!(cos_gt(v, decode(oct), 0.9999999976));\n\n        // Check if the angle between the original and the roundtrip\n        // is less than 0.003 degrees\n        assert!(cos_gt(v, decode(octp), 0.9999999986));\n    }\n\n    #[test]\n    fn oct32_pt_roundtrip_component_precision(v in (-1.0f32..1.0, -1.0f32..1.0, -1.0f32..1.0)) {\n        let oct = encode(v);\n        let octp = encode_precise(v);\n\n        assert!(l1_delta_lt(v, decode(oct), 0.00005));\n        assert!(l1_delta_lt(v, decode(octp), 0.00003));\n    }\n}\n"
  },
  {
    "path": "sub_crates/halton/Cargo.toml",
    "content": "[package]\nname = \"halton\"\nversion = \"0.1.0\"\nauthors = [\"Nathan Vegdahl <cessen@cessen.com>\"]\nedition = \"2018\"\nlicense = \"MIT\"\nbuild = \"build.rs\"\n\n[lib]\nname = \"halton\"\npath = \"src/lib.rs\"\n"
  },
  {
    "path": "sub_crates/halton/LICENSE.md",
    "content": "The code in this project is adapted from code written by Leonhard Gruenschloss:\n\nCopyright (c) 2012 Leonhard Gruenschloss (leonhard@gruenschloss.org)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "sub_crates/halton/build.rs",
    "content": "// Copyright (c) 2012 Leonhard Gruenschloss (leonhard@gruenschloss.org)\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights to\n// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\n// of the Software, and to permit persons to whom the Software is furnished to do\n// so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in\n// all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n//\n// Adapted from Python to Rust and to generate Rust instead of C by Nathan Vegdahl\n\n// Generate Rust code for evaluating Halton points with Faure-permutations for different bases.\n\nuse std::{env, fs::File, io::Write, path::Path};\n\n/// How many components to generate.\nconst NUM_DIMENSIONS: usize = 128;\n\nfn main() {\n    let out_dir = env::var(\"OUT_DIR\").unwrap();\n    let dest_path = Path::new(&out_dir).join(\"halton.rs\");\n    let mut f = File::create(&dest_path).unwrap();\n\n    // Init prime number array.\n    let primes = {\n        let mut primes = Vec::new();\n        let mut candidate = 1;\n        for _ in 0..NUM_DIMENSIONS {\n            loop {\n                candidate += 1;\n                if is_prime(candidate) {\n                    primes.push(candidate);\n                    break;\n                }\n            }\n        }\n        primes\n    };\n\n    // Init Faure permutations.\n    let faure = {\n        let mut faure: Vec<Vec<usize>> = Vec::new();\n        for b in 0..(primes.last().unwrap() + 1) {\n            let perm = get_faure_permutation(&faure, b);\n            faure.push(perm);\n        }\n        faure\n    };\n\n    // Write the beginning bits of the file\n    f.write_all(\n        format!(\n            r#\"\n// Copyright (c) 2012 Leonhard Gruenschloss (leonhard@gruenschloss.org)\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights to\n// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\n// of the Software, and to permit persons to whom the Software is furnished to do\n// so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in\n// all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n// This file is automatically generated.\n\n// Compute points of the Halton sequence with with Faure-permutations for different bases.\n\npub const MAX_DIMENSION: u32 = {};\n\"#,\n            NUM_DIMENSIONS\n        )\n        .as_bytes(),\n    )\n    .unwrap();\n\n    // Write the sampling function\n    f.write_all(\n        format!(\n            r#\"\npub fn sample(index: u32, dimension: u32) -> f32 {{\n    let mut index = index;\n\n    match dimension {{\"#\n        )\n        .as_bytes(),\n    )\n    .unwrap();\n\n    // Write the special-cased first dimension\n    f.write_all(\n        format!(\n            r#\"\n        // Special case: radical inverse in base 2, with direct bit reversal.\n        0 => {{\n            index = (index << 16) | (index >> 16);\n            index = ((index & 0x00ff00ff) << 8) | ((index & 0xff00ff00) >> 8);\n            index = ((index & 0x0f0f0f0f) << 4) | ((index & 0xf0f0f0f0) >> 4);\n            index = ((index & 0x33333333) << 2) | ((index & 0xcccccccc) >> 2);\n            index = ((index & 0x55555555) << 1) | ((index & 0xaaaaaaaa) >> 1);\n            return (index as f32) * (1.0 / ((1u64 << 32) as f32));\n        }}\"#,\n        )\n        .as_bytes(),\n    )\n    .unwrap();\n\n    // The rest of the dimensions.\n    for i in 1..NUM_DIMENSIONS {\n        let base = primes[i];\n\n        // Based on the permutation table size, we process multiple digits at once.\n        let mut digits = 1;\n        let mut pow_base = base;\n        while pow_base * base <= 500 {\n            // Maximum permutation table size.\n            pow_base *= base;\n            digits += 1;\n        }\n\n        let mut max_power = pow_base;\n        let mut powers = Vec::new();\n        while (max_power * pow_base) < (1 << 32) {\n            // 32-bit unsigned precision\n            powers.push(max_power);\n            max_power *= pow_base;\n        }\n\n        // Build the permutation table.\n        let perm = (0..pow_base)\n            .map(|j| invert(&faure, base, j, digits))\n            .collect::<Vec<_>>();\n        let perm_string = {\n            let mut perm_string = String::new();\n            for i in perm.iter() {\n                let s = format!(\"{}, \", i);\n                perm_string.push_str(&s);\n            }\n            perm_string\n        };\n\n        let mut power = max_power / pow_base;\n        f.write_all(\n            format!(\n                r#\"\n\n        {} => {{\n            static PERM{}: [u16; {}] = [{}];\"#,\n                i,\n                base,\n                perm.len(),\n                perm_string\n            )\n            .as_bytes(),\n        )\n        .unwrap();\n\n        f.write_all(\n            format!(\n                r#\"\n            return unsafe {{(\n                *PERM{}.get_unchecked((index % {}) as usize) as u32 * {}\"#,\n                base, pow_base, power\n            )\n            .as_bytes(),\n        )\n        .unwrap();\n\n        // Advance to next set of digits.\n        let mut div = 1;\n        while power / pow_base > 1 {\n            div *= pow_base;\n            power /= pow_base;\n            f.write_all(\n                format!(\n                    r#\"\n                + *PERM{}.get_unchecked(((index / {}) % {}) as usize) as u32 * {}\"#,\n                    base, div, pow_base, power\n                )\n                .as_bytes(),\n            )\n            .unwrap();\n        }\n\n        f.write_all(\n            format!(\n                r#\"\n                + *PERM{}.get_unchecked(((index / {}) % {}) as usize) as u32\n            )}} as f32\n            * (0.999999940395355224609375f32 / ({}u32 as f32)); // Results in [0,1).\n        }}\n        \"#,\n                base,\n                div * pow_base,\n                pow_base,\n                max_power\n            )\n            .as_bytes(),\n        )\n        .unwrap();\n    }\n\n    f.write_all(\n        format!(\n            r#\"\n        _ => panic!(\"Halton sampling: exceeded max dimensions.\"),\n    }}\n}}\n    \"#\n        )\n        .as_bytes(),\n    )\n    .unwrap();\n}\n\n/// Check primality. Not optimized, since it's not performance-critical.\nfn is_prime(p: usize) -> bool {\n    for i in 2..p {\n        if (p % i) == 0 {\n            return false;\n        }\n    }\n    return true;\n}\n\n/// Computes the Faure digit permutation for 0, ..., b - 1.\nfn get_faure_permutation(faure: &Vec<Vec<usize>>, b: usize) -> Vec<usize> {\n    if b < 2 {\n        return vec![0];\n    } else if b == 2 {\n        return vec![0, 1];\n    } else if (b & 1) != 0 {\n        // odd\n        let c = (b - 1) / 2;\n\n        return (0..b)\n            .map(|i| {\n                if i == c {\n                    return c;\n                }\n\n                let f: usize = faure[b - 1][i - ((i > c) as usize)];\n                f + ((f >= c) as usize)\n            })\n            .collect();\n    } else {\n        // even\n        let c = b / 2;\n\n        return (0..b)\n            .map(|i| {\n                if i < c {\n                    2 * faure[c][i]\n                } else {\n                    2 * faure[c][i - c] + 1\n                }\n            })\n            .collect();\n    }\n}\n\n/// Compute the radical inverse with Faure permutations.\nfn invert(faure: &Vec<Vec<usize>>, base: usize, mut index: usize, digits: usize) -> usize {\n    let mut result = 0;\n    for _ in 0..digits {\n        let remainder = index % base;\n        index = index / base;\n        result = result * base + faure[base][remainder];\n    }\n    return result;\n}\n"
  },
  {
    "path": "sub_crates/halton/src/lib.rs",
    "content": "#![allow(dead_code)]\n#![allow(unused_parens)]\n#![allow(clippy::cast_lossless)]\n#![allow(clippy::excessive_precision)]\n#![allow(clippy::unreadable_literal)]\n#![allow(clippy::needless_return)]\n\n// Include the file generated by the build.rs script\ninclude!(concat!(env!(\"OUT_DIR\"), \"/halton.rs\"));\n"
  },
  {
    "path": "sub_crates/math3d/Cargo.toml",
    "content": "[package]\nname = \"math3d\"\nversion = \"0.1.0\"\nauthors = [\"Nathan Vegdahl <cessen@cessen.com>\"]\nedition = \"2018\"\nlicense = \"MIT, Apache 2.0\"\n\n[lib]\nname = \"math3d\"\npath = \"src/lib.rs\"\n\n# Local crate dependencies\n[dependencies]\nglam = \"0.15\"\napprox = \"0.4\"\n"
  },
  {
    "path": "sub_crates/math3d/LICENSE.md",
    "content": "Copyright (c) 2020 Nathan Vegdahl\n\nThis project is licensed under either of\n\n* MIT license (licenses/MIT.txt or http://opensource.org/licenses/MIT)\n* Apache License, Version 2.0, (licenses/Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0)\n\nat your option.\n"
  },
  {
    "path": "sub_crates/math3d/src/lib.rs",
    "content": "#![allow(dead_code)]\n\nmod normal;\nmod point;\nmod transform;\nmod vector;\n\npub use self::{normal::Normal, point::Point, transform::Transform, vector::Vector};\n\n/// Trait for calculating dot products.\npub trait DotProduct {\n    fn dot(self, other: Self) -> f32;\n}\n\n#[inline]\npub fn dot<T: DotProduct>(a: T, b: T) -> f32 {\n    a.dot(b)\n}\n\n/// Trait for calculating cross products.\npub trait CrossProduct {\n    fn cross(self, other: Self) -> Self;\n}\n\n#[inline]\npub fn cross<T: CrossProduct>(a: T, b: T) -> T {\n    a.cross(b)\n}\n"
  },
  {
    "path": "sub_crates/math3d/src/normal.rs",
    "content": "#![allow(dead_code)]\n\nuse std::{\n    cmp::PartialEq,\n    ops::{Add, Div, Mul, Neg, Sub},\n};\n\nuse glam::Vec3A;\n\nuse super::{CrossProduct, DotProduct, Transform, Vector};\n\n/// A surface normal in 3d homogeneous space.\n#[derive(Debug, Copy, Clone)]\npub struct Normal {\n    pub co: Vec3A,\n}\n\nimpl Normal {\n    #[inline(always)]\n    pub fn new(x: f32, y: f32, z: f32) -> Normal {\n        Normal {\n            co: Vec3A::new(x, y, z),\n        }\n    }\n\n    #[inline(always)]\n    pub fn length(&self) -> f32 {\n        self.co.length()\n    }\n\n    #[inline(always)]\n    pub fn length2(&self) -> f32 {\n        self.co.length_squared()\n    }\n\n    #[inline(always)]\n    pub fn normalized(&self) -> Normal {\n        Normal {\n            co: self.co.normalize(),\n        }\n    }\n\n    #[inline(always)]\n    pub fn into_vector(self) -> Vector {\n        Vector { co: self.co }\n    }\n\n    #[inline(always)]\n    pub fn get_n(&self, n: usize) -> f32 {\n        match n {\n            0 => self.x(),\n            1 => self.y(),\n            2 => self.z(),\n            _ => panic!(\"Attempt to access dimension beyond z.\"),\n        }\n    }\n\n    #[inline(always)]\n    pub fn x(&self) -> f32 {\n        self.co[0]\n    }\n\n    #[inline(always)]\n    pub fn y(&self) -> f32 {\n        self.co[1]\n    }\n\n    #[inline(always)]\n    pub fn z(&self) -> f32 {\n        self.co[2]\n    }\n\n    #[inline(always)]\n    pub fn set_x(&mut self, x: f32) {\n        self.co[0] = x;\n    }\n\n    #[inline(always)]\n    pub fn set_y(&mut self, y: f32) {\n        self.co[1] = y;\n    }\n\n    #[inline(always)]\n    pub fn set_z(&mut self, z: f32) {\n        self.co[2] = z;\n    }\n}\n\nimpl PartialEq for Normal {\n    #[inline(always)]\n    fn eq(&self, other: &Normal) -> bool {\n        self.co == other.co\n    }\n}\n\nimpl Add for Normal {\n    type Output = Normal;\n\n    #[inline(always)]\n    fn add(self, other: Normal) -> Normal {\n        Normal {\n            co: self.co + other.co,\n        }\n    }\n}\n\nimpl Sub for Normal {\n    type Output = Normal;\n\n    #[inline(always)]\n    fn sub(self, other: Normal) -> Normal {\n        Normal {\n            co: self.co - other.co,\n        }\n    }\n}\n\nimpl Mul<f32> for Normal {\n    type Output = Normal;\n\n    #[inline(always)]\n    fn mul(self, other: f32) -> Normal {\n        Normal {\n            co: self.co * other,\n        }\n    }\n}\n\nimpl Mul<Transform> for Normal {\n    type Output = Normal;\n\n    #[inline]\n    fn mul(self, other: Transform) -> Normal {\n        Normal {\n            co: other.0.matrix3.inverse().transpose().mul_vec3a(self.co),\n        }\n    }\n}\n\nimpl Div<f32> for Normal {\n    type Output = Normal;\n\n    #[inline(always)]\n    fn div(self, other: f32) -> Normal {\n        Normal {\n            co: self.co / other,\n        }\n    }\n}\n\nimpl Neg for Normal {\n    type Output = Normal;\n\n    #[inline(always)]\n    fn neg(self) -> Normal {\n        Normal { co: self.co * -1.0 }\n    }\n}\n\nimpl DotProduct for Normal {\n    #[inline(always)]\n    fn dot(self, other: Normal) -> f32 {\n        self.co.dot(other.co)\n    }\n}\n\nimpl CrossProduct for Normal {\n    #[inline]\n    fn cross(self, other: Normal) -> Normal {\n        Normal {\n            co: self.co.cross(other.co),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::super::{CrossProduct, DotProduct, Transform};\n    use super::*;\n    use approx::assert_ulps_eq;\n\n    #[test]\n    fn add() {\n        let v1 = Normal::new(1.0, 2.0, 3.0);\n        let v2 = Normal::new(1.5, 4.5, 2.5);\n        let v3 = Normal::new(2.5, 6.5, 5.5);\n\n        assert_eq!(v3, v1 + v2);\n    }\n\n    #[test]\n    fn sub() {\n        let v1 = Normal::new(1.0, 2.0, 3.0);\n        let v2 = Normal::new(1.5, 4.5, 2.5);\n        let v3 = Normal::new(-0.5, -2.5, 0.5);\n\n        assert_eq!(v3, v1 - v2);\n    }\n\n    #[test]\n    fn mul_scalar() {\n        let v1 = Normal::new(1.0, 2.0, 3.0);\n        let v2 = 2.0;\n        let v3 = Normal::new(2.0, 4.0, 6.0);\n\n        assert_eq!(v3, v1 * v2);\n    }\n\n    #[test]\n    fn mul_matrix_1() {\n        let n = Normal::new(1.0, 2.5, 4.0);\n        let m = Transform::new_from_values(\n            1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0,\n        );\n        let nm = n * m;\n        let nm2 = Normal::new(-4.0625, 1.78125, -0.03125);\n        for i in 0..3 {\n            assert_ulps_eq!(nm.co[i], nm2.co[i], max_ulps = 4);\n        }\n    }\n\n    #[test]\n    fn div() {\n        let v1 = Normal::new(1.0, 2.0, 3.0);\n        let v2 = 2.0;\n        let v3 = Normal::new(0.5, 1.0, 1.5);\n\n        assert_eq!(v3, v1 / v2);\n    }\n\n    #[test]\n    fn length() {\n        let n = Normal::new(1.0, 2.0, 3.0);\n        assert!((n.length() - 3.7416573867739413).abs() < 0.000001);\n    }\n\n    #[test]\n    fn length2() {\n        let n = Normal::new(1.0, 2.0, 3.0);\n        assert_eq!(n.length2(), 14.0);\n    }\n\n    #[test]\n    fn normalized() {\n        let n1 = Normal::new(1.0, 2.0, 3.0);\n        let n2 = Normal::new(0.2672612419124244, 0.5345224838248488, 0.8017837257372732);\n        let n3 = n1.normalized();\n        assert!((n3.x() - n2.x()).abs() < 0.000001);\n        assert!((n3.y() - n2.y()).abs() < 0.000001);\n        assert!((n3.z() - n2.z()).abs() < 0.000001);\n    }\n\n    #[test]\n    fn dot_test() {\n        let v1 = Normal::new(1.0, 2.0, 3.0);\n        let v2 = Normal::new(1.5, 4.5, 2.5);\n        let v3 = 18.0f32;\n\n        assert_eq!(v3, v1.dot(v2));\n    }\n\n    #[test]\n    fn cross_test() {\n        let v1 = Normal::new(1.0, 0.0, 0.0);\n        let v2 = Normal::new(0.0, 1.0, 0.0);\n        let v3 = Normal::new(0.0, 0.0, 1.0);\n\n        assert_eq!(v3, v1.cross(v2));\n    }\n}\n"
  },
  {
    "path": "sub_crates/math3d/src/point.rs",
    "content": "#![allow(dead_code)]\n\nuse std::{\n    cmp::PartialEq,\n    ops::{Add, Mul, Sub},\n};\n\nuse glam::Vec3A;\n\nuse super::{Transform, Vector};\n\n/// A position in 3d homogeneous space.\n#[derive(Debug, Copy, Clone)]\npub struct Point {\n    pub co: Vec3A,\n}\n\nimpl Point {\n    #[inline(always)]\n    pub fn new(x: f32, y: f32, z: f32) -> Point {\n        Point {\n            co: Vec3A::new(x, y, z),\n        }\n    }\n\n    #[inline(always)]\n    pub fn min(&self, other: Point) -> Point {\n        let n1 = self;\n        let n2 = other;\n\n        Point {\n            co: n1.co.min(n2.co),\n        }\n    }\n\n    #[inline(always)]\n    pub fn max(&self, other: Point) -> Point {\n        let n1 = self;\n        let n2 = other;\n\n        Point {\n            co: n1.co.max(n2.co),\n        }\n    }\n\n    #[inline(always)]\n    pub fn into_vector(self) -> Vector {\n        Vector { co: self.co }\n    }\n\n    #[inline(always)]\n    pub fn get_n(&self, n: usize) -> f32 {\n        match n {\n            0 => self.x(),\n            1 => self.y(),\n            2 => self.z(),\n            _ => panic!(\"Attempt to access dimension beyond z.\"),\n        }\n    }\n\n    #[inline(always)]\n    pub fn x(&self) -> f32 {\n        self.co[0]\n    }\n\n    #[inline(always)]\n    pub fn y(&self) -> f32 {\n        self.co[1]\n    }\n\n    #[inline(always)]\n    pub fn z(&self) -> f32 {\n        self.co[2]\n    }\n\n    #[inline(always)]\n    pub fn set_x(&mut self, x: f32) {\n        self.co[0] = x;\n    }\n\n    #[inline(always)]\n    pub fn set_y(&mut self, y: f32) {\n        self.co[1] = y;\n    }\n\n    #[inline(always)]\n    pub fn set_z(&mut self, z: f32) {\n        self.co[2] = z;\n    }\n}\n\nimpl PartialEq for Point {\n    #[inline(always)]\n    fn eq(&self, other: &Point) -> bool {\n        self.co == other.co\n    }\n}\n\nimpl Add<Vector> for Point {\n    type Output = Point;\n\n    #[inline(always)]\n    fn add(self, other: Vector) -> Point {\n        Point {\n            co: self.co + other.co,\n        }\n    }\n}\n\nimpl Sub for Point {\n    type Output = Vector;\n\n    #[inline(always)]\n    fn sub(self, other: Point) -> Vector {\n        Vector {\n            co: self.co - other.co,\n        }\n    }\n}\n\nimpl Sub<Vector> for Point {\n    type Output = Point;\n\n    #[inline(always)]\n    fn sub(self, other: Vector) -> Point {\n        Point {\n            co: self.co - other.co,\n        }\n    }\n}\n\nimpl Mul<Transform> for Point {\n    type Output = Point;\n\n    #[inline]\n    fn mul(self, other: Transform) -> Point {\n        Point {\n            co: other.0.transform_point3a(self.co),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::super::{Transform, Vector};\n    use super::*;\n\n    #[test]\n    fn add() {\n        let p1 = Point::new(1.0, 2.0, 3.0);\n        let v1 = Vector::new(1.5, 4.5, 2.5);\n        let p2 = Point::new(2.5, 6.5, 5.5);\n\n        assert_eq!(p2, p1 + v1);\n    }\n\n    #[test]\n    fn sub() {\n        let p1 = Point::new(1.0, 2.0, 3.0);\n        let p2 = Point::new(1.5, 4.5, 2.5);\n        let v1 = Vector::new(-0.5, -2.5, 0.5);\n\n        assert_eq!(v1, p1 - p2);\n    }\n\n    #[test]\n    fn mul_matrix_1() {\n        let p = Point::new(1.0, 2.5, 4.0);\n        let m = Transform::new_from_values(\n            1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0,\n        );\n        let pm = Point::new(15.5, 54.0, 70.0);\n        assert_eq!(p * m, pm);\n    }\n\n    #[test]\n    fn mul_matrix_2() {\n        let p = Point::new(1.0, 2.5, 4.0);\n        let m = Transform::new_from_values(\n            1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0,\n        );\n        let pm = Point::new(15.5, 54.0, 70.0);\n        assert_eq!(p * m, pm);\n    }\n\n    #[test]\n    fn mul_matrix_3() {\n        // Make sure matrix multiplication composes the way one would expect\n        let p = Point::new(1.0, 2.5, 4.0);\n        let m1 = Transform::new_from_values(\n            1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0,\n        );\n        let m2 =\n            Transform::new_from_values(4.0, 1.0, 2.0, 3.5, 3.0, 6.0, 5.0, 2.0, 2.0, 2.0, 4.0, 12.0);\n        println!(\"{:?}\", m1 * m2);\n\n        let pmm1 = p * (m1 * m2);\n        let pmm2 = (p * m1) * m2;\n\n        assert!((pmm1 - pmm2).length2() <= 0.00001); // Assert pmm1 and pmm2 are roughly equal\n    }\n}\n"
  },
  {
    "path": "sub_crates/math3d/src/transform.rs",
    "content": "#![allow(dead_code)]\n\nuse std::ops::{Add, Mul};\n\nuse approx::relative_eq;\nuse glam::{Affine3A, Mat3, Mat4, Vec3};\n\nuse super::Point;\n\n/// A 4x3 affine transform matrix, used for transforms.\n#[derive(Debug, Copy, Clone, PartialEq)]\npub struct Transform(pub Affine3A);\n\nimpl Transform {\n    /// Creates a new identity matrix\n    #[inline]\n    pub fn new() -> Transform {\n        Transform(Affine3A::IDENTITY)\n    }\n\n    /// Creates a new matrix with the specified values:\n    /// a b c d\n    /// e f g h\n    /// i j k l\n    /// m n o p\n    #[inline]\n    #[allow(clippy::many_single_char_names)]\n    #[allow(clippy::too_many_arguments)]\n    pub fn new_from_values(\n        a: f32,\n        b: f32,\n        c: f32,\n        d: f32,\n        e: f32,\n        f: f32,\n        g: f32,\n        h: f32,\n        i: f32,\n        j: f32,\n        k: f32,\n        l: f32,\n    ) -> Transform {\n        Transform(Affine3A::from_mat3_translation(\n            Mat3::from_cols(Vec3::new(a, e, i), Vec3::new(b, f, j), Vec3::new(c, g, k)),\n            Vec3::new(d, h, l),\n        ))\n    }\n\n    #[inline]\n    pub fn from_location(loc: Point) -> Transform {\n        Transform(Affine3A::from_translation(loc.co.into()))\n    }\n\n    /// Returns whether the matrices are approximately equal to each other.\n    /// Each corresponding element in the matrices cannot have a relative\n    /// error exceeding epsilon.\n    #[inline]\n    pub fn aprx_eq(&self, other: Transform, epsilon: f32) -> bool {\n        let mut eq = true;\n        for c in 0..3 {\n            for r in 0..3 {\n                let a = self.0.matrix3.col(c)[r];\n                let b = other.0.matrix3.col(c)[r];\n                eq &= relative_eq!(a, b, epsilon = epsilon);\n            }\n        }\n        for i in 0..3 {\n            let a = self.0.translation[i];\n            let b = other.0.translation[i];\n            eq &= relative_eq!(a, b, epsilon = epsilon);\n        }\n        eq\n    }\n\n    /// Returns the inverse of the Matrix\n    #[inline]\n    pub fn inverse(&self) -> Transform {\n        Transform(self.0.inverse())\n    }\n}\n\nimpl Default for Transform {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n/// Multiply two matrices together\nimpl Mul for Transform {\n    type Output = Self;\n\n    #[inline]\n    fn mul(self, other: Self) -> Self {\n        Self(other.0 * self.0)\n    }\n}\n\n/// Multiply a matrix by a f32\nimpl Mul<f32> for Transform {\n    type Output = Self;\n\n    #[inline]\n    fn mul(self, other: f32) -> Self {\n        Self(Affine3A::from_mat4(Mat4::from(self.0) * other))\n    }\n}\n\n/// Add two matrices together\nimpl Add for Transform {\n    type Output = Self;\n\n    #[inline]\n    fn add(self, other: Self) -> Self {\n        Self(Affine3A::from_mat4(\n            Mat4::from(self.0) + Mat4::from(other.0),\n        ))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn equality_test() {\n        let a = Transform::new();\n        let b = Transform::new();\n        let c =\n            Transform::new_from_values(1.1, 0.0, 0.0, 0.0, 0.0, 1.1, 0.0, 0.0, 0.0, 0.0, 1.1, 0.0);\n\n        assert_eq!(a, b);\n        assert!(a != c);\n    }\n\n    #[test]\n    fn approximate_equality_test() {\n        let a = Transform::new();\n        let b = Transform::new_from_values(\n            1.000001, 0.0, 0.0, 0.0, 0.0, 1.000001, 0.0, 0.0, 0.0, 0.0, 1.000001, 0.0,\n        );\n        let c = Transform::new_from_values(\n            1.000003, 0.0, 0.0, 0.0, 0.0, 1.000003, 0.0, 0.0, 0.0, 0.0, 1.000003, 0.0,\n        );\n        let d = Transform::new_from_values(\n            -1.000001, 0.0, 0.0, 0.0, 0.0, -1.000001, 0.0, 0.0, 0.0, 0.0, -1.000001, 0.0,\n        );\n\n        assert!(a.aprx_eq(b, 0.000001));\n        assert!(!a.aprx_eq(c, 0.000001));\n        assert!(!a.aprx_eq(d, 0.000001));\n    }\n\n    #[test]\n    fn multiply_test() {\n        let a = Transform::new_from_values(\n            1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0,\n        );\n        let b = Transform::new_from_values(\n            1.0, 5.0, 9.0, 13.0, 2.0, 6.0, 10.0, 14.0, 3.0, 7.0, 11.0, 15.0,\n        );\n        let c = Transform::new_from_values(\n            97.0, 50.0, 136.0, 162.5, 110.0, 60.0, 156.0, 185.0, 123.0, 70.0, 176.0, 207.5,\n        );\n\n        assert_eq!(a * b, c);\n    }\n\n    #[test]\n    fn inverse_test() {\n        let a = Transform::new_from_values(\n            1.0, 0.33, 0.0, -2.0, 0.0, 1.0, 0.0, 0.0, 2.1, 0.7, 1.3, 0.0,\n        );\n        let b = a.inverse();\n        let c = Transform::new();\n\n        assert!((dbg!(a * b)).aprx_eq(dbg!(c), 0.0000001));\n    }\n}\n"
  },
  {
    "path": "sub_crates/math3d/src/vector.rs",
    "content": "#![allow(dead_code)]\n\nuse std::{\n    cmp::PartialEq,\n    ops::{Add, Div, Mul, Neg, Sub},\n};\n\nuse glam::Vec3A;\n\nuse super::{CrossProduct, DotProduct, Normal, Point, Transform};\n\n/// A direction vector in 3d homogeneous space.\n#[derive(Debug, Copy, Clone)]\npub struct Vector {\n    pub co: Vec3A,\n}\n\nimpl Vector {\n    #[inline(always)]\n    pub fn new(x: f32, y: f32, z: f32) -> Vector {\n        Vector {\n            co: Vec3A::new(x, y, z),\n        }\n    }\n\n    #[inline(always)]\n    pub fn length(&self) -> f32 {\n        self.co.length()\n    }\n\n    #[inline(always)]\n    pub fn length2(&self) -> f32 {\n        self.co.length_squared()\n    }\n\n    #[inline(always)]\n    pub fn normalized(&self) -> Vector {\n        Vector {\n            co: self.co.normalize(),\n        }\n    }\n\n    #[inline(always)]\n    pub fn abs(&self) -> Vector {\n        Vector {\n            co: self.co * self.co.signum(),\n        }\n    }\n\n    #[inline(always)]\n    pub fn into_point(self) -> Point {\n        Point { co: self.co }\n    }\n\n    #[inline(always)]\n    pub fn into_normal(self) -> Normal {\n        Normal { co: self.co }\n    }\n\n    #[inline(always)]\n    pub fn get_n(&self, n: usize) -> f32 {\n        match n {\n            0 => self.x(),\n            1 => self.y(),\n            2 => self.z(),\n            _ => panic!(\"Attempt to access dimension beyond z.\"),\n        }\n    }\n\n    #[inline(always)]\n    pub fn x(&self) -> f32 {\n        self.co[0]\n    }\n\n    #[inline(always)]\n    pub fn y(&self) -> f32 {\n        self.co[1]\n    }\n\n    #[inline(always)]\n    pub fn z(&self) -> f32 {\n        self.co[2]\n    }\n\n    #[inline(always)]\n    pub fn set_x(&mut self, x: f32) {\n        self.co[0] = x;\n    }\n\n    #[inline(always)]\n    pub fn set_y(&mut self, y: f32) {\n        self.co[1] = y;\n    }\n\n    #[inline(always)]\n    pub fn set_z(&mut self, z: f32) {\n        self.co[2] = z;\n    }\n}\n\nimpl PartialEq for Vector {\n    #[inline(always)]\n    fn eq(&self, other: &Vector) -> bool {\n        self.co == other.co\n    }\n}\n\nimpl Add for Vector {\n    type Output = Vector;\n\n    #[inline(always)]\n    fn add(self, other: Vector) -> Vector {\n        Vector {\n            co: self.co + other.co,\n        }\n    }\n}\n\nimpl Sub for Vector {\n    type Output = Vector;\n\n    #[inline(always)]\n    fn sub(self, other: Vector) -> Vector {\n        Vector {\n            co: self.co - other.co,\n        }\n    }\n}\n\nimpl Mul<f32> for Vector {\n    type Output = Vector;\n\n    #[inline(always)]\n    fn mul(self, other: f32) -> Vector {\n        Vector {\n            co: self.co * other,\n        }\n    }\n}\n\nimpl Mul<Transform> for Vector {\n    type Output = Vector;\n\n    #[inline]\n    fn mul(self, other: Transform) -> Vector {\n        Vector {\n            co: other.0.transform_vector3a(self.co),\n        }\n    }\n}\n\nimpl Div<f32> for Vector {\n    type Output = Vector;\n\n    #[inline(always)]\n    fn div(self, other: f32) -> Vector {\n        Vector {\n            co: self.co / other,\n        }\n    }\n}\n\nimpl Neg for Vector {\n    type Output = Vector;\n\n    #[inline(always)]\n    fn neg(self) -> Vector {\n        Vector { co: self.co * -1.0 }\n    }\n}\n\nimpl DotProduct for Vector {\n    #[inline(always)]\n    fn dot(self, other: Vector) -> f32 {\n        self.co.dot(other.co)\n    }\n}\n\nimpl CrossProduct for Vector {\n    #[inline]\n    fn cross(self, other: Vector) -> Vector {\n        Vector {\n            co: self.co.cross(other.co),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::super::{CrossProduct, DotProduct, Transform};\n    use super::*;\n\n    #[test]\n    fn add() {\n        let v1 = Vector::new(1.0, 2.0, 3.0);\n        let v2 = Vector::new(1.5, 4.5, 2.5);\n        let v3 = Vector::new(2.5, 6.5, 5.5);\n\n        assert_eq!(v3, v1 + v2);\n    }\n\n    #[test]\n    fn sub() {\n        let v1 = Vector::new(1.0, 2.0, 3.0);\n        let v2 = Vector::new(1.5, 4.5, 2.5);\n        let v3 = Vector::new(-0.5, -2.5, 0.5);\n\n        assert_eq!(v3, v1 - v2);\n    }\n\n    #[test]\n    fn mul_scalar() {\n        let v1 = Vector::new(1.0, 2.0, 3.0);\n        let v2 = 2.0;\n        let v3 = Vector::new(2.0, 4.0, 6.0);\n\n        assert_eq!(v3, v1 * v2);\n    }\n\n    #[test]\n    fn mul_matrix_1() {\n        let v = Vector::new(1.0, 2.5, 4.0);\n        let m = Transform::new_from_values(\n            1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0,\n        );\n        assert_eq!(v * m, Vector::new(14.0, 46.0, 58.0));\n    }\n\n    #[test]\n    fn mul_matrix_2() {\n        let v = Vector::new(1.0, 2.5, 4.0);\n        let m = Transform::new_from_values(\n            1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0,\n        );\n        assert_eq!(v * m, Vector::new(14.0, 46.0, 58.0));\n    }\n\n    #[test]\n    fn div() {\n        let v1 = Vector::new(1.0, 2.0, 3.0);\n        let v2 = 2.0;\n        let v3 = Vector::new(0.5, 1.0, 1.5);\n\n        assert_eq!(v3, v1 / v2);\n    }\n\n    #[test]\n    fn length() {\n        let v = Vector::new(1.0, 2.0, 3.0);\n        assert!((v.length() - 3.7416573867739413).abs() < 0.000001);\n    }\n\n    #[test]\n    fn length2() {\n        let v = Vector::new(1.0, 2.0, 3.0);\n        assert_eq!(v.length2(), 14.0);\n    }\n\n    #[test]\n    fn normalized() {\n        let v1 = Vector::new(1.0, 2.0, 3.0);\n        let v2 = Vector::new(0.2672612419124244, 0.5345224838248488, 0.8017837257372732);\n        let v3 = v1.normalized();\n        assert!((v3.x() - v2.x()).abs() < 0.000001);\n        assert!((v3.y() - v2.y()).abs() < 0.000001);\n        assert!((v3.z() - v2.z()).abs() < 0.000001);\n    }\n\n    #[test]\n    fn dot_test() {\n        let v1 = Vector::new(1.0, 2.0, 3.0);\n        let v2 = Vector::new(1.5, 4.5, 2.5);\n        let v3 = 18.0f32;\n\n        assert_eq!(v3, v1.dot(v2));\n    }\n\n    #[test]\n    fn cross_test() {\n        let v1 = Vector::new(1.0, 0.0, 0.0);\n        let v2 = Vector::new(0.0, 1.0, 0.0);\n        let v3 = Vector::new(0.0, 0.0, 1.0);\n\n        assert_eq!(v3, v1.cross(v2));\n    }\n}\n"
  },
  {
    "path": "sub_crates/spectral_upsampling/Cargo.toml",
    "content": "[package]\nname = \"spectral_upsampling\"\nversion = \"0.1.0\"\nauthors = [\"Nathan Vegdahl <cessen@cessen.com>\"]\nedition = \"2018\"\nlicense = \"MIT, Apache 2.0\"\n\n[lib]\nname = \"spectral_upsampling\"\npath = \"src/lib.rs\"\n\n[dependencies]\nglam = \"0.15\""
  },
  {
    "path": "sub_crates/spectral_upsampling/LICENSE.md",
    "content": "## Adapted code and data files\n\nThis crate includes code adapted from the supplemental material of the paper [\"Physically Meaningful Rendering using Tristimulus Colours\" by Meng et al.](https://cg.ivd.kit.edu/spectrum.php)  Specifically the code in `src/meng/`.  That code has no explicit license, but I contacted one of the authors and confirmed that it is intended to be used freely.  Take that for what you will!\n\nThis crate also includes data files from the supplemental material of the paper [\"A Low-Dimensional Function Space for Efficient Spectral Upsampling\" by Jakob et al.](https://rgl.epfl.ch/publications/Jakob2019Spectral).  Specifically, the files under `jakob_tables/`.  Please see LICENSE.txt in that directory for their license.\n\n\n## Remaining code and files\n\nCopyright (c) 2020 Nathan Vegdahl\n\nThis project is licensed under either of\n\n* MIT license (licenses/MIT.txt or http://opensource.org/licenses/MIT)\n* Apache License, Version 2.0, (licenses/Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0)\n\nat your option.\n"
  },
  {
    "path": "sub_crates/spectral_upsampling/build.rs",
    "content": "// Get Jakob tables into a native rust format.\n\nuse std::{\n    env,\n    fs::File,\n    io::{self, Read, Write},\n    path::Path,\n};\n\n/// How many polynomial coefficients?\nconst RGB2SPEC_N_COEFFS: usize = 3;\n\n/// Table resolution.\nconst TABLE_RES: usize = 64;\n\n// For the small table, what is the middle value used?\nconst MID_VALUE: f32 = 0.5;\n\nfn main() {\n    // Write tables to Rust file\n    let out_dir = env::var(\"OUT_DIR\").unwrap();\n    let dest_path = Path::new(&out_dir).join(\"jakob_table_inc.rs\");\n    let mut f = File::create(&dest_path).unwrap();\n\n    // Rec.709\n    let rec709_table = rgb2spec_load_small(\"jakob_tables/srgb.coeff\");\n    f.write_all(format!(\"\\nconst REC709_TABLE_RES: usize = {};\", TABLE_RES).as_bytes())\n        .unwrap();\n    f.write_all(format!(\"\\nconst REC709_TABLE_MID_VALUE: f32 = {};\", MID_VALUE).as_bytes())\n        .unwrap();\n    f.write_all(\"\\n#[allow(clippy::unreadable_literal, clippy::approx_constant)]\".as_bytes())\n        .unwrap();\n    f.write_all(\"\\npub static REC709_TABLE: &[[(f32, f32, f32); 2]; 64 * 64 * 3] = &[\".as_bytes())\n        .unwrap();\n    for item in &rec709_table {\n        f.write_all(\n            format!(\n                \"\\n    [({}, {}, {}), ({}, {}, {})],\",\n                item[0].0, item[0].1, item[0].2, item[1].0, item[1].1, item[1].2\n            )\n            .as_bytes(),\n        )\n        .unwrap();\n    }\n    f.write_all(\"\\n];\".as_bytes()).unwrap();\n\n    // Rec.2020\n    let rec2020_table = rgb2spec_load_small(\"jakob_tables/rec2020.coeff\");\n    f.write_all(format!(\"\\nconst REC2020_TABLE_RES: usize = {};\", TABLE_RES).as_bytes())\n        .unwrap();\n    f.write_all(format!(\"\\nconst REC2020_TABLE_MID_VALUE: f32 = {};\", MID_VALUE).as_bytes())\n        .unwrap();\n    f.write_all(\"\\n#[allow(clippy::unreadable_literal, clippy::approx_constant)]\".as_bytes())\n        .unwrap();\n    f.write_all(\"\\npub static REC2020_TABLE: &[[(f32, f32, f32); 2]; 64 * 64 * 3] = &[\".as_bytes())\n        .unwrap();\n    for item in &rec2020_table {\n        f.write_all(\n            format!(\n                \"\\n    [({}, {}, {}), ({}, {}, {})],\",\n                item[0].0, item[0].1, item[0].2, item[1].0, item[1].1, item[1].2\n            )\n            .as_bytes(),\n        )\n        .unwrap();\n    }\n    f.write_all(\"\\n];\".as_bytes()).unwrap();\n\n    // sRGB / ACES\n    let aces_table = rgb2spec_load_small(\"jakob_tables/aces2065_1.coeff\");\n    f.write_all(format!(\"\\nconst ACES_TABLE_RES: usize = {};\", TABLE_RES).as_bytes())\n        .unwrap();\n    f.write_all(format!(\"\\nconst ACES_TABLE_MID_VALUE: f32 = {};\", MID_VALUE).as_bytes())\n        .unwrap();\n    f.write_all(\"\\n#[allow(clippy::unreadable_literal, clippy::approx_constant)]\".as_bytes())\n        .unwrap();\n    f.write_all(\"\\npub static ACES_TABLE: &[[(f32, f32, f32); 2]; 64 * 64 * 3] = &[\".as_bytes())\n        .unwrap();\n    for item in &aces_table {\n        f.write_all(\n            format!(\n                \"\\n    [({}, {}, {}), ({}, {}, {})],\",\n                item[0].0, item[0].1, item[0].2, item[1].0, item[1].1, item[1].2\n            )\n            .as_bytes(),\n        )\n        .unwrap();\n    }\n    f.write_all(\"\\n];\".as_bytes()).unwrap();\n}\n\n/// Underlying representation\npub struct RGB2Spec {\n    res: usize,\n    scale: Vec<f32>,\n    data: Vec<[f32; RGB2SPEC_N_COEFFS]>,\n}\n\npub fn rgb2spec_load(filepath: &str) -> RGB2Spec {\n    let file_contents = {\n        let mut file_contents = Vec::new();\n        let mut f = io::BufReader::new(File::open(filepath).unwrap());\n        f.read_to_end(&mut file_contents).unwrap();\n        file_contents\n    };\n\n    // Check the header\n    let header = &file_contents[0..4];\n    if header != \"SPEC\".as_bytes() {\n        panic!(\"Not a spectral table.\");\n    }\n\n    // Get resolution of the table\n    let res = u32::from_le_bytes([\n        file_contents[4],\n        file_contents[5],\n        file_contents[6],\n        file_contents[7],\n    ]) as usize;\n\n    // Calculate sizes\n    let size_scale = res;\n    let size_data = res * res * res * RGB2SPEC_N_COEFFS;\n\n    // Load the table scale data\n    let mut scale = Vec::with_capacity(size_scale);\n    for i in 0..size_scale {\n        let ii = i * 4 + 8;\n        let n = f32::from_bits(u32::from_le_bytes([\n            file_contents[ii],\n            file_contents[ii + 1],\n            file_contents[ii + 2],\n            file_contents[ii + 3],\n        ]));\n        scale.push(n);\n    }\n\n    // Load the table coefficient data\n    let mut data = Vec::with_capacity(size_data);\n    for i in 0..size_data {\n        let ii = i * 4 * RGB2SPEC_N_COEFFS + 8 + (size_scale * 4);\n        let n1 = f32::from_bits(u32::from_le_bytes([\n            file_contents[ii],\n            file_contents[ii + 1],\n            file_contents[ii + 2],\n            file_contents[ii + 3],\n        ]));\n        let n2 = f32::from_bits(u32::from_le_bytes([\n            file_contents[ii + 4],\n            file_contents[ii + 5],\n            file_contents[ii + 6],\n            file_contents[ii + 7],\n        ]));\n        let n3 = f32::from_bits(u32::from_le_bytes([\n            file_contents[ii + 8],\n            file_contents[ii + 9],\n            file_contents[ii + 10],\n            file_contents[ii + 11],\n        ]));\n        data.push([n1, n2, n3]);\n    }\n\n    RGB2Spec {\n        res: res,\n        scale: scale,\n        data: data,\n    }\n}\n\npub fn rgb2spec_load_small(filepath: &str) -> Vec<[(f32, f32, f32); 2]> {\n    let big_table = rgb2spec_load(filepath);\n    assert!(big_table.res == TABLE_RES);\n\n    // Calculate z offsets and such for the mid value.\n    let dz: usize = 1 * big_table.res * big_table.res;\n    let z05_i = rgb2spec_find_interval(&big_table.scale, MID_VALUE);\n    let z05_1: f32 = (MID_VALUE - big_table.scale[z05_i])\n        / (big_table.scale[z05_i + 1] - big_table.scale[z05_i]);\n    let z05_0: f32 = 1.0 - z05_1;\n\n    // Fill in table.\n    let mut table = vec![[(0.0, 0.0, 0.0); 2]; TABLE_RES * TABLE_RES * 3];\n    for i in 0..3 {\n        let offset = i * big_table.res * big_table.res * big_table.res;\n        for j in 0..(big_table.res * big_table.res) {\n            let one_coef = big_table.data[offset + ((TABLE_RES - 1) * dz) + j];\n\n            let mid_coef_0 = big_table.data[offset + (z05_i * dz) + j];\n            let mid_coef_1 = big_table.data[offset + ((z05_i + 1) * dz) + j];\n            let mid_coef = [\n                (mid_coef_0[0] * z05_0) + (mid_coef_1[0] * z05_1),\n                (mid_coef_0[1] * z05_0) + (mid_coef_1[1] * z05_1),\n                (mid_coef_0[2] * z05_0) + (mid_coef_1[2] * z05_1),\n            ];\n\n            table[(i * big_table.res * big_table.res) + j] = [\n                (mid_coef[0], mid_coef[1], mid_coef[2]),\n                (one_coef[0], one_coef[1], one_coef[2]),\n            ];\n        }\n    }\n\n    table\n}\n\nfn rgb2spec_find_interval(values: &[f32], x: f32) -> usize {\n    let last_interval = values.len() - 2;\n    let mut left = 0;\n    let mut size = last_interval;\n\n    while size > 0 {\n        let half = size >> 1;\n        let middle = left + half + 1;\n\n        if values[middle] < x {\n            left = middle;\n            size -= half + 1;\n        } else {\n            size = half;\n        }\n    }\n\n    if left < last_interval {\n        left\n    } else {\n        last_interval\n    }\n}\n"
  },
  {
    "path": "sub_crates/spectral_upsampling/jakob_tables/LICENSE.txt",
    "content": "Copyright (c) 2020 Wenzel Jakob <wenzel.jakob@epfl.ch>, All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors\n   may be used to endorse or promote products derived from this software\n   without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "sub_crates/spectral_upsampling/src/jakob.rs",
    "content": "/// This file implements a lighter alternative version of the Jakob\n/// 2019 spectral upsampling method.  Instead of using the entire 3D\n/// looking table, we use two 2d slices of the table and interpolate\n/// between the evaluated spectral values calculated from those tables.\n///\n/// The provides similar color matching as full Jakob, at the expense of\n/// somewhat lower quality spectrums, and the inability to precalculate\n/// the coefficents for even more efficient evaluation later on.\nuse glam::Vec4;\n\n/// How many polynomial coefficients?\nconst RGB2SPEC_N_COEFFS: usize = 3;\n\n// Include tables generated by the build.rs script\ninclude!(concat!(env!(\"OUT_DIR\"), \"/jakob_table_inc.rs\"));\n\n#[inline]\npub fn rec709_to_spectrum_p4(lambdas: Vec4, rgb: (f32, f32, f32)) -> Vec4 {\n    small_rgb_to_spectrum_p4(\n        REC709_TABLE,\n        REC709_TABLE_RES,\n        REC709_TABLE_MID_VALUE,\n        lambdas,\n        rgb,\n    )\n}\n\n#[inline]\npub fn rec2020_to_spectrum_p4(lambdas: Vec4, rgb: (f32, f32, f32)) -> Vec4 {\n    small_rgb_to_spectrum_p4(\n        REC2020_TABLE,\n        REC2020_TABLE_RES,\n        REC2020_TABLE_MID_VALUE,\n        lambdas,\n        rgb,\n    )\n}\n\n#[inline]\npub fn aces_to_spectrum_p4(lambdas: Vec4, rgb: (f32, f32, f32)) -> Vec4 {\n    small_rgb_to_spectrum_p4(\n        ACES_TABLE,\n        ACES_TABLE_RES,\n        ACES_TABLE_MID_VALUE,\n        lambdas,\n        rgb,\n    )\n}\n\n//===============================================================\n// Core functions, specialized above for specific color spaces.\n\n#[inline(always)]\n#[allow(clippy::many_single_char_names)]\nfn small_rgb_to_spectrum_p4(\n    table: &[[(f32, f32, f32); 2]],\n    table_res: usize,\n    table_mid_value: f32,\n    lambdas: Vec4,\n    rgb: (f32, f32, f32),\n) -> Vec4 {\n    // Determine largest RGB component, and calculate the other two\n    // components scaled for lookups.\n    let (i, max_val, x, y) = if rgb.0 > rgb.1 && rgb.0 > rgb.2 {\n        (0, rgb.0, rgb.1, rgb.2)\n    } else if rgb.1 > rgb.2 {\n        (1, rgb.1, rgb.2, rgb.0)\n    } else {\n        (2, rgb.2, rgb.0, rgb.1)\n    };\n    if max_val == 0.0 {\n        // If max_val is zero, just return zero.  This avoids NaN's from\n        // divide by zero.  This is also correct, since it's black.\n        return Vec4::splat(0.0);\n    }\n    let x = x * 63.0 / max_val;\n    let y = y * 63.0 / max_val;\n\n    // Calculate lookup coordinates.\n    let xi = (x as usize).min(table_res - 2);\n    let yi = (y as usize).min(table_res - 2);\n    let offset = (table_res * table_res * i) + (yi * table_res) + xi;\n    let dx = 1;\n    let dy = table_res;\n\n    // Look up values from table.\n    let a0 = table[offset];\n    let a1 = table[offset + dx];\n    let a2 = table[offset + dy];\n    let a3 = table[offset + dy + dx];\n\n    // Convert to SIMD format for faster interpolation.\n    let a0 = [\n        Vec4::new(a0[0].0, a0[0].1, a0[0].2, 0.0),\n        Vec4::new(a0[1].0, a0[1].1, a0[1].2, 0.0),\n    ];\n    let a1 = [\n        Vec4::new(a1[0].0, a1[0].1, a1[0].2, 0.0),\n        Vec4::new(a1[1].0, a1[1].1, a1[1].2, 0.0),\n    ];\n    let a2 = [\n        Vec4::new(a2[0].0, a2[0].1, a2[0].2, 0.0),\n        Vec4::new(a2[1].0, a2[1].1, a2[1].2, 0.0),\n    ];\n    let a3 = [\n        Vec4::new(a3[0].0, a3[0].1, a3[0].2, 0.0),\n        Vec4::new(a3[1].0, a3[1].1, a3[1].2, 0.0),\n    ];\n\n    // Do interpolation.\n    let x1: f32 = x - xi as f32;\n    let x0: f32 = 1.0 - x1 as f32;\n    let y1: f32 = y - yi as f32;\n    let y0: f32 = 1.0 - y1 as f32;\n    let b0 = [(a0[0] * x0) + (a1[0] * x1), (a0[1] * x0) + (a1[1] * x1)];\n    let b1 = [(a2[0] * x0) + (a3[0] * x1), (a2[1] * x0) + (a3[1] * x1)];\n    let c = [(b0[0] * y0) + (b1[0] * y1), (b0[1] * y0) + (b1[1] * y1)];\n\n    // Evaluate the spectral function and return the result.\n    if max_val <= table_mid_value {\n        rgb2spec_eval_4([c[0][0], c[0][1], c[0][2]], lambdas) * (1.0 / table_mid_value) * max_val\n    } else if max_val < 1.0 {\n        let n = (max_val - table_mid_value) / (1.0 - table_mid_value);\n        let s0 = rgb2spec_eval_4([c[0][0], c[0][1], c[0][2]], lambdas);\n        let s1 = rgb2spec_eval_4([c[1][0], c[1][1], c[1][2]], lambdas);\n        (s0 * (1.0 - n)) + (s1 * n)\n    } else {\n        rgb2spec_eval_4([c[1][0], c[1][1], c[1][2]], lambdas) * max_val\n    }\n}\n\n//============================================================\n// Coefficient -> eval functions\n\n#[inline(always)]\nfn rgb2spec_fma_4(a: Vec4, b: Vec4, c: Vec4) -> Vec4 {\n    (a * b) + c\n}\n\nfn rgb2spec_eval_4(coeff: [f32; RGB2SPEC_N_COEFFS], lambda: Vec4) -> Vec4 {\n    let co0 = Vec4::splat(coeff[0]);\n    let co1 = Vec4::splat(coeff[1]);\n    let co2 = Vec4::splat(coeff[2]);\n\n    let x = rgb2spec_fma_4(rgb2spec_fma_4(co0, lambda, co1), lambda, co2);\n\n    let y = {\n        // TODO: replace this with a SIMD sqrt op.\n        let (x, y, z, w) = rgb2spec_fma_4(x, x, Vec4::splat(1.0)).into();\n        Vec4::new(x.sqrt(), y.sqrt(), z.sqrt(), w.sqrt()).recip()\n    };\n\n    rgb2spec_fma_4(Vec4::splat(0.5) * x, y, Vec4::splat(0.5))\n}\n"
  },
  {
    "path": "sub_crates/spectral_upsampling/src/lib.rs",
    "content": "// Since this is basicallt translated from C, silence a bunch of\n// clippy warnings that stem from the C code.\n#![allow(clippy::needless_return)]\n#![allow(clippy::useless_let_if_seq)]\n#![allow(clippy::cognitive_complexity)]\n\npub mod jakob;\npub mod meng;\n"
  },
  {
    "path": "sub_crates/spectral_upsampling/src/meng/generate_meng_spectra_tables.py",
    "content": "#!/usr/bin/env python3\n\n# This file is originally from the supplemental material of the paper\n# \"Physically Meaningful Rendering using Tristimulus Colours\" by Meng et al.\n# It has been adapted by Nathan Vegdahl to generate Rust instead of C.\n# Only the data tables are generated, and should be put in spectra_tables.rs\n# The executable code lives in lib.rs.\n\nimport numpy as np\nimport scipy\nimport math\nimport time\nimport os\nimport sys\n\ntry:\n    import colour.plotting as clr\n    import colour.recovery as rec\n    import colour\n    have_colour_package = True\nexcept:\n    print(\"Install colour-science using 'sudo pip install colour-science' to get xy grid plots.\")\n    print(\"See http://www.colour-science.org for more information.\")\n    have_colour_package = False\n\n# Looking at the code, it looks like \"Path\" is used unconditionally, so\n# matplotlib is actually just required.  Import unconditionally.\n# --Nathan V\n#try:\n#print(\"Install matplotlib to get plots.\")\nimport matplotlib.pyplot as plt\nfrom matplotlib.path import Path\nhave_matplotlib = True\n#except:\n#    have_matplotlib = False\n\n# ------------------------------------------------------------------------------\n# Color matching functions.\n# Note: The load function assumes a CSV input, where each row\n# has wavelength, x, y, z (in that order).\n# For our paper, we used the truncated set of CMF from 380nm to 780nm, CIE 1931\n# standard colorimetric observer, as recommended in CIE Technical Report\n# Colorimetry, 2004 (ISBN 3901906339). The CMF values can be found n Table T.4. \n# of the technical report.\n#\n# The same values can be obtained by downloading \n# the CIE 1931 2-deg. XYZ CMFS here: http://www.cvrl.org/cmfs.htm.\n# In the table, use values for wavelengths in [380, 780] and round to 6 \n# decimal places.\n# ------------------------------------------------------------------------------\nclass Cmf:\n    cmf = []\n\n    @classmethod\n    def load(cls, filename):\n        cls.cmf = np.loadtxt(filename, delimiter=',')\n        assert(cls.cmf.shape[1] == 4)\n\n    @classmethod\n    def num_bins(cls):\n        return cls.cmf.shape[0]\n\n    @classmethod\n    def bin_size(cls):\n        return cls.cmf[1,0]-cls.cmf[0,0]\n\n    @classmethod\n    def wavelength(cls):\n        return cls.cmf[:,0]\n\n    @classmethod\n    def x_bar(cls):\n        return cls.cmf[:,1]\n\n    @classmethod\n    def y_bar(cls):\n        return cls.cmf[:,2]\n\n    @classmethod\n    def z_bar(cls):\n        return cls.cmf[:,3]\n\n    @classmethod\n    def xyz_from_spectrum(cls, spectrum):\n        '''As CIE instructs, we integrate using simple summation.'''\n        assert(cls.cmf.shape[0] == len(spectrum))\n        d_lambda = cls.wavelength()[1]-cls.wavelength()[0]\n\n        xyz = [0, 0, 0]\n        for x_bar, y_bar, z_bar, s in zip(cls.x_bar(), cls.y_bar(), cls.z_bar(), spectrum):\n            xyz[0] += x_bar * s\n            xyz[1] += y_bar * s\n            xyz[2] += z_bar * s\n\n        return [v * d_lambda for v in xyz]\n\n    @classmethod\n    def xyz_ee_white(cls):\n        ee_white = [1] * cls.cmf.shape[0]\n        return cls.xyz_from_spectrum(ee_white)\n\n# ------------------------------------------------------------------------------\n# Transform between color spaces.\n# ------------------------------------------------------------------------------\nclass Transform:\n    # --------------------------------------------------------------------------\n    # Homogenize/dehomogenize vectors.\n    # --------------------------------------------------------------------------\n    @staticmethod\n    def hom(v2):\n        assert(len(v2) >= 2)\n        return np.matrix([[v2[0]], [v2[1]], [1]])\n\n    @staticmethod\n    def dehom(v3):\n        assert((v3.shape[0] == 3 and v3.shape[1] == 1)\n            or (v3.shape[0] == 1 and v3.shape[1] == 3))\n        v = v3.flatten().tolist()[0]\n        return [v[0]/v[2], v[1]/v[2]]\n    \n\n    # ------------------------------------------------------------------------------\n    # Convert from xyy to xyz and back.\n    # ------------------------------------------------------------------------------\n    @staticmethod\n    def xyz_from_xyy(xyy):\n        return (xyy[0] * xyy[2]/xyy[1], \n                xyy[2], \n                (1-xyy[0]-xyy[1]) * xyy[2]/xyy[1])\n\n\n    @staticmethod\n    def xyy_from_xyz(xyz):\n        s = sum(xyz)\n        return (xyz[0] / s, xyz[1] / s, xyz[1])\n\n    # ------------------------------------------------------------------------------\n    # Convert from srgb to xyz and back.\n    # ------------------------------------------------------------------------------\n    def xyz_from_srgb(srgb):\n        # This matrix is computed by transforming the sRGB primaries into xyz.\n        # Primaries are \n        # red:   xy = 0.64, Y = 0.2126\n        # green: xy = 0.30, Y = 0.7152\n        # blue:  xy = 0.15, Y = 0.0722,\n        # where the luminance values are taken from HDTV Recommendation BT.709\n        # http://www.itu.int/rec/R-REC-BT.709/en\n        M = np.matrix([[ 0.41231515,  0.3576,      0.1805    ]\n                       [ 0.2126    ,  0.7152,      0.0722    ]\n                       [ 0.01932727,  0.1192,      0.95063333]])\n        return np.dot(M, srgb)\n\n    def srgb_from_xyz(xyz):\n        # This is the inverse of the above matrix.\n        M = np.matrix([[ 3.24156456, -1.53766524, -0.49870224],\n                       [-0.96920119,  1.87588535,  0.04155324],\n                       [ 0.05562416, -0.20395525,  1.05685902]])\n        return np.dot(M, xyz)\n\n    # EE-white adapted sRGB (Smits uses this).\n    def xyz_from_ergb(ergb):\n        M = np.matrix([\n            [0.496859,  0.339094,  0.164047],\n            [0.256193,  0.678188,  0.065619],\n            [0.023290,  0.113031,  0.863978]\n        ])\n        return np.dot(M, xyz)\n\n    # ------------------------------------------------------------------------------\n    # Convert from xy to xy* and back.\n    # ------------------------------------------------------------------------------\n    mat_xystar_to_xy = None\n    mat_xy_to_xystar = None\n\n    @classmethod\n    def init_xystar(cls):\n        '''xy* is a color space where the line between blue and red is horizontal.\n           Also, equal-energy white is the origin.\n           xy* depends only on the color matching functions used.'''\n        num_bins = len(Cmf.wavelength())\n\n        # Pure blue.\n        s = [0] * num_bins\n        s[0] = 1\n        xy0 = cls.xyy_from_xyz(Cmf.xyz_from_spectrum(s))\n\n        # Pure red.\n        s = [0] * num_bins\n        s[-1] = 1\n        xy1 = cls.xyy_from_xyz(Cmf.xyz_from_spectrum(s))\n\n        d = np.array(xy1[:2])-np.array(xy0[:2])\n        d /= math.sqrt(np.vdot(d, d))\n\n        # Translation to make ee-white (in xy) the origin.\n        T = np.matrix([[ 1, 0, -1/3],\n                       [ 0, 1, -1/3],\n                       [ 0, 0,    1]])\n        # Rotation to make purple line horizontal.\n        R = np.matrix([[ d[0], d[1], 0],\n                       [-d[1], d[0], 0],\n                       [    0,    0, 1]]) \n\n        cls.mat_xy_to_xystar = np.dot(R, T)\n        cls.mat_xystar_to_xy = cls.mat_xy_to_xystar.getI()\n\n    @classmethod\n    def xystar_from_xy(cls, xy):\n        return cls.dehom(np.dot(cls.mat_xy_to_xystar, cls.hom(xy)))\n\n    @classmethod\n    def xy_from_xystar(cls, xystar):\n        return cls.dehom(np.dot(cls.mat_xystar_to_xy, cls.hom(xystar)))\n\n    # ------------------------------------------------------------------------------\n    # Convert from xy to uv and back.\n    # ------------------------------------------------------------------------------\n    mat_uv_to_xystar = None\n    mat_xystar_to_uv = None\n    mat_uv_to_xy     = None\n    mat_xy_to_uv     = None\n\n    @classmethod\n    def init_uv(cls, xystar_bbox, grid_res):\n        '''uv is derived from xy* by transforming grid points to integer coordinates.\n           uv depends on xy* and the grid used.'''\n\n        # Translate xystar bounding box min to origin.\n        T = np.matrix([[1, 0, -xystar_bbox[0][0]],\n                       [0, 1, -xystar_bbox[0][1]],\n                       [0, 0,                1]])\n\n        # Scale so that one grid cell has unit size.\n        w = xystar_bbox[1][0]-xystar_bbox[0][0]\n        h = xystar_bbox[1][1]-xystar_bbox[0][1]\n        S = np.matrix([[grid_res[0] / w, 0, 0],\n                       [0, grid_res[1] / h, 0],\n                       [0, 0, 1]])\n\n        cls.mat_xystar_to_uv = np.dot(S, T)\n        cls.mat_uv_to_xystar = cls.mat_xystar_to_uv.getI()\n        cls.mat_xy_to_uv     = np.dot(cls.mat_xystar_to_uv, cls.mat_xy_to_xystar)\n        cls.mat_uv_to_xy     = cls.mat_xy_to_uv.getI()\n\n    @classmethod\n    def uv_from_xy(cls, xy):\n        return cls.dehom(np.dot(cls.mat_xy_to_uv, cls.hom(xy)))\n\n    @classmethod\n    def xy_from_uv(cls, uv):\n        return cls.dehom(np.dot(cls.mat_uv_to_xy, cls.hom(uv)))\n\n    @classmethod\n    def uv_from_xystar(cls, xystar):\n        return cls.dehom(np.dot(cls.mat_xystar_to_uv, cls.hom(xystar)))\n\n    @classmethod\n    def xystar_from_uv(cls, uv):\n        return cls.dehom(np.dot(cls.mat_uv_to_xystar, cls.hom(uv)))\n\n# ------------------------------------------------------------------------------\n# Compute functor for all elements of data using a process pool, and call \n# finished with (i, result) afterwards.\n# ------------------------------------------------------------------------------\ndef multiprocess_progress(data, functor, finished, data_size, early_clip=None):\n    from multiprocessing import Process, current_process, Queue\n\n    num_procs = os.cpu_count()-1\n\n    def worker(wnum, input_queue, output_queue):\n        os.sched_setaffinity(0, [wnum])\n        while True:\n            try:\n                idx, value = input_queue.get(block=False)\n                if value == 'STOP':\n                    break\n                output_queue.put((idx, functor(value)))\n            except:\n                pass\n            os.sched_yield()\n\n    task_queue = Queue(2*num_procs)\n    done_queue = Queue(2*num_procs)\n\n    # Launch workers.\n    print('Running {} workers ...'.format(num_procs))\n    processes = []\n    for i in range(num_procs):\n        processes.append(Process(target = worker,\n            args = (i, task_queue, done_queue),\n            name = 'worker {}'.format(i),\n            daemon = True))\n        processes[-1].start()\n\n    # Push input data, and check for output data.\n    num_sent = 0\n    num_done = 0\n    num_clipped = 0\n    iterator = iter(data)\n    perc = 0\n\n    def print_progress(msg=None):\n        msg_str = ''\n        if msg is not None:\n            msg_str = '['+msg+']'\n        print('\\033[2K\\r{} sent, {} done, {} clipped, {} total ({} %) {}'.format(num_sent, \n            num_done, num_clipped, data_size, perc, msg_str), end='')\n\n    while num_done < data_size:\n        print_progress('sending work')\n\n        while num_sent < data_size and not task_queue.full():\n            nextval = next(iterator)\n            clipped = False\n            if early_clip is not None:\n                clipped, clip_result = early_clip(num_sent, nextval)\n                if clipped:\n                    finished(num_sent, clip_result)\n                    num_clipped += 1\n                    num_done += 1\n\n            if not clipped:\n                task_queue.put((num_sent, nextval))\n\n            num_sent += 1\n            os.sched_yield()\n\n        while True:\n            try:\n                i, result = done_queue.get(block=False)\n                finished(i, result)\n                num_done += 1\n                perc = int(num_done / data_size * 100)\n                print_progress('collecting results')\n            except:\n                break;\n            time.sleep(0)\n\n        print_progress()\n        time.sleep(0)\n\n    # Terminate workers.\n    for i in range(num_procs):\n        task_queue.put((-1, 'STOP'))\n\n    for p in processes:\n        p.join()\n\n    print('\\n ... done')\n\n# ------------------------------------------------------------------------------\n# Given a color in XYZ, determine a smooth spectrum that corresponds to that \n# color.\n# ------------------------------------------------------------------------------\ndef find_spectrum(xyz):\n    from scipy.optimize import minimize\n    \n    # As an objective, we use a similar roughness term as Smits did.\n    def objective(S):\n        roughness = 0\n        for i in range(len(S)-1):\n            roughness += (S[i]-S[i+1])**2\n        # Note: We found much better convergence with the square term!\n        # roughness = math.sqrt(roughness)\n        return roughness\n\n    num_bins = Cmf.num_bins()\n    x0       = [1] * num_bins\n    \n    # Constraint: Match XYZ values.\n    cnstr = { \n        'type': 'eq', \n        'fun': lambda s: (np.array(Cmf.xyz_from_spectrum(s))-xyz)\n    }\n\n    # We want positive spectra.\n    bnds = [(0, 1000)] * num_bins\n    \n    res = minimize(objective, x0, method='SLSQP', constraints=cnstr, \n                   bounds=bnds, options={\"maxiter\": 2000, \"ftol\": 1e-10})\n    if not res.success:\n        err_message = 'Error for xyz={} after {} iterations: {}'.format(xyz, res.nit, res.message)\n        return ([0] * num_bins, True, err_message)\n    else:\n        # The result may contain some very tiny negative values due \n        # to numerical issues. Clamp those to 0.\n        return ([max(x, 0) for x in res.x], False, \"\")\n\n\n# ------------------------------------------------------------------------------\n# Get the boundary of the horseshoe as a path in xy*.\n# ------------------------------------------------------------------------------\ndef horseshoe_path():\n    verts = []\n    codes = []\n\n    d_lambda = Cmf.wavelength()[1]-Cmf.wavelength()[0]\n    for x, y, z in zip(Cmf.x_bar(), Cmf.y_bar(), Cmf.z_bar()):\n        xyz    = [x*d_lambda, y*d_lambda, z*d_lambda]\n        xyY    = Transform.xyy_from_xyz(xyz)\n        xystar = Transform.xystar_from_xy(xyY[:2])\n        verts.append(xystar)\n        codes.append(Path.LINETO)\n\n    codes[0] = Path.MOVETO\n    codes.append(Path.CLOSEPOLY)\n\n    vx = [x for (x, y) in verts]\n    vy = [y for (x, y) in verts]\n    bbox = [ (min(vx), min(vy)), (max(vx), max(vy)) ]\n\n    verts.append((0,0))\n    return (Path(verts, codes), bbox)\n\n# ------------------------------------------------------------------------------\n# Grid data structures.\n# ------------------------------------------------------------------------------\n\nclass DataPoint:\n    def __init__(self):\n        self.xystar             = (0, 0)\n        self.uv                 = (0, 0)\n        self.Y                  = 0\n        self.spectrum           = [0]\n        self.M                  = 0\n        self.inside             = False\n        self.equal_energy_white = False\n        self.broken             = False\n\n    def update_uv(self):\n        self.uv = Transform.uv_from_xystar(self.xystar)\n\nclass GridCell:\n    def __init__(self):\n        self.indices   = []\n        self.triangles = []\n        self.inside    = True\n\n# binary search to find intersection\ndef find_intersection(p0, p1, i0, i1, clip_path):\n    delta = p1-p0\n    if np.linalg.norm(delta) < 0.0001:\n        # Points are very close, terminate.\n        # Move new intersection slightly into the gamut.\n        delta *= 0.998\n        if i0:\n            return p1 - delta\n        else:\n            return p0 + delta\n\n    p01 = 0.5 * (p0 + p1)\n    i01 = clip_path.contains_point(p01)\n    if i0 != i01:\n        return find_intersection(p0, p01, i0, i01, clip_path)\n    elif i1 != i01:\n        return find_intersection(p01, p1, i01, i1, clip_path)\n    else:\n        print (\"something wrong here\")\n        return p01\n\ndef clip_edge(d0, d1, clip_path):\n    from operator import xor\n    if not xor(d0.inside, d1.inside):\n        return (False, None)\n\n    p0 = np.array(d0.xystar)\n    p1 = np.array(d1.xystar)\n    p  = find_intersection(p0, p1, d0.inside, d1.inside, clip_path)\n    \n    data_point        = DataPoint()\n    data_point.xystar = p\n    data_point.inside = True\n\n    return (True, data_point)\n\ndef generate_xystar_grid(scale):\n    print(\"Generating clip path ...\")\n    clip_path, bbox = horseshoe_path()\n\n    # We know that xy(1/3, 1/3) = xystar(0, 0) must be a grid point.\n    # subdivide the rectangle between that and the purest red regularly with res.\n    # Note: This can be freely chosen, but we found 6,4 to be a reasonable value.\n    res          = (6, 4)\n    white_xystar = [0, 0]\n    step_x       = abs(white_xystar[0]-bbox[1][0]) / res[0]\n    step_y       = abs(white_xystar[1]-bbox[0][1]) / res[1]\n\n    # Find bbox top left corner so that the whole diagram is contained.\n    add_x = int(math.ceil(abs(white_xystar[0]-bbox[0][0]) / step_x))\n    add_y = int(math.ceil(abs(bbox[1][1]-white_xystar[1]) / step_y))\n\n    # The index of white - we will set this spectrum to equal energy white.\n    white_idx = (add_x, res[1])\n\n    grid_res = (res[0] + add_x, res[1] + add_y)\n    bbox = [\n        # min\n        (white_xystar[0]- step_x * add_x, bbox[0][1]),\n        # max\n        (bbox[1][0], white_xystar[1] + step_y * add_y)\n    ]\n\n    grid        = [GridCell() for i in range(grid_res[0] * grid_res[1])]\n    data_points = []\n\n    # Generate grid points.\n    print(\" Generating grid points in xy* ...\")\n    for (x,y) in [(x,y) for y in range(grid_res[1]+1) for x in range(grid_res[0]+1)]:\n        data_point        = DataPoint()\n        data_point.xystar = (bbox[0][0] + step_x * x, bbox[0][1] + step_y * y)\n\n        if (x, y) == white_idx:\n            # Numerically, we want the white point to be at xy = (1/3, 1/3).\n            delta = np.array(data_point.xystar) - np.array(white_xystar)\n            assert(np.dot(delta, delta) < 1e-7)\n            data_point.equal_energy_white = True\n\n        # Clip on horseshoe.\n        if clip_path.contains_point(data_point.xystar) \\\n            or (x > 0 and y == 0): # Special case for purple line.\n            data_point.inside = True\n\n        new_idx = len(data_points)\n        data_points.append(data_point)\n    \n        # Add new index to this all four adjacent cells.\n        for (cx, cy) in [(x-dx, y-dy) for dy in range(2) for dx in range(2)]:\n            if cx >= 0 and cx < grid_res[0] and cy >= 0 and cy < grid_res[1]:\n                cell = grid[cy * grid_res[0] + cx]\n                cell.indices.append(new_idx)\n                cell.inside = cell.inside and data_point.inside\n\n    # Clip grid cells against horseshoe.\n    print(\" Clipping cells to xy gamut ...\")\n    for (x, y) in [(x, y) for x in range(grid_res[0]) for y in range(grid_res[1])]:\n        cell = grid[y * grid_res[0] + x]\n\n        # No need to clip cells that are completely inside.\n        if cell.inside:\n            continue\n\n        # We clip the two outgoing edges of each point:\n        #\n        # d2\n        #  .\n        # d0 . d1\n        # Note: We assume here that data_points was generated as a regular\n        #       grid in row major order.\n        d0 = data_points[(y+0)*(grid_res[0]+1)+(x+0)]\n        d1 = data_points[(y+0)*(grid_res[0]+1)+(x+1)]\n        d2 = data_points[(y+1)*(grid_res[0]+1)+(x+0)]\n\n        (clipped_h, p_h) = clip_edge(d0, d1, clip_path)\n        if clipped_h:\n            new_idx = len(data_points)\n            data_points.append(p_h)\n            cell.indices.append(new_idx)\n            if y > 0:\n                grid[(y-1) * grid_res[0] + x].indices.append(new_idx)\n\n        (clipped_v, p_v) = clip_edge(d0, d2, clip_path)\n        if clipped_v:\n            new_idx = len(data_points)\n            data_points.append(p_v)\n            cell.indices.append(new_idx)\n            if x > 0:\n                grid[y * grid_res[0] + x - 1].indices.append(new_idx)\n\n    # Compact grid points (throw away points that are not inside).\n    print(\" Compacting grid ...\")\n    new_data_points = []\n    new_indices = []\n    prefix = 0\n    for data_point in data_points:\n        if data_point.inside:\n            new_indices.append(prefix)\n            new_data_points.append(data_point)\n            prefix += 1\n        else:\n            new_indices.append(-1)\n    data_points = new_data_points\n\n    for gridcell in grid:\n        new_cell_indices = []\n        for index in range(len(gridcell.indices)):\n            old_index = gridcell.indices[index]\n            if new_indices[old_index] >= 0:\n                new_cell_indices.append(new_indices[old_index])\n        gridcell.indices = new_cell_indices[:]\n\n    # Scale points down towards white point to avoid singular spectra.\n    for data_point in data_points:\n        data_point.xystar = [v * scale for v in data_point.xystar]\n\n    bbox[0] = [v * scale for v in bbox[0]]\n    bbox[1] = [v * scale for v in bbox[1]]\n\n    return data_points, grid, grid_res, bbox\n\n# Plot the grid.\ndef plot_grid(filename, data_points, grid, bbox_xystar, xystar=True):\n    if not have_matplotlib or not have_colour_package:\n        return\n\n    print(\"Plotting the grid ...\")\n\n    plt.figure()\n    # Draw a nice chromaticity diagram.\n    clr.CIE_1931_chromaticity_diagram_plot(standalone=False)\n    clr.canvas(figure_size=(7,7))\n\n    # Show the sRGB gamut.\n    color_space = clr.get_RGB_colourspace('sRGB')\n    x = color_space.primaries[:,0].tolist()\n    y = color_space.primaries[:,1].tolist()\n    plt.fill(x, y, color='black', label='sRGB', fill=False)\n\n    # Draw crosses into all internal grid cells.\n    # for gridcell in grid:\n    #     if len(gridcell.indices) > 0 and gridcell.inside:\n    #         if xystar:\n    #             pointx = sum([data_points[i].xystar[0] for i in gridcell.indices])\n    #             pointy = sum([data_points[i].xystar[1] for i in gridcell.indices])\n    #             pointx /= len(gridcell.indices)\n    #             pointy /= len(gridcell.indices)\n    #             (pointx, pointy) = Transform.xy_from_xystar((pointx, pointy))\n    #             plt.plot(pointx, pointy, \"x\", color=\"black\")\n    #         else:\n    #             pointx = sum([data_points[i].uv[0] for i in gridcell.indices])\n    #             pointy = sum([data_points[i].uv[1] for i in gridcell.indices])\n    #             pointx /= len(gridcell.indices)\n    #             pointy /= len(gridcell.indices)\n    #             (pointx, pointy) = Transform.xy_from_uv((pointx, pointy))\n    #             plt.plot(pointx, pointy, \"x\", color=\"black\")\n \n    # Draw data points.\n    for i, data_point in enumerate(data_points):\n        if xystar:\n            p = Transform.xy_from_xystar(data_point.xystar)\n        else:\n            p = Transform.xy_from_uv(data_point.uv)\n\n        if data_point.equal_energy_white:\n            plt.plot(p[0], p[1], \"o\", color=\"white\", ms=4)\n        elif data_point.broken:\n            plt.plot(p[0], p[1], \"o\", color=\"red\", ms=4)\n        else:\n            plt.plot(p[0], p[1], \"o\", color=\"green\", ms=4)\n\n        # Show grid point indices, for debugging.\n        # plt.text(p[0]+0.01, p[1]-0.01, '{}'.format(i))\n\n    bp0 = Transform.xy_from_xystar([bbox_xystar[0][0], bbox_xystar[0][1]])\n    bp1 = Transform.xy_from_xystar([bbox_xystar[0][0], bbox_xystar[1][1]])\n    bp2 = Transform.xy_from_xystar([bbox_xystar[1][0], bbox_xystar[1][1]])\n    bp3 = Transform.xy_from_xystar([bbox_xystar[1][0], bbox_xystar[0][1]])\n    plt.plot([bp0[0], bp1[0], bp2[0], bp3[0], bp0[0]],\n             [bp0[1], bp1[1], bp2[1], bp3[1], bp0[1]],\n             label=\"Grid Bounding Box\")\n\n    plt.xlabel('$x$')\n    plt.ylabel('$y$')\n\n    plt.legend()\n    plt.savefig(filename)\n\n# ------------------------------------------------------------------------------\n# Compute spectra for all data points.\n# ------------------------------------------------------------------------------\ndef compute_spectrum(data_point):\n    xy = Transform.xy_from_uv(data_point.uv)\n\n    # Set luminance to y. This means that X+Y+Z = 1, \n    # since y = Y / (X+Y+Z) = y / (X+Y+Z).\n    xyY = [xy[0], xy[1], xy[1]]\n    xyz = Transform.xyz_from_xyy(xyY)\n\n    spectrum = []\n    broken   = False\n\n    if data_point.equal_energy_white:\n        # Since we want X=Y=Z=1/3 (so that X+Y+Z=1), the equal-energy white \n        # spectrum we want is 1/(3 int(x)) for x color matching function.\n        spectrum = [1 / (3 * Cmf.xyz_ee_white()[0])] * Cmf.num_bins()\n    else:\n        spectrum, broken, message = find_spectrum(xyz)\n\n        if broken:\n            print(\"Couldn't find a spectrum for uv=({uv[0]},{uv[1]})\".format(uv=data_point.uv))\n            print(message)\n\n    xyz = Cmf.xyz_from_spectrum(spectrum)\n    sum = xyz[0] + xyz[1] + xyz[2]\n    if sum > 1.01 or sum < 0.99:\n        print('Invalid brightness {} for uv=({uv[0]},{uv[1]})'.format(sum, uv=data_point.uv))\n\n    return (spectrum, broken)\n\n\n# ------------------------------------------------------------------------------\n\ndef compute_spectra(data_points):\n    print('Computing spectra ...')\n\n    def finished(i, result):\n        data_points[i].spectrum = result[0]\n        data_points[i].broken   = result[1]\n\n    multiprocess_progress(data_points, compute_spectrum, finished, len(data_points))\n\n\n# ------------------------------------------------------------------------------\n# Plot some of our fitted spectra.\n# Plot to multiple output files, since there are so many spectra.\n# ------------------------------------------------------------------------------\ndef plot_spectra(data_points):\n    if not have_matplotlib or not have_colour_package:\n        return\n\n    print('Plotting spectra ...')\n    plots_per_file = 15\n\n    #plt.figure(figsize=(12, 16))\n\n    cur_page = -1\n    ax_shape = (17, 4)\n    axes = None\n    for i, data_point in enumerate(data_points):\n        page_size =(ax_shape[0]*ax_shape[1])\n        page = i // page_size\n        if page > cur_page:\n            if cur_page >= 0:\n                plt.savefig('spectra_{}.svg'.format(cur_page))\n            fig, axes = plt.subplots(ax_shape[0], ax_shape[1], figsize=(14, 18))\n            cur_page = page\n\n        j = i % page_size\n        row = j % axes.shape[0]\n        col = j // axes.shape[0]\n        print(row, col)\n\n        if row >= axes.shape[0] or col >= axes.shape[1]:\n            print('cannot plot spectrum', i)\n            continue\n\n        ax = axes[row,col]\n\n        xy = Transform.xy_from_uv(data_point.uv)\n        # take a copy, we're going to normalise it\n        s = data_point.spectrum[:]\n        max_val = 0\n        for j in range(len(s)):\n            if s[j] > max_val:\n                max_val = s[j];\n        if max_val > 0:\n            for j in range(len(s)):\n                s[j] = s[j]/max_val\n        ax.plot(Cmf.wavelength(), s, color='black', lw=2)\n        ax.set_ylim(-0.01, 1.1)\n        ax.set_yticklabels([])\n        ax.set_xticklabels([])\n\n        perc = int((i+1) / len(data_points) * 100)\n        print(' {} / {} ({} %)             \\r'.format((i+1), len(data_points), perc), end='')\n    plt.savefig('spectra_{}.svg'.format(cur_page))\n\n    print('\\n... done')\n\n# ------------------------------------------------------------------------------\n# Write spectral data\n# ------------------------------------------------------------------------------\ndef write_output(data_points, grid, grid_res, filename):\n    print('Write output ...')\n    with open(filename, 'w') as f:\n        lambda_min       = Cmf.wavelength()[0]\n        lambda_max       = Cmf.wavelength()[-1]\n        num_spec_samples = Cmf.num_bins()\n        spec_bin_size    = Cmf.bin_size()\n        \n        f.write('// This file is auto-generated by generate_spectra_tables.py\\n')\n        f.write('#![allow(dead_code)]\\n')\n        f.write('#![cfg_attr(rustfmt, rustfmt_skip)]\\n')\n        f.write('#![allow(clippy::unreadable_literal)]\\n')\n        f.write('#![allow(clippy::excessive_precision)]\\n')\n        f.write('\\n')\n        f.write('/// This is 1 over the integral over either CMF.\\n')\n        f.write('/// Spectra can be mapped so that xyz=(1,1,1) is converted to constant 1 by\\n')\n        f.write('/// dividing by this value. This is important for valid reflectances.\\n')\n        f.write('pub const EQUAL_ENERGY_REFLECTANCE: f32 = {};'.format(1/max(Cmf.xyz_ee_white())));\n        \n        f.write('\\n\\n')\n        f.write('// Basic info on the spectrum grid.\\n')\n        f.write('pub(crate) const SPECTRUM_GRID_WIDTH: i32 = {};\\n'.format(grid_res[0]))\n        f.write('pub(crate) const SPECTRUM_GRID_HEIGHT: i32 = {};\\n'.format(grid_res[1]))\n        f.write('\\n')\n        \n        f.write('// The spectra here have these properties.\\n')\n        f.write('pub const SPECTRUM_SAMPLE_MIN: f32 = {};\\n'.format(lambda_min))\n        f.write('pub const SPECTRUM_SAMPLE_MAX: f32 = {};\\n'.format(lambda_max))\n        f.write('pub(crate) const SPECTRUM_BIN_SIZE: f32 = {};\\n'.format(spec_bin_size))\n        f.write('pub(crate) const SPECTRUM_NUM_SAMPLES: i32 = {};\\n'.format(num_spec_samples))\n        f.write('\\n')\n\n        # Conversion routines xy->xystar and xy->uv and back.\n        f.write('// xy* color space.\\n')\n        f.write('pub(crate) const SPECTRUM_MAT_XY_TO_XYSTAR: [f32; 6] = [\\n')\n        f.write('    {m[0]}, {m[1]}, {m[2]},\\n    {m[3]}, {m[4]}, {m[5]}\\n'\n            .format(m=Transform.mat_xy_to_xystar[:2,:].flatten().tolist()[0]))\n        f.write('];\\n')\n        f.write('pub(crate) const SPECTRUM_MAT_XYSTAR_TO_XY: [f32; 6] = [\\n')\n        f.write('    {m[0]}, {m[1]}, {m[2]},\\n    {m[3]}, {m[4]}, {m[5]}\\n'\n            .format(m=Transform.mat_xystar_to_xy[:2,:].flatten().tolist()[0]))\n        f.write('];\\n')\n        \n        f.write('// uv color space.\\n')\n        f.write('pub(crate) const SPECTRUM_MAT_XY_TO_UV: [f32; 6] = [\\n')\n        f.write('    {m[0]}, {m[1]}, {m[2]},\\n    {m[3]}, {m[4]}, {m[5]}\\n'\n            .format(m=Transform.mat_xy_to_uv[:2,:].flatten().tolist()[0]))\n        f.write('];\\n')\n        f.write('pub(crate) const SPECTRUM_MAT_UV_TO_XY: [f32; 6] = [\\n')\n        f.write('    {m[0]}, {m[1]}, {m[2]},\\n    {m[3]}, {m[4]}, {m[5]}\\n'\n            .format(m=Transform.mat_uv_to_xy[:2,:].flatten().tolist()[0]))\n        f.write('];\\n')\n        \n        f.write('// Grid cells. Laid out in row-major format.\\n')\n        f.write('// num_points = 0 for cells without data points.\\n')\n        f.write('#[derive(Copy, Clone)]\\n')\n        f.write('pub(crate) struct SpectrumGridCell {\\n')\n        f.write('    pub inside: bool,\\n')\n        f.write('    pub num_points: i32,\\n')\n        max_num_idx = 0\n        for c in grid:\n            if len(c.indices) > max_num_idx:\n                max_num_idx = len(c.indices)\n        f.write('    pub idx: [i32; {}],\\n'.format(max_num_idx))\n        f.write('}\\n\\n')\n        \n        # Count grid cells\n        grid_cell_count = 0\n        for (x, y) in [(x,y) for y in range(grid_res[1]) for x in range(grid_res[0])]:\n            grid_cell_count += 1\n        \n        # Write grid cells\n        f.write('pub(crate) const SPECTRUM_GRID: [SpectrumGridCell; {}] = [\\n'.format(grid_cell_count))\n        cell_strings = []\n        for (x, y) in [(x,y) for y in range(grid_res[1]) for x in range(grid_res[0])]:\n            cell = grid[y * grid_res[0] + x]\n            # pad cell indices with -1.\n            padded_indices = cell.indices[:] + [-1] * (max_num_idx-len(cell.indices))\n                    \n            num_inside = len(cell.indices)\n            if num_inside > 0:\n                idx_str = ', '.join(map(str, padded_indices))\n                if cell.inside and num_inside == 4:\n                    cell_strings.append('    SpectrumGridCell {{ inside: true, num_points: {}, idx: [{}] }}'.format(num_inside, idx_str))\n                else:\n                    cell_strings.append('    SpectrumGridCell {{ inside: false, num_points: {}, idx: [{}] }}'.format(num_inside, idx_str))\n            else:\n                cell_strings.append('    SpectrumGridCell {{ inside: false, num_points: 0, idx: [{}] }}'.format(', '.join(['-1'] * max_num_idx)))\n        f.write(',\\n'.join(cell_strings))\n        f.write('\\n];\\n\\n')\n        \n        f.write('// Grid data points.\\n')\n        f.write('#[derive(Copy, Clone)]\\n')\n        f.write('pub(crate) struct SpectrumDataPoint {\\n')\n        f.write('    pub xystar: (f32, f32),\\n')\n        f.write('    pub uv: (f32, f32),\\n')\n        f.write('    pub spectrum: [f32; {}], // X+Y+Z = 1\\n'.format(num_spec_samples))\n        f.write('}\\n\\n')\n        data_point_strings = []\n        data_point_count = 0\n        for p in data_points:\n            data_point_count += 1\n            spec_str = ', '.join([\"{:f}\".format(v) for v in list(p.spectrum)])\n            data_point_strings.append(\n                \"    SpectrumDataPoint {{\\n\"\n                \"        xystar: ({p.xystar[0]}, {p.xystar[1]}),\\n\"\n                \"        uv: ({p.uv[0]}, {p.uv[1]}),\\n\"\n                \"        spectrum: [{spec}],\\n\"\n                \"    }}\".format(p=p, spec=spec_str)\n            )\n        f.write('pub(crate) const SPECTRUM_DATA_POINTS: [SpectrumDataPoint; {}] = [\\n'.format(data_point_count))\n        f.write(',\\n'.join(data_point_strings))\n        f.write('\\n];\\n\\n')\n\n\n        f.write('// Color matching functions.\\n')\n        f.write('pub(crate) const CMF_WAVELENGTH: [f32; {}] = [\\n'.format(len(Cmf.wavelength())))\n        f.write('    {}\\n'.format(', '.join(str(v) for v in Cmf.wavelength())))\n        f.write('];\\n')\n        f.write('pub(crate) const CMF_X: [f32; {}] = [\\n'.format(len(Cmf.x_bar())))\n        f.write('    {}\\n'.format(', '.join(str(v) for v in Cmf.x_bar())))\n        f.write('];\\n')\n        f.write('pub(crate) const CMF_Y: [f32; {}] = [\\n'.format(len(Cmf.y_bar())))\n        f.write('    {}\\n'.format(', '.join(str(v) for v in Cmf.y_bar())))\n        f.write('];\\n')\n        f.write('pub(crate) const CMF_Z: [f32; {}] = [\\n'.format(len(Cmf.z_bar())))\n        f.write('    {}\\n'.format(', '.join(str(v) for v in Cmf.z_bar())))\n        f.write('];\\n\\n')\n    \n    print(' ... done')\n\n# ------------------------------------------------------------------------------\n# We need to triangulate along the spectral locus, since our regular grid\n# cannot properly capture this edge.\n# ------------------------------------------------------------------------------\ndef create_triangle_fans(grid):\n    print(\"generating triangle fans...\")\n    for cell in grid:\n        num_points = len(cell.indices)\n        # skip trivial inner cells (full quad interpolation)\\n\",\n        if len(cell.indices) == 4 and cell.inside:\n            # these could be sorted here, too. but turns out we always get them in scanline order\n            # so we will know exactly how to treat them in the exported c code.\n            continue\n\n        # triangulate hard cases (irregular quads + n-gons, 5-gons in practice)\n        if num_points > 0:\n            # used for delaunay or plotting:\\n\",\n            points = np.array([data_points[cell.indices[i]].xystar for i in range(num_points)])\n            centroid = (sum(points[:,0])/num_points, sum(points[:,1])/num_points)\n            dp = DataPoint()\n            dp.xystar = centroid\n            dp.update_uv()\n            index = len(data_points)\n            data_points.append(dp)\n\n            # create triangle fan:\n            pts = [(points[i], i, cell.indices[i], math.atan2((points[i]-centroid)[1], (points[i]-centroid)[0])) for i in range(num_points)]\n            pts = sorted(pts, key=lambda pt: pt[3])\n            # print('sorted {}'.format([pts[i][2] for i in range(num_points)]))\n            cell.indices = [index] + [pts[i][2] for i in range(num_points)]\n            # print('indices: {}'.format(cell.indices))\n            num_points = num_points + 1;\n            # do that again with the new sort order:\n            # points = np.array([data_points[cell.indices[i]].xystar for i in range(num_points)])\n            # now try triangle fan again with right pivot\n            cell.triangles = [[0, i+1, i+2] for i in range(len(cell.indices)-2)]\n\n# ------------------------------------------------------------------------------\n# Compute a high-resolution reflectance map. This map contains, for all\n# possible values of (xy), the largest value Y for which the corresponding \n# spectrum is still a valid reflectance.\n# ------------------------------------------------------------------------------\ndef compute_max_brightness(point):\n    x           = point[0]\n    y           = point[1]\n\n    try:\n        xyz = Transform.xyz_from_xyy((x, y, y)) # x+y+z = 1\n        spec, broken, msg = find_spectrum(xyz)\n        if broken: \n            print('{},{}: {}'.format(x, y, msg))\n            return -1\n\n        return 1.0/(106.8 * max(spec))\n    except:\n        print('Exception - this is fatal.')\n        raise\n\ndef compute_reflectance_map(res):\n    width      = res\n    height     = res\n    num_pixels = width * height\n    buffer     = [0, 0, 0.1] * num_pixels\n\n    def store_buffer():\n        with open('reflectance_map.pfm', 'wb') as file:\n            import struct\n            header = 'PF\\n{w} {h}\\n{le}\\n'.format(\n                w  = width, \n                h  = height, \n                le = -1 if sys.byteorder == 'little' else 1)\n\n            s = struct.pack('f' * len(buffer), *buffer)\n            file.write(bytes(header, encoding='utf-8'))\n            file.write(s)\n            file.close()\n\n    def coordinates():\n        for y in range(height):\n            for x in range(width):\n                yield (x / width, y / height)\n\n    def store_pixel(i, v):\n        global last_time_stored\n\n        if v == 0:\n            pass\n        elif v < 0:\n            buffer[3*i]   = -v\n            buffer[3*i+1] = 0\n            buffer[3*i+2] = 0\n        else:\n            buffer[3*i]   = v\n            buffer[3*i+1] = v\n            buffer[3*i+2] = v\n\n        now = time.time()\n        if (now-last_time_stored) > 60:\n            store_buffer()\n            last_time_stored = time.time()\n\n    def early_clip(idx, v):\n        global clip_path\n        if clip_path.contains_point(Transform.xystar_from_xy(v)):\n            return (False, 0)\n        return (True, 0)\n\n    multiprocess_progress(coordinates(),\n        compute_max_brightness,\n        store_pixel,\n        width*height, \n        early_clip)\n\n    store_buffer()\n\nif __name__ == \"__main__\":\n    # Parse command line options.\n    import argparse\n    parser = argparse.ArgumentParser(description='Generate spectrum_grid.h')\n    parser.add_argument('-s', '--scale', metavar='SCALE', type=float, default=0.97,\n        dest='scale',\n        help='Scale grid points toward the EE white point using this factor. Defaults to 0.99.')\n\n    parser.add_argument('-p', '--plot_spectra', default=False, action='store_const',\n        const=True, dest='plot',\n        help='Plot all spectra in a set of png files. Instructive, but takes quite a while.')\n\n    parser.add_argument('-r', '--reflectance_map', metavar='RES', type=int, default=0, \n        dest='reflectance_map',\n        help='Generate a high-resolution reflectance map instead of the grid header.')\n\n    parser.add_argument('cmf', metavar='CMF', type=str, help='The cmf file to be used.')\n\n    args = parser.parse_args()\n\n    # Init xystar.\n    Cmf.load(args.cmf)\n    Transform.init_xystar()\n\n    last_time_stored = 0\n    clip_path,_ =  horseshoe_path()\n\n    # plot spectral locus\n    # for i in range(0,Cmf.num_bins()):\n    #    print('{} {} {}'.format(Cmf.wavelength()[i],\n    #             Cmf.x_bar()[i]/(Cmf.x_bar()[i]+Cmf.y_bar()[i]+Cmf.z_bar()[i]),\n    #             Cmf.y_bar()[i]/(Cmf.x_bar()[i]+Cmf.y_bar()[i]+Cmf.z_bar()[i])))\n\n    if args.reflectance_map > 0:\n        compute_reflectance_map(args.reflectance_map)\n\n    else:\n        # Generate the grid.\n        data_points, grid, grid_res, xystar_bbox = generate_xystar_grid(args.scale)\n\n        # Init uv.\n        Transform.init_uv(xystar_bbox, grid_res)\n        for dp in data_points:\n            dp.update_uv()\n\n        create_triangle_fans(grid)\n        # plot_grid('grid.pdf', data_points, grid, xystar_bbox, False)\n\n        # Compute spectra and store in spectrum_data.h\n        compute_spectra(data_points)\n        write_output(data_points, grid, grid_res, \n            #'spectra_{}_{}.rs'.format(os.path.splitext(args.cmf)[0], args.scale))\n            'meng_spectra_tables.rs')\n\n        # Finally, plot all spectra.\n        if args.plot:\n            plot_spectra(data_points)\n\n"
  },
  {
    "path": "sub_crates/spectral_upsampling/src/meng/meng_spectra_tables.rs",
    "content": "// This file is auto-generated by generate_spectra_tables.py\n#![allow(dead_code)]\n#![cfg_attr(rustfmt, rustfmt_skip)]\n#![allow(clippy::unreadable_literal)]\n#![allow(clippy::excessive_precision)]\n\n/// This is 1 over the integral over either CMF.\n/// Spectra can be mapped so that xyz=(1,1,1) is converted to constant 1 by\n/// dividing by this value. This is important for valid reflectances.\npub const EQUAL_ENERGY_REFLECTANCE: f32 = 0.009355121400914532;\n\n// Basic info on the spectrum grid.\npub(crate) const SPECTRUM_GRID_WIDTH: i32 = 12;\npub(crate) const SPECTRUM_GRID_HEIGHT: i32 = 14;\n\n// The spectra here have these properties.\npub const SPECTRUM_SAMPLE_MIN: f32 = 360.0;\npub const SPECTRUM_SAMPLE_MAX: f32 = 830.0;\npub(crate) const SPECTRUM_BIN_SIZE: f32 = 5.0;\npub(crate) const SPECTRUM_NUM_SAMPLES: i32 = 95;\n\n// xy* color space.\npub(crate) const SPECTRUM_MAT_XY_TO_XYSTAR: [f32; 6] = [\n    0.9067484787957371, 0.4216719058718719, -0.44280679488920294,\n    -0.4216719058718719, 0.9067484787957371, -0.1616921909746217\n];\npub(crate) const SPECTRUM_MAT_XYSTAR_TO_XY: [f32; 6] = [\n    0.9067484787957371, -0.4216719058718719, 0.33333333333333326,\n    0.4216719058718719, 0.9067484787957371, 0.3333333333333333\n];\n// uv color space.\npub(crate) const SPECTRUM_MAT_XY_TO_UV: [f32; 6] = [\n    16.730260708356887, 7.7801960340706, -2.170152247475828,\n    -7.530081094743006, 16.192422314095225, 1.1125529268825942\n];\npub(crate) const SPECTRUM_MAT_UV_TO_XY: [f32; 6] = [\n    0.0491440520940413, -0.02361291916573777, 0.13292069743203658,\n    0.022853819546830627, 0.05077639329371236, -0.0068951571224999215\n];\n// Grid cells. Laid out in row-major format.\n// num_points = 0 for cells without data points.\n#[derive(Copy, Clone)]\npub(crate) struct SpectrumGridCell {\n    pub inside: bool,\n    pub num_points: i32,\n    pub idx: [i32; 6],\n}\n\npub(crate) const SPECTRUM_GRID: [SpectrumGridCell; 168] = [\n    SpectrumGridCell { inside: false, num_points: 5, idx: [148, 110, 0, 12, 111, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [0, 1, 12, 13, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [1, 2, 13, 14, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [2, 3, 14, 15, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [3, 4, 15, 16, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [4, 5, 16, 17, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [5, 6, 17, 18, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [6, 7, 18, 19, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [7, 8, 19, 20, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [8, 9, 20, 21, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [9, 10, 21, 22, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [149, 10, 11, 145, 22, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [150, 111, 12, 23, 112, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [12, 13, 23, 24, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [13, 14, 24, 25, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [14, 15, 25, 26, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [15, 16, 26, 27, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [16, 17, 27, 28, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [17, 18, 28, 29, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [18, 19, 29, 30, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [19, 20, 30, 31, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [20, 21, 31, 32, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [21, 22, 32, 33, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [151, 22, 145, 146, 33, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [152, 112, 23, 34, 113, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [23, 24, 34, 35, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [24, 25, 35, 36, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [25, 26, 36, 37, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [26, 27, 37, 38, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [27, 28, 38, 39, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [28, 29, 39, 40, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [29, 30, 40, 41, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [30, 31, 41, 42, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [31, 32, 42, 43, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 6, idx: [153, 32, 33, 147, 141, 43] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [154, 33, 146, 147, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [155, 113, 34, 44, 114, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [34, 35, 44, 45, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [35, 36, 45, 46, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [36, 37, 46, 47, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [37, 38, 47, 48, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [38, 39, 48, 49, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [39, 40, 49, 50, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [40, 41, 50, 51, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [41, 42, 51, 52, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [42, 43, 52, 53, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [156, 43, 141, 142, 53, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [157, 114, 44, 54, 115, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [44, 45, 54, 55, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [45, 46, 55, 56, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [46, 47, 56, 57, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [47, 48, 57, 58, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [48, 49, 58, 59, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [49, 50, 59, 60, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [50, 51, 60, 61, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [51, 52, 61, 62, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [52, 53, 62, 63, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [158, 53, 142, 143, 63, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [159, 115, 54, 116, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 6, idx: [160, 116, 54, 55, 64, 117] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [55, 56, 64, 65, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [56, 57, 65, 66, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [57, 58, 66, 67, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [58, 59, 67, 68, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [59, 60, 68, 69, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [60, 61, 69, 70, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [61, 62, 70, 71, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 6, idx: [161, 62, 63, 144, 138, 71] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [162, 63, 143, 144, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [163, 117, 64, 72, 118, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [64, 65, 72, 73, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [65, 66, 73, 74, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [66, 67, 74, 75, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [67, 68, 75, 76, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [68, 69, 76, 77, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [69, 70, 77, 78, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [70, 71, 78, 79, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [164, 71, 138, 139, 79, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [165, 118, 72, 80, 119, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [72, 73, 80, 81, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [73, 74, 81, 82, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [74, 75, 82, 83, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [75, 76, 83, 84, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [76, 77, 84, 85, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [77, 78, 85, 86, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 6, idx: [166, 78, 79, 140, 134, 86] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [167, 79, 139, 140, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [168, 119, 80, 120, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 6, idx: [169, 80, 81, 87, 121, 120] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [81, 82, 87, 88, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [82, 83, 88, 89, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [83, 84, 89, 90, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [84, 85, 90, 91, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [85, 86, 91, 92, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [170, 86, 134, 135, 92, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [171, 121, 87, 93, 122, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [87, 88, 93, 94, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [88, 89, 94, 95, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [89, 90, 95, 96, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [90, 91, 96, 97, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [91, 92, 97, 98, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [172, 92, 135, 136, 98, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [173, 122, 93, 99, 123, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [93, 94, 99, 100, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [94, 95, 100, 101, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [95, 96, 101, 102, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [96, 97, 102, 103, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 6, idx: [174, 97, 98, 137, 131, 103] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [175, 98, 136, 137, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [176, 123, 99, 124, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 6, idx: [177, 124, 99, 100, 104, 125] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [100, 101, 104, 105, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [101, 102, 105, 106, -1, -1] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [102, 103, 106, 107, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [178, 103, 131, 132, 107, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [179, 125, 104, 126, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 6, idx: [180, 104, 105, 108, 127, 126] },\n    SpectrumGridCell { inside: true, num_points: 4, idx: [105, 106, 108, 109, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 6, idx: [181, 106, 107, 133, 129, 109] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [182, 107, 132, 133, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [183, 127, 108, 128, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 5, idx: [184, 108, 109, 130, 128, -1] },\n    SpectrumGridCell { inside: false, num_points: 4, idx: [185, 109, 129, 130, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] },\n    SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }\n];\n\n// Grid data points.\n#[derive(Copy, Clone)]\npub(crate) struct SpectrumDataPoint {\n    pub xystar: (f32, f32),\n    pub uv: (f32, f32),\n    pub spectrum: [f32; 95], // X+Y+Z = 1\n}\n\npub(crate) const SPECTRUM_DATA_POINTS: [SpectrumDataPoint; 186] = [\n    SpectrumDataPoint {\n        xystar: (-0.27099054061447164, -0.22399328802249138),\n        uv: (0.9999999999999991, 0.0),\n        spectrum: [0.023575, 0.023575, 0.023574, 0.023571, 0.023565, 0.023554, 0.023534, 0.023498, 0.023433, 0.023312, 0.023102, 0.022727, 0.022062, 0.020919, 0.019066, 0.016400, 0.013015, 0.009187, 0.005332, 0.002029, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000067, 0.000577, 0.001244, 0.001906, 0.002492, 0.002971, 0.003344, 0.003630, 0.003844, 0.004001, 0.004113, 0.004195, 0.004253, 0.004292, 0.004320, 0.004341, 0.004355, 0.004364, 0.004372, 0.004377, 0.004380, 0.004382, 0.004384, 0.004384, 0.004385, 0.004386, 0.004386, 0.004386, 0.004386, 0.004386, 0.004386, 0.004387, 0.004386, 0.004386, 0.004386, 0.004386, 0.004386, 0.004385, 0.004385, 0.004385],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.21679243249157729, -0.22399328802249138),\n        uv: (2.0, 0.0),\n        spectrum: [0.023280, 0.023278, 0.023276, 0.023274, 0.023269, 0.023257, 0.023234, 0.023192, 0.023117, 0.022980, 0.022742, 0.022316, 0.021562, 0.020267, 0.018190, 0.015247, 0.011585, 0.007577, 0.003781, 0.000946, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000849, 0.002453, 0.004291, 0.006055, 0.007590, 0.008848, 0.009834, 0.010583, 0.011142, 0.011555, 0.011858, 0.012073, 0.012225, 0.012334, 0.012411, 0.012465, 0.012504, 0.012530, 0.012548, 0.012561, 0.012570, 0.012578, 0.012582, 0.012584, 0.012584, 0.012585, 0.012587, 0.012588, 0.012587, 0.012585, 0.012584, 0.012585, 0.012586, 0.012586, 0.012586, 0.012587, 0.012587, 0.012588, 0.012589, 0.012590, 0.012589],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, -0.22399328802249138),\n        uv: (3.0, 0.0),\n        spectrum: [0.023405, 0.023403, 0.023400, 0.023396, 0.023388, 0.023374, 0.023346, 0.023297, 0.023208, 0.023044, 0.022762, 0.022258, 0.021368, 0.019846, 0.017428, 0.014056, 0.009975, 0.005715, 0.002048, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000048, 0.002031, 0.004876, 0.007897, 0.010699, 0.013091, 0.015029, 0.016537, 0.017676, 0.018522, 0.019145, 0.019599, 0.019922, 0.020152, 0.020314, 0.020429, 0.020510, 0.020567, 0.020608, 0.020637, 0.020657, 0.020671, 0.020680, 0.020685, 0.020690, 0.020694, 0.020697, 0.020700, 0.020701, 0.020701, 0.020702, 0.020702, 0.020702, 0.020703, 0.020702, 0.020702, 0.020701, 0.020702, 0.020702, 0.020703, 0.020704, 0.020704],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, -0.22399328802249138),\n        uv: (4.0, 0.0),\n        spectrum: [0.023957, 0.023957, 0.023954, 0.023950, 0.023940, 0.023922, 0.023888, 0.023825, 0.023711, 0.023506, 0.023153, 0.022523, 0.021414, 0.019529, 0.016578, 0.012563, 0.007906, 0.003431, 0.000288, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002783, 0.006879, 0.011267, 0.015355, 0.018852, 0.021687, 0.023894, 0.025564, 0.026807, 0.027722, 0.028387, 0.028862, 0.029198, 0.029436, 0.029606, 0.029726, 0.029811, 0.029872, 0.029914, 0.029943, 0.029963, 0.029977, 0.029987, 0.029994, 0.029999, 0.030002, 0.030004, 0.030004, 0.030005, 0.030007, 0.030008, 0.030009, 0.030009, 0.030009, 0.030010, 0.030010, 0.030010, 0.030010, 0.030009, 0.030009, 0.030009],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, -0.22399328802249138),\n        uv: (5.0, 0.0),\n        spectrum: [0.024728, 0.024726, 0.024723, 0.024716, 0.024702, 0.024677, 0.024633, 0.024552, 0.024405, 0.024135, 0.023669, 0.022845, 0.021408, 0.018992, 0.015295, 0.010469, 0.005302, 0.001183, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002776, 0.008050, 0.014104, 0.019921, 0.024986, 0.029137, 0.032391, 0.034861, 0.036705, 0.038067, 0.039062, 0.039773, 0.040276, 0.040634, 0.040888, 0.041069, 0.041197, 0.041286, 0.041348, 0.041392, 0.041422, 0.041442, 0.041457, 0.041467, 0.041474, 0.041480, 0.041483, 0.041485, 0.041487, 0.041489, 0.041491, 0.041491, 0.041491, 0.041491, 0.041491, 0.041491, 0.041491, 0.041490, 0.041490, 0.041490, 0.041490],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, -0.22399328802249138),\n        uv: (6.0, 0.0),\n        spectrum: [0.025078, 0.025076, 0.025072, 0.025063, 0.025045, 0.025013, 0.024956, 0.024853, 0.024667, 0.024327, 0.023739, 0.022706, 0.020912, 0.017934, 0.013498, 0.008006, 0.002777, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002130, 0.008537, 0.016516, 0.024438, 0.031454, 0.037262, 0.041845, 0.045339, 0.047956, 0.049893, 0.051309, 0.052324, 0.053043, 0.053554, 0.053918, 0.054177, 0.054359, 0.054487, 0.054577, 0.054640, 0.054684, 0.054714, 0.054736, 0.054750, 0.054761, 0.054768, 0.054773, 0.054777, 0.054779, 0.054780, 0.054781, 0.054782, 0.054783, 0.054783, 0.054783, 0.054784, 0.054784, 0.054784, 0.054784, 0.054783, 0.054784],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, -0.22399328802249138),\n        uv: (7.000000000000001, 0.0),\n        spectrum: [0.024931, 0.024929, 0.024924, 0.024914, 0.024893, 0.024854, 0.024784, 0.024657, 0.024430, 0.024014, 0.023300, 0.022048, 0.019887, 0.016344, 0.011221, 0.005254, 0.000451, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000935, 0.008435, 0.018559, 0.028909, 0.038207, 0.045969, 0.052127, 0.056839, 0.060376, 0.062999, 0.064919, 0.066295, 0.067272, 0.067967, 0.068462, 0.068813, 0.069061, 0.069235, 0.069358, 0.069444, 0.069503, 0.069544, 0.069573, 0.069593, 0.069607, 0.069617, 0.069624, 0.069629, 0.069633, 0.069635, 0.069637, 0.069638, 0.069639, 0.069639, 0.069639, 0.069639, 0.069639, 0.069639, 0.069639, 0.069639, 0.069639],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, -0.22399328802249138),\n        uv: (8.0, 0.0),\n        spectrum: [0.023903, 0.023900, 0.023894, 0.023881, 0.023856, 0.023808, 0.023723, 0.023570, 0.023296, 0.022795, 0.021938, 0.020448, 0.017902, 0.013818, 0.008231, 0.002550, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.007273, 0.019410, 0.032632, 0.044862, 0.055243, 0.063561, 0.069970, 0.074804, 0.078402, 0.081043, 0.082941, 0.084288, 0.085247, 0.085931, 0.086416, 0.086758, 0.087000, 0.087169, 0.087288, 0.087370, 0.087428, 0.087467, 0.087495, 0.087514, 0.087528, 0.087538, 0.087545, 0.087550, 0.087553, 0.087555, 0.087555, 0.087556, 0.087556, 0.087556, 0.087556, 0.087557, 0.087557, 0.087557, 0.087556, 0.087557],\n    },\n    SpectrumDataPoint {\n        xystar: (0.162594324368683, -0.22399328802249138),\n        uv: (9.0, 0.0),\n        spectrum: [0.022866, 0.022863, 0.022855, 0.022838, 0.022807, 0.022748, 0.022645, 0.022458, 0.022124, 0.021516, 0.020478, 0.018688, 0.015669, 0.010949, 0.004930, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005327, 0.019065, 0.035406, 0.051084, 0.064651, 0.075650, 0.084187, 0.090661, 0.095500, 0.099063, 0.101628, 0.103452, 0.104751, 0.105676, 0.106332, 0.106796, 0.107123, 0.107352, 0.107513, 0.107625, 0.107703, 0.107757, 0.107795, 0.107821, 0.107840, 0.107853, 0.107862, 0.107869, 0.107873, 0.107876, 0.107878, 0.107880, 0.107881, 0.107882, 0.107882, 0.107883, 0.107883, 0.107883, 0.107883, 0.107883],\n    },\n    SpectrumDataPoint {\n        xystar: (0.21679243249157734, -0.22399328802249138),\n        uv: (10.0, 0.0),\n        spectrum: [0.019140, 0.019137, 0.019128, 0.019110, 0.019076, 0.019012, 0.018899, 0.018696, 0.018337, 0.017684, 0.016577, 0.014692, 0.011577, 0.006932, 0.001819, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002556, 0.017879, 0.037710, 0.057330, 0.074579, 0.088691, 0.099708, 0.108097, 0.114386, 0.119028, 0.122375, 0.124758, 0.126455, 0.127665, 0.128523, 0.129129, 0.129556, 0.129857, 0.130066, 0.130213, 0.130314, 0.130384, 0.130434, 0.130469, 0.130493, 0.130511, 0.130523, 0.130532, 0.130538, 0.130542, 0.130545, 0.130547, 0.130548, 0.130548, 0.130549, 0.130549, 0.130549, 0.130549, 0.130550, 0.130550],\n    },\n    SpectrumDataPoint {\n        xystar: (0.27099054061447164, -0.22399328802249138),\n        uv: (11.0, 0.0),\n        spectrum: [0.013761, 0.013757, 0.013748, 0.013730, 0.013696, 0.013633, 0.013521, 0.013320, 0.012968, 0.012331, 0.011262, 0.009486, 0.006657, 0.002812, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.016185, 0.039428, 0.063213, 0.084465, 0.102012, 0.115790, 0.126325, 0.134246, 0.140107, 0.144339, 0.147353, 0.149500, 0.151032, 0.152117, 0.152886, 0.153427, 0.153806, 0.154071, 0.154256, 0.154384, 0.154473, 0.154536, 0.154581, 0.154614, 0.154636, 0.154651, 0.154662, 0.154669, 0.154674, 0.154678, 0.154681, 0.154683, 0.154684, 0.154685, 0.154684, 0.154685, 0.154685, 0.154686, 0.154687],\n    },\n    SpectrumDataPoint {\n        xystar: (0.3251886487373659, -0.22399328802249138),\n        uv: (12.0, 0.0),\n        spectrum: [0.003893, 0.003891, 0.003884, 0.003870, 0.003846, 0.003800, 0.003722, 0.003585, 0.003356, 0.002951, 0.002309, 0.001377, 0.000247, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.012060, 0.037710, 0.066528, 0.093367, 0.116030, 0.134066, 0.147987, 0.158525, 0.166362, 0.172040, 0.176091, 0.178980, 0.181040, 0.182501, 0.183534, 0.184262, 0.184772, 0.185130, 0.185380, 0.185554, 0.185675, 0.185759, 0.185819, 0.185861, 0.185890, 0.185911, 0.185926, 0.185936, 0.185943, 0.185948, 0.185951, 0.185953, 0.185955, 0.185956, 0.185957, 0.185958, 0.185958, 0.185958, 0.185958],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.27099054061447164, -0.16799496601686853),\n        uv: (0.9999999999999991, 1.0),\n        spectrum: [0.009112, 0.009112, 0.009112, 0.009112, 0.009112, 0.009111, 0.009111, 0.009109, 0.009107, 0.009103, 0.009095, 0.009081, 0.009054, 0.009003, 0.008917, 0.008781, 0.008589, 0.008337, 0.008025, 0.007656, 0.007231, 0.006756, 0.006238, 0.005686, 0.005112, 0.004527, 0.003941, 0.003362, 0.002797, 0.002260, 0.001759, 0.001304, 0.000905, 0.000572, 0.000311, 0.000128, 0.000024, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000023, 0.000068, 0.000126, 0.000195, 0.000266, 0.000333, 0.000393, 0.000448, 0.000496, 0.000536, 0.000570, 0.000596, 0.000617, 0.000632, 0.000643, 0.000649, 0.000651, 0.000653, 0.000654, 0.000654, 0.000652, 0.000651, 0.000650, 0.000649, 0.000647, 0.000647, 0.000646, 0.000646, 0.000646, 0.000647, 0.000647, 0.000647, 0.000648, 0.000648, 0.000649, 0.000649, 0.000650, 0.000650, 0.000651, 0.000651, 0.000651, 0.000651, 0.000651, 0.000651, 0.000650, 0.000649, 0.000648, 0.000648, 0.000647],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.21679243249157729, -0.16799496601686853),\n        uv: (2.0, 1.0),\n        spectrum: [0.008348, 0.008348, 0.008348, 0.008348, 0.008348, 0.008347, 0.008347, 0.008345, 0.008344, 0.008339, 0.008330, 0.008315, 0.008287, 0.008239, 0.008156, 0.008026, 0.007842, 0.007601, 0.007304, 0.006952, 0.006548, 0.006098, 0.005606, 0.005085, 0.004543, 0.003994, 0.003446, 0.002908, 0.002388, 0.001897, 0.001443, 0.001038, 0.000689, 0.000406, 0.000194, 0.000059, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000003, 0.000057, 0.000155, 0.000290, 0.000449, 0.000624, 0.000807, 0.000989, 0.001163, 0.001323, 0.001466, 0.001591, 0.001696, 0.001783, 0.001854, 0.001910, 0.001954, 0.001987, 0.002011, 0.002029, 0.002042, 0.002051, 0.002057, 0.002062, 0.002065, 0.002066, 0.002067, 0.002067, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002067, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002067, 0.002067, 0.002066, 0.002066, 0.002066, 0.002066, 0.002066, 0.002066],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, -0.16799496601686853),\n        uv: (3.0, 1.0),\n        spectrum: [0.007628, 0.007627, 0.007627, 0.007627, 0.007627, 0.007626, 0.007624, 0.007622, 0.007620, 0.007615, 0.007607, 0.007593, 0.007568, 0.007521, 0.007440, 0.007313, 0.007133, 0.006897, 0.006605, 0.006258, 0.005862, 0.005419, 0.004939, 0.004431, 0.003908, 0.003379, 0.002855, 0.002345, 0.001860, 0.001408, 0.001000, 0.000648, 0.000365, 0.000159, 0.000037, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000035, 0.000136, 0.000294, 0.000499, 0.000739, 0.001004, 0.001280, 0.001557, 0.001826, 0.002078, 0.002309, 0.002513, 0.002688, 0.002837, 0.002960, 0.003059, 0.003137, 0.003198, 0.003244, 0.003279, 0.003304, 0.003322, 0.003334, 0.003343, 0.003349, 0.003353, 0.003356, 0.003358, 0.003359, 0.003359, 0.003359, 0.003360, 0.003359, 0.003359, 0.003359, 0.003359, 0.003359, 0.003359, 0.003360, 0.003361, 0.003361, 0.003362, 0.003362, 0.003363, 0.003363, 0.003363, 0.003363, 0.003362, 0.003361, 0.003361, 0.003360, 0.003360, 0.003359, 0.003359, 0.003358],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, -0.16799496601686853),\n        uv: (4.0, 1.0),\n        spectrum: [0.006988, 0.006988, 0.006987, 0.006987, 0.006986, 0.006984, 0.006984, 0.006982, 0.006981, 0.006975, 0.006966, 0.006948, 0.006918, 0.006864, 0.006777, 0.006643, 0.006455, 0.006208, 0.005906, 0.005552, 0.005149, 0.004706, 0.004227, 0.003722, 0.003208, 0.002695, 0.002195, 0.001715, 0.001271, 0.000871, 0.000532, 0.000265, 0.000083, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000086, 0.000256, 0.000496, 0.000797, 0.001141, 0.001512, 0.001895, 0.002276, 0.002644, 0.002988, 0.003300, 0.003572, 0.003806, 0.004003, 0.004166, 0.004298, 0.004402, 0.004483, 0.004545, 0.004591, 0.004626, 0.004651, 0.004669, 0.004682, 0.004691, 0.004698, 0.004704, 0.004707, 0.004709, 0.004710, 0.004710, 0.004709, 0.004707, 0.004707, 0.004706, 0.004705, 0.004705, 0.004704, 0.004703, 0.004704, 0.004704, 0.004704, 0.004705, 0.004705, 0.004705, 0.004705, 0.004704, 0.004704, 0.004704, 0.004704, 0.004705, 0.004705, 0.004704, 0.004704, 0.004704],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, -0.16799496601686853),\n        uv: (5.0, 1.0),\n        spectrum: [0.006347, 0.006347, 0.006347, 0.006349, 0.006348, 0.006347, 0.006346, 0.006345, 0.006343, 0.006339, 0.006330, 0.006311, 0.006279, 0.006221, 0.006128, 0.005984, 0.005786, 0.005528, 0.005213, 0.004846, 0.004430, 0.003973, 0.003486, 0.002983, 0.002477, 0.001983, 0.001510, 0.001074, 0.000689, 0.000372, 0.000140, 0.000009, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000088, 0.000291, 0.000592, 0.000976, 0.001424, 0.001913, 0.002423, 0.002933, 0.003424, 0.003886, 0.004307, 0.004681, 0.005001, 0.005271, 0.005492, 0.005671, 0.005812, 0.005919, 0.006002, 0.006063, 0.006110, 0.006144, 0.006169, 0.006185, 0.006197, 0.006208, 0.006215, 0.006219, 0.006221, 0.006222, 0.006222, 0.006223, 0.006224, 0.006223, 0.006222, 0.006222, 0.006221, 0.006219, 0.006219, 0.006219, 0.006219, 0.006219, 0.006219, 0.006219, 0.006218, 0.006219, 0.006220, 0.006221, 0.006220, 0.006220, 0.006221, 0.006222, 0.006223, 0.006223, 0.006223],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, -0.16799496601686853),\n        uv: (6.0, 1.0),\n        spectrum: [0.005678, 0.005678, 0.005678, 0.005677, 0.005677, 0.005676, 0.005675, 0.005673, 0.005670, 0.005665, 0.005657, 0.005639, 0.005608, 0.005552, 0.005456, 0.005307, 0.005100, 0.004833, 0.004505, 0.004126, 0.003700, 0.003237, 0.002750, 0.002255, 0.001769, 0.001308, 0.000889, 0.000529, 0.000245, 0.000060, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000012, 0.000199, 0.000537, 0.001000, 0.001560, 0.002186, 0.002850, 0.003522, 0.004177, 0.004796, 0.005361, 0.005864, 0.006297, 0.006663, 0.006964, 0.007207, 0.007399, 0.007548, 0.007660, 0.007744, 0.007805, 0.007849, 0.007881, 0.007904, 0.007921, 0.007932, 0.007940, 0.007945, 0.007949, 0.007951, 0.007952, 0.007953, 0.007953, 0.007953, 0.007954, 0.007954, 0.007955, 0.007956, 0.007956, 0.007956, 0.007957, 0.007956, 0.007956, 0.007956, 0.007954, 0.007953, 0.007952, 0.007952, 0.007952, 0.007952, 0.007952, 0.007952, 0.007952, 0.007953, 0.007953],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, -0.16799496601686853),\n        uv: (7.000000000000001, 1.0),\n        spectrum: [0.005026, 0.005026, 0.005026, 0.005026, 0.005025, 0.005024, 0.005023, 0.005021, 0.005017, 0.005011, 0.005000, 0.004981, 0.004946, 0.004884, 0.004780, 0.004620, 0.004399, 0.004115, 0.003773, 0.003378, 0.002943, 0.002478, 0.001999, 0.001526, 0.001079, 0.000681, 0.000353, 0.000118, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000078, 0.000389, 0.000894, 0.001553, 0.002323, 0.003160, 0.004024, 0.004877, 0.005688, 0.006434, 0.007100, 0.007677, 0.008166, 0.008568, 0.008894, 0.009153, 0.009353, 0.009505, 0.009619, 0.009702, 0.009762, 0.009805, 0.009835, 0.009857, 0.009872, 0.009882, 0.009890, 0.009895, 0.009898, 0.009900, 0.009902, 0.009903, 0.009904, 0.009904, 0.009905, 0.009905, 0.009906, 0.009906, 0.009906, 0.009906, 0.009906, 0.009906, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, -0.16799496601686853),\n        uv: (8.0, 1.0),\n        spectrum: [0.004322, 0.004322, 0.004322, 0.004322, 0.004321, 0.004319, 0.004318, 0.004317, 0.004313, 0.004307, 0.004297, 0.004276, 0.004239, 0.004173, 0.004062, 0.003892, 0.003658, 0.003360, 0.003006, 0.002604, 0.002168, 0.001716, 0.001266, 0.000845, 0.000480, 0.000198, 0.000027, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000191, 0.000679, 0.001407, 0.002314, 0.003337, 0.004418, 0.005501, 0.006544, 0.007513, 0.008383, 0.009143, 0.009786, 0.010319, 0.010750, 0.011093, 0.011358, 0.011560, 0.011711, 0.011822, 0.011902, 0.011960, 0.012001, 0.012031, 0.012052, 0.012067, 0.012076, 0.012083, 0.012088, 0.012091, 0.012093, 0.012095, 0.012096, 0.012097, 0.012097, 0.012098, 0.012099, 0.012099, 0.012099, 0.012099, 0.012098, 0.012098, 0.012098, 0.012097, 0.012096, 0.012095, 0.012094, 0.012094, 0.012094, 0.012094, 0.012094, 0.012095, 0.012095, 0.012096],\n    },\n    SpectrumDataPoint {\n        xystar: (0.162594324368683, -0.16799496601686853),\n        uv: (9.0, 1.0),\n        spectrum: [0.003529, 0.003529, 0.003529, 0.003529, 0.003528, 0.003527, 0.003526, 0.003524, 0.003520, 0.003512, 0.003499, 0.003477, 0.003438, 0.003368, 0.003253, 0.003079, 0.002842, 0.002544, 0.002194, 0.001806, 0.001399, 0.000995, 0.000622, 0.000306, 0.000087, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000385, 0.001133, 0.002157, 0.003372, 0.004692, 0.006043, 0.007359, 0.008592, 0.009707, 0.010684, 0.011515, 0.012204, 0.012764, 0.013209, 0.013555, 0.013819, 0.014017, 0.014163, 0.014269, 0.014344, 0.014398, 0.014437, 0.014465, 0.014483, 0.014496, 0.014505, 0.014511, 0.014515, 0.014518, 0.014520, 0.014521, 0.014521, 0.014522, 0.014523, 0.014524, 0.014523, 0.014523, 0.014523, 0.014524, 0.014524, 0.014524, 0.014524, 0.014524, 0.014524, 0.014524, 0.014524, 0.014524, 0.014523, 0.014523, 0.014523, 0.014523, 0.014523],\n    },\n    SpectrumDataPoint {\n        xystar: (0.21679243249157734, -0.16799496601686853),\n        uv: (10.0, 1.0),\n        spectrum: [0.002618, 0.002618, 0.002617, 0.002618, 0.002618, 0.002618, 0.002617, 0.002614, 0.002609, 0.002601, 0.002589, 0.002567, 0.002527, 0.002457, 0.002341, 0.002168, 0.001939, 0.001655, 0.001333, 0.000992, 0.000655, 0.000354, 0.000123, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000107, 0.000762, 0.001841, 0.003227, 0.004797, 0.006445, 0.008081, 0.009630, 0.011042, 0.012286, 0.013350, 0.014237, 0.014960, 0.015536, 0.015984, 0.016326, 0.016582, 0.016770, 0.016908, 0.017007, 0.017078, 0.017129, 0.017165, 0.017189, 0.017206, 0.017219, 0.017227, 0.017233, 0.017238, 0.017239, 0.017241, 0.017243, 0.017243, 0.017243, 0.017244, 0.017244, 0.017244, 0.017244, 0.017244, 0.017243, 0.017242, 0.017242, 0.017242, 0.017241, 0.017242, 0.017242, 0.017242, 0.017241, 0.017242, 0.017242, 0.017242, 0.017243],\n    },\n    SpectrumDataPoint {\n        xystar: (0.27099054061447164, -0.16799496601686853),\n        uv: (11.0, 1.0),\n        spectrum: [0.001462, 0.001462, 0.001462, 0.001461, 0.001461, 0.001460, 0.001459, 0.001456, 0.001452, 0.001446, 0.001434, 0.001412, 0.001375, 0.001314, 0.001214, 0.001070, 0.000885, 0.000671, 0.000445, 0.000235, 0.000074, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000382, 0.001410, 0.002911, 0.004721, 0.006685, 0.008678, 0.010596, 0.012363, 0.013934, 0.015285, 0.016416, 0.017342, 0.018080, 0.018656, 0.019095, 0.019425, 0.019668, 0.019844, 0.019971, 0.020062, 0.020128, 0.020173, 0.020205, 0.020228, 0.020243, 0.020255, 0.020263, 0.020269, 0.020273, 0.020276, 0.020277, 0.020278, 0.020279, 0.020279, 0.020279, 0.020280, 0.020280, 0.020279, 0.020279, 0.020279, 0.020280, 0.020280, 0.020281, 0.020281, 0.020282, 0.020282, 0.020282, 0.020282, 0.020281, 0.020281, 0.020281],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.27099054061447164, -0.11199664401124569),\n        uv: (0.9999999999999991, 1.9999999999999998),\n        spectrum: [0.007120, 0.007120, 0.007121, 0.007121, 0.007121, 0.007122, 0.007122, 0.007121, 0.007123, 0.007125, 0.007127, 0.007131, 0.007140, 0.007155, 0.007176, 0.007204, 0.007238, 0.007274, 0.007306, 0.007324, 0.007315, 0.007274, 0.007186, 0.007042, 0.006837, 0.006572, 0.006243, 0.005850, 0.005391, 0.004869, 0.004287, 0.003647, 0.002966, 0.002273, 0.001599, 0.000985, 0.000478, 0.000131, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.21679243249157729, -0.11199664401124569),\n        uv: (2.0, 1.9999999999999998),\n        spectrum: [0.007476, 0.007476, 0.007475, 0.007476, 0.007475, 0.007475, 0.007473, 0.007471, 0.007469, 0.007467, 0.007464, 0.007456, 0.007441, 0.007412, 0.007361, 0.007280, 0.007164, 0.007010, 0.006820, 0.006593, 0.006330, 0.006034, 0.005711, 0.005364, 0.005002, 0.004629, 0.004251, 0.003874, 0.003501, 0.003137, 0.002785, 0.002451, 0.002138, 0.001849, 0.001588, 0.001356, 0.001155, 0.000984, 0.000843, 0.000729, 0.000644, 0.000583, 0.000546, 0.000527, 0.000525, 0.000539, 0.000562, 0.000594, 0.000628, 0.000664, 0.000699, 0.000733, 0.000763, 0.000788, 0.000811, 0.000828, 0.000842, 0.000852, 0.000859, 0.000865, 0.000871, 0.000874, 0.000876, 0.000878, 0.000880, 0.000881, 0.000881, 0.000881, 0.000880, 0.000880, 0.000880, 0.000879, 0.000878, 0.000878, 0.000879, 0.000880, 0.000879, 0.000878, 0.000879, 0.000880, 0.000881, 0.000881, 0.000880, 0.000880, 0.000882, 0.000883, 0.000882, 0.000882, 0.000881, 0.000882, 0.000881, 0.000880, 0.000879, 0.000878, 0.000878],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, -0.11199664401124569),\n        uv: (3.0, 1.9999999999999998),\n        spectrum: [0.006771, 0.006771, 0.006770, 0.006769, 0.006769, 0.006768, 0.006767, 0.006766, 0.006765, 0.006763, 0.006759, 0.006750, 0.006734, 0.006703, 0.006651, 0.006568, 0.006453, 0.006302, 0.006116, 0.005894, 0.005639, 0.005354, 0.005044, 0.004715, 0.004373, 0.004022, 0.003670, 0.003321, 0.002978, 0.002648, 0.002332, 0.002036, 0.001764, 0.001524, 0.001316, 0.001145, 0.001010, 0.000910, 0.000847, 0.000817, 0.000819, 0.000849, 0.000903, 0.000976, 0.001064, 0.001163, 0.001267, 0.001372, 0.001473, 0.001568, 0.001656, 0.001733, 0.001799, 0.001855, 0.001900, 0.001937, 0.001965, 0.001989, 0.002007, 0.002021, 0.002032, 0.002041, 0.002047, 0.002050, 0.002053, 0.002055, 0.002057, 0.002058, 0.002058, 0.002059, 0.002058, 0.002058, 0.002057, 0.002057, 0.002056, 0.002055, 0.002054, 0.002054, 0.002054, 0.002054, 0.002054, 0.002054, 0.002054, 0.002053, 0.002052, 0.002051, 0.002049, 0.002048, 0.002047, 0.002047, 0.002047, 0.002048, 0.002048, 0.002049, 0.002048],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, -0.11199664401124569),\n        uv: (4.0, 1.9999999999999998),\n        spectrum: [0.006020, 0.006020, 0.006020, 0.006020, 0.006020, 0.006020, 0.006020, 0.006018, 0.006016, 0.006014, 0.006011, 0.006002, 0.005988, 0.005960, 0.005913, 0.005839, 0.005732, 0.005590, 0.005414, 0.005206, 0.004967, 0.004699, 0.004405, 0.004093, 0.003766, 0.003434, 0.003100, 0.002771, 0.002452, 0.002148, 0.001865, 0.001606, 0.001380, 0.001190, 0.001042, 0.000936, 0.000872, 0.000849, 0.000864, 0.000916, 0.001002, 0.001118, 0.001259, 0.001419, 0.001591, 0.001772, 0.001955, 0.002135, 0.002307, 0.002464, 0.002608, 0.002734, 0.002844, 0.002934, 0.003007, 0.003067, 0.003114, 0.003151, 0.003178, 0.003197, 0.003210, 0.003219, 0.003224, 0.003228, 0.003229, 0.003230, 0.003230, 0.003231, 0.003232, 0.003233, 0.003234, 0.003234, 0.003235, 0.003236, 0.003236, 0.003237, 0.003238, 0.003239, 0.003239, 0.003239, 0.003239, 0.003238, 0.003238, 0.003238, 0.003237, 0.003236, 0.003236, 0.003235, 0.003235, 0.003234, 0.003234, 0.003233, 0.003233, 0.003232, 0.003232],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, -0.11199664401124569),\n        uv: (5.0, 1.9999999999999998),\n        spectrum: [0.005330, 0.005330, 0.005330, 0.005330, 0.005329, 0.005330, 0.005330, 0.005330, 0.005329, 0.005325, 0.005319, 0.005309, 0.005293, 0.005262, 0.005212, 0.005135, 0.005026, 0.004886, 0.004714, 0.004511, 0.004278, 0.004016, 0.003732, 0.003432, 0.003121, 0.002808, 0.002495, 0.002191, 0.001901, 0.001629, 0.001384, 0.001170, 0.000995, 0.000861, 0.000773, 0.000732, 0.000739, 0.000791, 0.000886, 0.001021, 0.001192, 0.001396, 0.001625, 0.001872, 0.002131, 0.002394, 0.002655, 0.002908, 0.003145, 0.003364, 0.003557, 0.003727, 0.003873, 0.003994, 0.004093, 0.004172, 0.004234, 0.004283, 0.004319, 0.004346, 0.004364, 0.004378, 0.004387, 0.004394, 0.004398, 0.004401, 0.004404, 0.004405, 0.004406, 0.004407, 0.004408, 0.004408, 0.004407, 0.004407, 0.004407, 0.004407, 0.004407, 0.004406, 0.004407, 0.004406, 0.004405, 0.004406, 0.004406, 0.004405, 0.004405, 0.004405, 0.004405, 0.004405, 0.004406, 0.004406, 0.004406, 0.004406, 0.004405, 0.004405, 0.004405],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, -0.11199664401124569),\n        uv: (6.0, 1.9999999999999998),\n        spectrum: [0.004609, 0.004609, 0.004609, 0.004609, 0.004608, 0.004607, 0.004607, 0.004606, 0.004605, 0.004603, 0.004599, 0.004591, 0.004575, 0.004546, 0.004498, 0.004425, 0.004321, 0.004186, 0.004018, 0.003818, 0.003592, 0.003339, 0.003066, 0.002780, 0.002486, 0.002190, 0.001901, 0.001623, 0.001362, 0.001123, 0.000915, 0.000743, 0.000614, 0.000531, 0.000501, 0.000525, 0.000601, 0.000729, 0.000904, 0.001122, 0.001380, 0.001670, 0.001987, 0.002322, 0.002666, 0.003012, 0.003351, 0.003678, 0.003982, 0.004259, 0.004508, 0.004723, 0.004908, 0.005061, 0.005186, 0.005286, 0.005367, 0.005427, 0.005472, 0.005506, 0.005530, 0.005547, 0.005558, 0.005567, 0.005574, 0.005579, 0.005582, 0.005585, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005587, 0.005587, 0.005586, 0.005586, 0.005586, 0.005585, 0.005585, 0.005584, 0.005585, 0.005586, 0.005586, 0.005586, 0.005586, 0.005585, 0.005585, 0.005585, 0.005585],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, -0.11199664401124569),\n        uv: (7.000000000000001, 1.9999999999999998),\n        spectrum: [0.003895, 0.003896, 0.003896, 0.003896, 0.003896, 0.003897, 0.003896, 0.003895, 0.003894, 0.003892, 0.003887, 0.003877, 0.003860, 0.003830, 0.003783, 0.003710, 0.003608, 0.003476, 0.003314, 0.003123, 0.002906, 0.002667, 0.002408, 0.002137, 0.001860, 0.001585, 0.001318, 0.001064, 0.000829, 0.000622, 0.000448, 0.000316, 0.000232, 0.000203, 0.000231, 0.000317, 0.000462, 0.000662, 0.000916, 0.001218, 0.001561, 0.001940, 0.002346, 0.002770, 0.003203, 0.003633, 0.004051, 0.004451, 0.004822, 0.005161, 0.005462, 0.005724, 0.005946, 0.006131, 0.006280, 0.006400, 0.006493, 0.006565, 0.006619, 0.006658, 0.006687, 0.006708, 0.006723, 0.006734, 0.006742, 0.006748, 0.006751, 0.006754, 0.006756, 0.006757, 0.006758, 0.006758, 0.006758, 0.006758, 0.006758, 0.006757, 0.006757, 0.006756, 0.006755, 0.006755, 0.006755, 0.006754, 0.006754, 0.006755, 0.006755, 0.006754, 0.006754, 0.006754, 0.006754, 0.006754, 0.006754, 0.006754, 0.006754, 0.006755, 0.006755],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, -0.11199664401124569),\n        uv: (8.0, 1.9999999999999998),\n        spectrum: [0.003179, 0.003179, 0.003180, 0.003180, 0.003180, 0.003180, 0.003179, 0.003178, 0.003176, 0.003172, 0.003166, 0.003156, 0.003139, 0.003109, 0.003061, 0.002990, 0.002891, 0.002762, 0.002605, 0.002422, 0.002215, 0.001988, 0.001743, 0.001490, 0.001237, 0.000987, 0.000750, 0.000531, 0.000339, 0.000181, 0.000066, 0.000002, 0.000000, 0.000000, 0.000030, 0.000135, 0.000310, 0.000556, 0.000867, 0.001237, 0.001660, 0.002124, 0.002623, 0.003143, 0.003674, 0.004202, 0.004716, 0.005207, 0.005663, 0.006078, 0.006447, 0.006766, 0.007037, 0.007261, 0.007445, 0.007591, 0.007706, 0.007795, 0.007863, 0.007914, 0.007952, 0.007979, 0.007999, 0.008013, 0.008023, 0.008029, 0.008032, 0.008035, 0.008037, 0.008038, 0.008039, 0.008039, 0.008039, 0.008038, 0.008037, 0.008036, 0.008035, 0.008034, 0.008034, 0.008034, 0.008034, 0.008034, 0.008035, 0.008036, 0.008036, 0.008035, 0.008035, 0.008034, 0.008034, 0.008033, 0.008033, 0.008033, 0.008033, 0.008034, 0.008034],\n    },\n    SpectrumDataPoint {\n        xystar: (0.162594324368683, -0.11199664401124569),\n        uv: (9.0, 1.9999999999999998),\n        spectrum: [0.002410, 0.002410, 0.002410, 0.002410, 0.002409, 0.002409, 0.002408, 0.002407, 0.002406, 0.002404, 0.002399, 0.002391, 0.002376, 0.002348, 0.002304, 0.002236, 0.002141, 0.002019, 0.001870, 0.001698, 0.001506, 0.001299, 0.001085, 0.000869, 0.000660, 0.000465, 0.000294, 0.000154, 0.000053, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000013, 0.000130, 0.000349, 0.000663, 0.001064, 0.001543, 0.002089, 0.002687, 0.003322, 0.003980, 0.004642, 0.005293, 0.005918, 0.006501, 0.007035, 0.007510, 0.007925, 0.008278, 0.008572, 0.008811, 0.009002, 0.009151, 0.009266, 0.009354, 0.009420, 0.009468, 0.009502, 0.009526, 0.009543, 0.009556, 0.009564, 0.009569, 0.009573, 0.009576, 0.009577, 0.009579, 0.009580, 0.009580, 0.009581, 0.009581, 0.009581, 0.009581, 0.009581, 0.009581, 0.009581, 0.009581, 0.009581, 0.009580, 0.009580, 0.009579, 0.009579, 0.009579, 0.009578, 0.009577, 0.009577, 0.009577, 0.009578, 0.009579, 0.009580, 0.009581],\n    },\n    SpectrumDataPoint {\n        xystar: (0.21679243249157734, -0.11199664401124569),\n        uv: (10.0, 1.9999999999999998),\n        spectrum: [0.001571, 0.001571, 0.001571, 0.001572, 0.001572, 0.001571, 0.001571, 0.001571, 0.001570, 0.001567, 0.001563, 0.001555, 0.001541, 0.001518, 0.001478, 0.001417, 0.001334, 0.001229, 0.001102, 0.000958, 0.000801, 0.000639, 0.000477, 0.000324, 0.000191, 0.000086, 0.000019, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000130, 0.000400, 0.000800, 0.001318, 0.001937, 0.002637, 0.003399, 0.004200, 0.005016, 0.005824, 0.006605, 0.007338, 0.008012, 0.008617, 0.009145, 0.009595, 0.009971, 0.010278, 0.010524, 0.010718, 0.010868, 0.010981, 0.011064, 0.011125, 0.011169, 0.011201, 0.011224, 0.011240, 0.011251, 0.011259, 0.011265, 0.011269, 0.011272, 0.011274, 0.011275, 0.011276, 0.011275, 0.011276, 0.011275, 0.011275, 0.011274, 0.011274, 0.011274, 0.011273, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011273, 0.011273, 0.011273, 0.011273],\n    },\n    SpectrumDataPoint {\n        xystar: (0.27099054061447164, -0.11199664401124569),\n        uv: (11.0, 1.9999999999999998),\n        spectrum: [0.000577, 0.000577, 0.000578, 0.000580, 0.000582, 0.000583, 0.000584, 0.000583, 0.000581, 0.000578, 0.000574, 0.000568, 0.000558, 0.000542, 0.000514, 0.000474, 0.000422, 0.000356, 0.000284, 0.000206, 0.000130, 0.000066, 0.000018, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000152, 0.000495, 0.001009, 0.001672, 0.002460, 0.003344, 0.004295, 0.005282, 0.006273, 0.007241, 0.008158, 0.009005, 0.009766, 0.010434, 0.011005, 0.011483, 0.011874, 0.012188, 0.012435, 0.012624, 0.012766, 0.012871, 0.012948, 0.013004, 0.013044, 0.013074, 0.013096, 0.013112, 0.013123, 0.013131, 0.013136, 0.013141, 0.013143, 0.013142, 0.013141, 0.013140, 0.013139, 0.013140, 0.013139, 0.013141, 0.013141, 0.013141, 0.013142, 0.013141, 0.013142, 0.013142, 0.013142, 0.013142, 0.013142, 0.013141, 0.013142, 0.013143, 0.013143, 0.013144, 0.013143, 0.013143, 0.013143],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.27099054061447164, -0.05599832200562286),\n        uv: (0.9999999999999991, 2.999999999999999),\n        spectrum: [0.000957, 0.000958, 0.000957, 0.000958, 0.000959, 0.000961, 0.000967, 0.000977, 0.000994, 0.001028, 0.001083, 0.001179, 0.001346, 0.001632, 0.002099, 0.002790, 0.003714, 0.004851, 0.006154, 0.007554, 0.008965, 0.010288, 0.011420, 0.012276, 0.012792, 0.012927, 0.012661, 0.011995, 0.010942, 0.009517, 0.007762, 0.005768, 0.003708, 0.001834, 0.000469, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.21679243249157729, -0.05599832200562286),\n        uv: (2.0, 2.999999999999999),\n        spectrum: [0.006672, 0.006673, 0.006672, 0.006673, 0.006672, 0.006672, 0.006671, 0.006671, 0.006670, 0.006669, 0.006667, 0.006663, 0.006655, 0.006640, 0.006614, 0.006575, 0.006517, 0.006441, 0.006347, 0.006233, 0.006100, 0.005949, 0.005781, 0.005597, 0.005399, 0.005190, 0.004970, 0.004743, 0.004509, 0.004269, 0.004021, 0.003768, 0.003509, 0.003246, 0.002982, 0.002716, 0.002451, 0.002187, 0.001926, 0.001672, 0.001427, 0.001194, 0.000975, 0.000771, 0.000588, 0.000425, 0.000288, 0.000177, 0.000092, 0.000033, 0.000002, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, -0.05599832200562286),\n        uv: (3.0, 2.999999999999999),\n        spectrum: [0.006014, 0.006014, 0.006015, 0.006015, 0.006014, 0.006014, 0.006014, 0.006013, 0.006012, 0.006011, 0.006009, 0.006005, 0.005998, 0.005983, 0.005956, 0.005913, 0.005852, 0.005771, 0.005670, 0.005548, 0.005407, 0.005248, 0.005073, 0.004886, 0.004688, 0.004484, 0.004276, 0.004064, 0.003851, 0.003637, 0.003426, 0.003217, 0.003013, 0.002814, 0.002624, 0.002443, 0.002271, 0.002110, 0.001960, 0.001822, 0.001694, 0.001578, 0.001472, 0.001377, 0.001292, 0.001215, 0.001148, 0.001090, 0.001039, 0.000996, 0.000959, 0.000928, 0.000902, 0.000882, 0.000867, 0.000855, 0.000845, 0.000838, 0.000833, 0.000829, 0.000827, 0.000825, 0.000824, 0.000822, 0.000822, 0.000821, 0.000821, 0.000820, 0.000819, 0.000818, 0.000818, 0.000817, 0.000816, 0.000816, 0.000816, 0.000816, 0.000815, 0.000815, 0.000815, 0.000814, 0.000814, 0.000814, 0.000814, 0.000814, 0.000814, 0.000814, 0.000814, 0.000814, 0.000813, 0.000813, 0.000813, 0.000812, 0.000812, 0.000812, 0.000812],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, -0.05599832200562286),\n        uv: (4.0, 2.999999999999999),\n        spectrum: [0.005300, 0.005300, 0.005299, 0.005299, 0.005299, 0.005299, 0.005299, 0.005299, 0.005298, 0.005296, 0.005292, 0.005288, 0.005279, 0.005265, 0.005239, 0.005198, 0.005141, 0.005067, 0.004972, 0.004858, 0.004726, 0.004578, 0.004414, 0.004240, 0.004057, 0.003869, 0.003680, 0.003490, 0.003304, 0.003122, 0.002947, 0.002781, 0.002625, 0.002481, 0.002351, 0.002237, 0.002138, 0.002055, 0.001985, 0.001929, 0.001887, 0.001856, 0.001837, 0.001826, 0.001825, 0.001830, 0.001840, 0.001854, 0.001872, 0.001889, 0.001906, 0.001924, 0.001939, 0.001951, 0.001962, 0.001971, 0.001979, 0.001984, 0.001988, 0.001991, 0.001994, 0.001995, 0.001996, 0.001998, 0.001998, 0.001998, 0.001999, 0.001998, 0.001998, 0.001998, 0.001998, 0.001998, 0.001998, 0.001997, 0.001996, 0.001996, 0.001995, 0.001995, 0.001995, 0.001995, 0.001995, 0.001995, 0.001995, 0.001995, 0.001995, 0.001996, 0.001996, 0.001996, 0.001996, 0.001995, 0.001995, 0.001995, 0.001994, 0.001995, 0.001995],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, -0.05599832200562286),\n        uv: (5.0, 2.999999999999999),\n        spectrum: [0.004560, 0.004561, 0.004561, 0.004561, 0.004561, 0.004560, 0.004562, 0.004562, 0.004561, 0.004560, 0.004557, 0.004553, 0.004544, 0.004530, 0.004507, 0.004470, 0.004418, 0.004350, 0.004264, 0.004163, 0.004046, 0.003914, 0.003769, 0.003614, 0.003454, 0.003288, 0.003121, 0.002955, 0.002794, 0.002643, 0.002502, 0.002372, 0.002255, 0.002158, 0.002082, 0.002026, 0.001993, 0.001981, 0.001986, 0.002012, 0.002054, 0.002112, 0.002183, 0.002263, 0.002351, 0.002443, 0.002536, 0.002628, 0.002714, 0.002795, 0.002867, 0.002930, 0.002986, 0.003032, 0.003070, 0.003101, 0.003123, 0.003141, 0.003154, 0.003164, 0.003171, 0.003175, 0.003177, 0.003178, 0.003177, 0.003177, 0.003177, 0.003178, 0.003178, 0.003179, 0.003179, 0.003179, 0.003179, 0.003180, 0.003180, 0.003181, 0.003181, 0.003180, 0.003181, 0.003180, 0.003180, 0.003179, 0.003178, 0.003178, 0.003178, 0.003178, 0.003179, 0.003179, 0.003178, 0.003178, 0.003178, 0.003178, 0.003178, 0.003178, 0.003178],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, -0.05599832200562286),\n        uv: (6.0, 2.999999999999999),\n        spectrum: [0.003867, 0.003867, 0.003867, 0.003867, 0.003867, 0.003868, 0.003868, 0.003868, 0.003867, 0.003865, 0.003862, 0.003857, 0.003849, 0.003833, 0.003809, 0.003771, 0.003719, 0.003651, 0.003568, 0.003469, 0.003354, 0.003228, 0.003092, 0.002949, 0.002802, 0.002654, 0.002510, 0.002372, 0.002241, 0.002121, 0.002017, 0.001930, 0.001866, 0.001825, 0.001810, 0.001822, 0.001859, 0.001923, 0.002011, 0.002121, 0.002251, 0.002396, 0.002553, 0.002721, 0.002894, 0.003067, 0.003237, 0.003400, 0.003552, 0.003691, 0.003814, 0.003921, 0.004012, 0.004089, 0.004152, 0.004202, 0.004240, 0.004269, 0.004292, 0.004309, 0.004321, 0.004330, 0.004336, 0.004340, 0.004343, 0.004345, 0.004346, 0.004347, 0.004348, 0.004348, 0.004348, 0.004348, 0.004348, 0.004348, 0.004347, 0.004347, 0.004346, 0.004346, 0.004346, 0.004345, 0.004345, 0.004345, 0.004345, 0.004345, 0.004344, 0.004344, 0.004344, 0.004344, 0.004344, 0.004344, 0.004344, 0.004344, 0.004343, 0.004343, 0.004343],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, -0.05599832200562286),\n        uv: (7.000000000000001, 2.999999999999999),\n        spectrum: [0.003146, 0.003146, 0.003147, 0.003147, 0.003147, 0.003148, 0.003148, 0.003148, 0.003149, 0.003150, 0.003147, 0.003144, 0.003137, 0.003124, 0.003101, 0.003065, 0.003016, 0.002951, 0.002870, 0.002774, 0.002667, 0.002549, 0.002423, 0.002292, 0.002160, 0.002031, 0.001910, 0.001799, 0.001700, 0.001616, 0.001550, 0.001506, 0.001490, 0.001502, 0.001547, 0.001622, 0.001726, 0.001863, 0.002027, 0.002218, 0.002432, 0.002664, 0.002911, 0.003166, 0.003424, 0.003681, 0.003930, 0.004168, 0.004388, 0.004589, 0.004767, 0.004921, 0.005053, 0.005162, 0.005250, 0.005320, 0.005374, 0.005415, 0.005445, 0.005467, 0.005484, 0.005496, 0.005505, 0.005512, 0.005517, 0.005520, 0.005524, 0.005525, 0.005526, 0.005527, 0.005527, 0.005528, 0.005528, 0.005527, 0.005527, 0.005527, 0.005527, 0.005526, 0.005525, 0.005525, 0.005523, 0.005523, 0.005522, 0.005522, 0.005523, 0.005522, 0.005522, 0.005522, 0.005522, 0.005522, 0.005523, 0.005522, 0.005522, 0.005522, 0.005522],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, -0.05599832200562286),\n        uv: (8.0, 2.999999999999999),\n        spectrum: [0.002426, 0.002428, 0.002429, 0.002430, 0.002427, 0.002430, 0.002432, 0.002431, 0.002427, 0.002426, 0.002424, 0.002418, 0.002410, 0.002396, 0.002373, 0.002341, 0.002299, 0.002239, 0.002164, 0.002078, 0.001986, 0.001884, 0.001772, 0.001661, 0.001549, 0.001443, 0.001343, 0.001252, 0.001176, 0.001117, 0.001083, 0.001073, 0.001099, 0.001163, 0.001260, 0.001401, 0.001578, 0.001792, 0.002042, 0.002319, 0.002621, 0.002943, 0.003279, 0.003622, 0.003966, 0.004306, 0.004633, 0.004943, 0.005230, 0.005487, 0.005716, 0.005917, 0.006084, 0.006225, 0.006341, 0.006430, 0.006499, 0.006555, 0.006594, 0.006624, 0.006649, 0.006661, 0.006668, 0.006677, 0.006681, 0.006683, 0.006686, 0.006688, 0.006687, 0.006690, 0.006694, 0.006695, 0.006696, 0.006695, 0.006693, 0.006692, 0.006691, 0.006691, 0.006691, 0.006689, 0.006688, 0.006690, 0.006691, 0.006692, 0.006694, 0.006692, 0.006692, 0.006694, 0.006693, 0.006692, 0.006692, 0.006692, 0.006691, 0.006692, 0.006692],\n    },\n    SpectrumDataPoint {\n        xystar: (0.162594324368683, -0.05599832200562286),\n        uv: (9.0, 2.999999999999999),\n        spectrum: [0.001712, 0.001712, 0.001712, 0.001712, 0.001712, 0.001711, 0.001711, 0.001711, 0.001711, 0.001711, 0.001710, 0.001707, 0.001701, 0.001691, 0.001670, 0.001636, 0.001591, 0.001533, 0.001465, 0.001386, 0.001296, 0.001202, 0.001103, 0.001004, 0.000910, 0.000824, 0.000746, 0.000683, 0.000637, 0.000614, 0.000617, 0.000654, 0.000727, 0.000842, 0.001000, 0.001201, 0.001446, 0.001732, 0.002055, 0.002412, 0.002798, 0.003207, 0.003630, 0.004063, 0.004495, 0.004919, 0.005327, 0.005712, 0.006068, 0.006390, 0.006674, 0.006920, 0.007128, 0.007300, 0.007438, 0.007548, 0.007634, 0.007698, 0.007746, 0.007782, 0.007806, 0.007824, 0.007837, 0.007846, 0.007852, 0.007857, 0.007860, 0.007862, 0.007864, 0.007865, 0.007866, 0.007867, 0.007868, 0.007868, 0.007869, 0.007869, 0.007869, 0.007870, 0.007870, 0.007870, 0.007870, 0.007869, 0.007869, 0.007869, 0.007868, 0.007868, 0.007868, 0.007867, 0.007867, 0.007868, 0.007868, 0.007868, 0.007868, 0.007869, 0.007869],\n    },\n    SpectrumDataPoint {\n        xystar: (0.21679243249157734, -0.05599832200562286),\n        uv: (10.0, 2.999999999999999),\n        spectrum: [0.001022, 0.001024, 0.001025, 0.001025, 0.001027, 0.001024, 0.001024, 0.001023, 0.001021, 0.001013, 0.001012, 0.001003, 0.000991, 0.000972, 0.000948, 0.000920, 0.000877, 0.000822, 0.000756, 0.000687, 0.000610, 0.000531, 0.000449, 0.000363, 0.000286, 0.000219, 0.000164, 0.000127, 0.000108, 0.000116, 0.000155, 0.000230, 0.000351, 0.000517, 0.000732, 0.000997, 0.001312, 0.001672, 0.002072, 0.002511, 0.002983, 0.003478, 0.003990, 0.004506, 0.005021, 0.005522, 0.006006, 0.006466, 0.006887, 0.007275, 0.007609, 0.007907, 0.008161, 0.008372, 0.008548, 0.008686, 0.008800, 0.008885, 0.008953, 0.009000, 0.009038, 0.009066, 0.009087, 0.009099, 0.009106, 0.009110, 0.009116, 0.009119, 0.009119, 0.009119, 0.009119, 0.009119, 0.009115, 0.009115, 0.009112, 0.009112, 0.009111, 0.009111, 0.009110, 0.009108, 0.009109, 0.009108, 0.009106, 0.009107, 0.009106, 0.009105, 0.009105, 0.009103, 0.009103, 0.009102, 0.009102, 0.009102, 0.009102, 0.009101, 0.009102],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.27099054061447164, 0.0),\n        uv: (0.9999999999999991, 3.9999999999999996),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000071, 0.001598, 0.004288, 0.007769, 0.011604, 0.015331, 0.018511, 0.020803, 0.021982, 0.021926, 0.020638, 0.018184, 0.014704, 0.010461, 0.005968, 0.002079, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.21679243249157729, 0.0),\n        uv: (2.0, 3.9999999999999996),\n        spectrum: [0.004842, 0.004842, 0.004843, 0.004843, 0.004843, 0.004844, 0.004845, 0.004846, 0.004849, 0.004852, 0.004859, 0.004872, 0.004893, 0.004933, 0.005000, 0.005100, 0.005236, 0.005408, 0.005613, 0.005842, 0.006084, 0.006328, 0.006566, 0.006782, 0.006964, 0.007102, 0.007186, 0.007210, 0.007163, 0.007035, 0.006815, 0.006496, 0.006070, 0.005541, 0.004920, 0.004228, 0.003491, 0.002736, 0.001995, 0.001306, 0.000715, 0.000268, 0.000013, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, 0.0),\n        uv: (3.0, 3.9999999999999996),\n        spectrum: [0.005225, 0.005225, 0.005225, 0.005225, 0.005226, 0.005227, 0.005226, 0.005227, 0.005227, 0.005228, 0.005227, 0.005228, 0.005227, 0.005228, 0.005226, 0.005224, 0.005220, 0.005213, 0.005204, 0.005192, 0.005177, 0.005157, 0.005132, 0.005099, 0.005060, 0.005012, 0.004957, 0.004893, 0.004816, 0.004725, 0.004620, 0.004496, 0.004351, 0.004188, 0.004001, 0.003794, 0.003567, 0.003321, 0.003060, 0.002789, 0.002510, 0.002226, 0.001942, 0.001663, 0.001394, 0.001140, 0.000904, 0.000690, 0.000503, 0.000347, 0.000219, 0.000123, 0.000055, 0.000015, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, 0.0),\n        uv: (4.0, 3.9999999999999996),\n        spectrum: [0.004583, 0.004582, 0.004582, 0.004582, 0.004582, 0.004582, 0.004582, 0.004582, 0.004582, 0.004581, 0.004578, 0.004574, 0.004571, 0.004565, 0.004558, 0.004550, 0.004541, 0.004530, 0.004517, 0.004500, 0.004481, 0.004460, 0.004436, 0.004407, 0.004372, 0.004331, 0.004286, 0.004235, 0.004178, 0.004115, 0.004043, 0.003963, 0.003873, 0.003770, 0.003657, 0.003534, 0.003397, 0.003251, 0.003094, 0.002929, 0.002759, 0.002584, 0.002408, 0.002232, 0.002059, 0.001892, 0.001732, 0.001582, 0.001445, 0.001321, 0.001212, 0.001117, 0.001038, 0.000973, 0.000920, 0.000877, 0.000844, 0.000818, 0.000798, 0.000783, 0.000773, 0.000765, 0.000760, 0.000757, 0.000755, 0.000754, 0.000752, 0.000751, 0.000750, 0.000749, 0.000748, 0.000747, 0.000746, 0.000745, 0.000745, 0.000744, 0.000743, 0.000742, 0.000741, 0.000740, 0.000739, 0.000739, 0.000739, 0.000739, 0.000739, 0.000741, 0.000741, 0.000741, 0.000742, 0.000742, 0.000741, 0.000741, 0.000740, 0.000740, 0.000740],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.0),\n        uv: (5.0, 3.9999999999999996),\n        spectrum: [0.003833, 0.003833, 0.003833, 0.003833, 0.003834, 0.003834, 0.003835, 0.003835, 0.003835, 0.003835, 0.003835, 0.003835, 0.003835, 0.003834, 0.003832, 0.003829, 0.003825, 0.003821, 0.003816, 0.003810, 0.003803, 0.003794, 0.003783, 0.003769, 0.003752, 0.003734, 0.003713, 0.003689, 0.003661, 0.003628, 0.003592, 0.003551, 0.003504, 0.003451, 0.003391, 0.003326, 0.003255, 0.003180, 0.003100, 0.003017, 0.002931, 0.002843, 0.002756, 0.002669, 0.002584, 0.002502, 0.002424, 0.002351, 0.002284, 0.002225, 0.002171, 0.002125, 0.002086, 0.002053, 0.002026, 0.002004, 0.001987, 0.001973, 0.001962, 0.001955, 0.001949, 0.001945, 0.001942, 0.001940, 0.001938, 0.001936, 0.001935, 0.001934, 0.001933, 0.001932, 0.001932, 0.001931, 0.001931, 0.001930, 0.001930, 0.001929, 0.001929, 0.001928, 0.001928, 0.001927, 0.001926, 0.001926, 0.001926, 0.001926, 0.001926, 0.001925, 0.001925, 0.001925, 0.001926, 0.001926, 0.001926, 0.001926, 0.001926, 0.001926, 0.001926],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.0),\n        uv: (6.0, 3.9999999999999996),\n        spectrum: [0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, 0.0),\n        uv: (7.000000000000001, 3.9999999999999996),\n        spectrum: [0.002415, 0.002415, 0.002415, 0.002415, 0.002414, 0.002413, 0.002413, 0.002413, 0.002413, 0.002413, 0.002413, 0.002414, 0.002413, 0.002411, 0.002410, 0.002408, 0.002410, 0.002411, 0.002414, 0.002420, 0.002428, 0.002438, 0.002451, 0.002468, 0.002487, 0.002508, 0.002531, 0.002558, 0.002587, 0.002620, 0.002657, 0.002696, 0.002740, 0.002790, 0.002847, 0.002910, 0.002979, 0.003053, 0.003133, 0.003217, 0.003303, 0.003392, 0.003480, 0.003569, 0.003656, 0.003739, 0.003818, 0.003891, 0.003958, 0.004020, 0.004073, 0.004119, 0.004157, 0.004189, 0.004213, 0.004232, 0.004247, 0.004259, 0.004267, 0.004273, 0.004277, 0.004279, 0.004281, 0.004282, 0.004282, 0.004283, 0.004283, 0.004283, 0.004284, 0.004284, 0.004284, 0.004284, 0.004285, 0.004284, 0.004284, 0.004284, 0.004283, 0.004283, 0.004282, 0.004282, 0.004281, 0.004280, 0.004280, 0.004279, 0.004279, 0.004279, 0.004278, 0.004278, 0.004278, 0.004278, 0.004277, 0.004277, 0.004277, 0.004277, 0.004277],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, 0.0),\n        uv: (8.0, 3.9999999999999996),\n        spectrum: [0.001684, 0.001684, 0.001683, 0.001684, 0.001684, 0.001684, 0.001685, 0.001685, 0.001684, 0.001683, 0.001684, 0.001684, 0.001685, 0.001686, 0.001688, 0.001693, 0.001699, 0.001708, 0.001719, 0.001732, 0.001749, 0.001769, 0.001794, 0.001823, 0.001857, 0.001896, 0.001941, 0.001990, 0.002046, 0.002109, 0.002181, 0.002263, 0.002354, 0.002459, 0.002575, 0.002704, 0.002844, 0.002994, 0.003152, 0.003319, 0.003491, 0.003666, 0.003843, 0.004018, 0.004190, 0.004356, 0.004514, 0.004662, 0.004797, 0.004919, 0.005025, 0.005115, 0.005191, 0.005254, 0.005304, 0.005345, 0.005376, 0.005399, 0.005418, 0.005432, 0.005443, 0.005451, 0.005456, 0.005460, 0.005463, 0.005464, 0.005465, 0.005465, 0.005465, 0.005466, 0.005465, 0.005465, 0.005464, 0.005464, 0.005464, 0.005463, 0.005462, 0.005462, 0.005462, 0.005461, 0.005461, 0.005461, 0.005461, 0.005461, 0.005460, 0.005459, 0.005459, 0.005458, 0.005458, 0.005457, 0.005457, 0.005457, 0.005457, 0.005457, 0.005457],\n    },\n    SpectrumDataPoint {\n        xystar: (0.162594324368683, 0.0),\n        uv: (9.0, 3.9999999999999996),\n        spectrum: [0.000960, 0.000959, 0.000960, 0.000960, 0.000960, 0.000961, 0.000961, 0.000961, 0.000961, 0.000959, 0.000959, 0.000960, 0.000962, 0.000965, 0.000970, 0.000978, 0.000989, 0.001002, 0.001021, 0.001043, 0.001069, 0.001099, 0.001135, 0.001177, 0.001227, 0.001284, 0.001350, 0.001425, 0.001509, 0.001603, 0.001712, 0.001835, 0.001975, 0.002129, 0.002303, 0.002494, 0.002703, 0.002927, 0.003166, 0.003415, 0.003675, 0.003940, 0.004206, 0.004471, 0.004730, 0.004979, 0.005215, 0.005435, 0.005636, 0.005815, 0.005973, 0.006110, 0.006224, 0.006320, 0.006397, 0.006458, 0.006505, 0.006542, 0.006571, 0.006591, 0.006606, 0.006616, 0.006624, 0.006630, 0.006634, 0.006636, 0.006638, 0.006639, 0.006639, 0.006640, 0.006640, 0.006640, 0.006639, 0.006639, 0.006639, 0.006638, 0.006637, 0.006637, 0.006637, 0.006637, 0.006637, 0.006637, 0.006637, 0.006637, 0.006636, 0.006635, 0.006636, 0.006635, 0.006635, 0.006634, 0.006634, 0.006634, 0.006635, 0.006635, 0.006636],\n    },\n    SpectrumDataPoint {\n        xystar: (0.21679243249157734, 0.0),\n        uv: (10.0, 3.9999999999999996),\n        spectrum: [0.000240, 0.000240, 0.000241, 0.000241, 0.000242, 0.000244, 0.000246, 0.000248, 0.000249, 0.000250, 0.000249, 0.000251, 0.000254, 0.000256, 0.000259, 0.000265, 0.000278, 0.000296, 0.000317, 0.000345, 0.000380, 0.000422, 0.000472, 0.000531, 0.000599, 0.000676, 0.000763, 0.000862, 0.000973, 0.001099, 0.001241, 0.001404, 0.001589, 0.001798, 0.002031, 0.002287, 0.002566, 0.002866, 0.003184, 0.003517, 0.003862, 0.004212, 0.004566, 0.004918, 0.005263, 0.005595, 0.005911, 0.006205, 0.006475, 0.006717, 0.006929, 0.007112, 0.007265, 0.007391, 0.007493, 0.007574, 0.007635, 0.007681, 0.007714, 0.007737, 0.007753, 0.007763, 0.007771, 0.007777, 0.007782, 0.007786, 0.007791, 0.007795, 0.007796, 0.007798, 0.007800, 0.007801, 0.007800, 0.007799, 0.007797, 0.007796, 0.007796, 0.007797, 0.007797, 0.007798, 0.007798, 0.007798, 0.007798, 0.007798, 0.007799, 0.007800, 0.007800, 0.007801, 0.007802, 0.007802, 0.007802, 0.007803, 0.007803, 0.007803, 0.007804],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.27099054061447164, 0.05599832200562286),\n        uv: (0.9999999999999991, 5.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001414, 0.010027, 0.021396, 0.031903, 0.038994, 0.041104, 0.037951, 0.030090, 0.018983, 0.007336, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.21679243249157729, 0.05599832200562286),\n        uv: (2.0, 5.0),\n        spectrum: [0.001605, 0.001606, 0.001606, 0.001607, 0.001608, 0.001609, 0.001613, 0.001620, 0.001628, 0.001642, 0.001666, 0.001712, 0.001799, 0.001953, 0.002210, 0.002600, 0.003134, 0.003811, 0.004615, 0.005517, 0.006485, 0.007476, 0.008442, 0.009335, 0.010115, 0.010748, 0.011205, 0.011458, 0.011481, 0.011258, 0.010760, 0.009968, 0.008898, 0.007585, 0.006092, 0.004523, 0.002996, 0.001636, 0.000583, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000002, 0.000000, 0.000000, 0.000001, 0.000000, 0.000002, 0.000002, 0.000002, 0.000003, 0.000002, 0.000004, 0.000003],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, 0.05599832200562286),\n        uv: (3.0, 5.0),\n        spectrum: [0.003983, 0.003984, 0.003984, 0.003985, 0.003986, 0.003987, 0.003987, 0.003988, 0.003989, 0.003994, 0.004001, 0.004010, 0.004027, 0.004058, 0.004111, 0.004191, 0.004301, 0.004443, 0.004613, 0.004807, 0.005022, 0.005251, 0.005489, 0.005724, 0.005948, 0.006154, 0.006332, 0.006478, 0.006582, 0.006637, 0.006630, 0.006552, 0.006395, 0.006157, 0.005836, 0.005438, 0.004971, 0.004446, 0.003877, 0.003279, 0.002672, 0.002074, 0.001508, 0.000999, 0.000572, 0.000249, 0.000052, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, 0.05599832200562286),\n        uv: (4.0, 5.0),\n        spectrum: [0.003745, 0.003746, 0.003746, 0.003746, 0.003746, 0.003746, 0.003747, 0.003747, 0.003748, 0.003750, 0.003754, 0.003759, 0.003769, 0.003784, 0.003811, 0.003850, 0.003904, 0.003974, 0.004058, 0.004154, 0.004263, 0.004380, 0.004504, 0.004630, 0.004754, 0.004872, 0.004981, 0.005077, 0.005157, 0.005216, 0.005250, 0.005252, 0.005219, 0.005147, 0.005033, 0.004880, 0.004685, 0.004454, 0.004186, 0.003890, 0.003568, 0.003227, 0.002874, 0.002515, 0.002159, 0.001814, 0.001486, 0.001184, 0.000912, 0.000674, 0.000473, 0.000311, 0.000185, 0.000095, 0.000037, 0.000007, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.05599832200562286),\n        uv: (5.0, 5.0),\n        spectrum: [0.003086, 0.003086, 0.003086, 0.003086, 0.003085, 0.003085, 0.003085, 0.003086, 0.003087, 0.003088, 0.003091, 0.003096, 0.003103, 0.003118, 0.003141, 0.003176, 0.003226, 0.003290, 0.003368, 0.003461, 0.003566, 0.003682, 0.003807, 0.003937, 0.004068, 0.004197, 0.004321, 0.004437, 0.004541, 0.004630, 0.004699, 0.004743, 0.004760, 0.004749, 0.004702, 0.004625, 0.004515, 0.004374, 0.004206, 0.004012, 0.003797, 0.003565, 0.003320, 0.003066, 0.002809, 0.002555, 0.002308, 0.002074, 0.001856, 0.001657, 0.001480, 0.001325, 0.001194, 0.001085, 0.000993, 0.000920, 0.000863, 0.000819, 0.000785, 0.000760, 0.000741, 0.000728, 0.000718, 0.000710, 0.000704, 0.000700, 0.000696, 0.000694, 0.000693, 0.000690, 0.000689, 0.000688, 0.000688, 0.000688, 0.000688, 0.000688, 0.000687, 0.000686, 0.000686, 0.000686, 0.000686, 0.000686, 0.000686, 0.000686, 0.000685, 0.000685, 0.000685, 0.000685, 0.000684, 0.000684, 0.000684, 0.000684, 0.000684, 0.000684, 0.000684],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.05599832200562286),\n        uv: (6.0, 5.0),\n        spectrum: [0.002356, 0.002354, 0.002355, 0.002355, 0.002355, 0.002354, 0.002354, 0.002354, 0.002356, 0.002358, 0.002363, 0.002370, 0.002382, 0.002399, 0.002428, 0.002469, 0.002521, 0.002589, 0.002675, 0.002773, 0.002883, 0.003006, 0.003142, 0.003285, 0.003431, 0.003577, 0.003723, 0.003865, 0.003996, 0.004116, 0.004221, 0.004307, 0.004371, 0.004412, 0.004428, 0.004418, 0.004380, 0.004316, 0.004230, 0.004121, 0.003991, 0.003846, 0.003687, 0.003520, 0.003348, 0.003173, 0.003003, 0.002838, 0.002687, 0.002548, 0.002424, 0.002316, 0.002222, 0.002148, 0.002085, 0.002036, 0.001997, 0.001966, 0.001945, 0.001928, 0.001914, 0.001904, 0.001896, 0.001890, 0.001888, 0.001884, 0.001882, 0.001880, 0.001879, 0.001877, 0.001878, 0.001878, 0.001878, 0.001878, 0.001878, 0.001878, 0.001878, 0.001879, 0.001878, 0.001879, 0.001879, 0.001879, 0.001880, 0.001879, 0.001878, 0.001878, 0.001877, 0.001877, 0.001876, 0.001876, 0.001876, 0.001876, 0.001876, 0.001877, 0.001877],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, 0.05599832200562286),\n        uv: (7.000000000000001, 5.0),\n        spectrum: [0.001660, 0.001659, 0.001659, 0.001659, 0.001659, 0.001659, 0.001659, 0.001658, 0.001658, 0.001659, 0.001660, 0.001665, 0.001672, 0.001686, 0.001712, 0.001751, 0.001806, 0.001880, 0.001969, 0.002075, 0.002196, 0.002334, 0.002483, 0.002642, 0.002808, 0.002975, 0.003142, 0.003305, 0.003463, 0.003613, 0.003753, 0.003879, 0.003988, 0.004081, 0.004155, 0.004209, 0.004242, 0.004255, 0.004247, 0.004219, 0.004175, 0.004117, 0.004045, 0.003965, 0.003878, 0.003788, 0.003698, 0.003610, 0.003527, 0.003448, 0.003379, 0.003316, 0.003262, 0.003217, 0.003180, 0.003150, 0.003127, 0.003110, 0.003098, 0.003088, 0.003081, 0.003076, 0.003071, 0.003067, 0.003064, 0.003061, 0.003059, 0.003057, 0.003056, 0.003054, 0.003054, 0.003053, 0.003052, 0.003052, 0.003051, 0.003050, 0.003051, 0.003050, 0.003051, 0.003051, 0.003051, 0.003053, 0.003054, 0.003054, 0.003054, 0.003054, 0.003054, 0.003053, 0.003054, 0.003053, 0.003054, 0.003054, 0.003054, 0.003054, 0.003054],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, 0.05599832200562286),\n        uv: (8.0, 5.0),\n        spectrum: [0.000930, 0.000931, 0.000931, 0.000931, 0.000932, 0.000933, 0.000933, 0.000934, 0.000934, 0.000936, 0.000938, 0.000942, 0.000950, 0.000965, 0.000993, 0.001035, 0.001095, 0.001173, 0.001270, 0.001385, 0.001517, 0.001665, 0.001826, 0.001999, 0.002178, 0.002362, 0.002549, 0.002736, 0.002922, 0.003103, 0.003279, 0.003447, 0.003604, 0.003751, 0.003883, 0.004000, 0.004102, 0.004189, 0.004261, 0.004319, 0.004363, 0.004393, 0.004412, 0.004421, 0.004420, 0.004413, 0.004400, 0.004383, 0.004365, 0.004346, 0.004326, 0.004308, 0.004293, 0.004279, 0.004269, 0.004261, 0.004254, 0.004249, 0.004245, 0.004243, 0.004241, 0.004240, 0.004238, 0.004237, 0.004236, 0.004235, 0.004235, 0.004234, 0.004234, 0.004234, 0.004234, 0.004234, 0.004233, 0.004232, 0.004231, 0.004230, 0.004229, 0.004228, 0.004228, 0.004227, 0.004227, 0.004227, 0.004227, 0.004228, 0.004228, 0.004228, 0.004228, 0.004228, 0.004228, 0.004227, 0.004226, 0.004226, 0.004226, 0.004226, 0.004226],\n    },\n    SpectrumDataPoint {\n        xystar: (0.162594324368683, 0.05599832200562286),\n        uv: (9.0, 5.0),\n        spectrum: [0.000215, 0.000214, 0.000214, 0.000215, 0.000215, 0.000215, 0.000215, 0.000215, 0.000214, 0.000214, 0.000216, 0.000221, 0.000231, 0.000249, 0.000279, 0.000325, 0.000388, 0.000471, 0.000572, 0.000694, 0.000833, 0.000989, 0.001161, 0.001346, 0.001542, 0.001746, 0.001956, 0.002170, 0.002386, 0.002602, 0.002815, 0.003026, 0.003230, 0.003428, 0.003618, 0.003798, 0.003968, 0.004127, 0.004276, 0.004414, 0.004541, 0.004659, 0.004767, 0.004864, 0.004950, 0.005026, 0.005093, 0.005152, 0.005203, 0.005244, 0.005280, 0.005309, 0.005332, 0.005351, 0.005367, 0.005380, 0.005389, 0.005398, 0.005405, 0.005411, 0.005415, 0.005418, 0.005420, 0.005421, 0.005423, 0.005423, 0.005423, 0.005422, 0.005422, 0.005421, 0.005420, 0.005419, 0.005419, 0.005418, 0.005417, 0.005416, 0.005416, 0.005415, 0.005414, 0.005415, 0.005415, 0.005414, 0.005414, 0.005413, 0.005413, 0.005413, 0.005412, 0.005412, 0.005412, 0.005411, 0.005411, 0.005410, 0.005410, 0.005410, 0.005410],\n    },\n    SpectrumDataPoint {\n        xystar: (0.21679243249157734, 0.05599832200562286),\n        uv: (10.0, 5.0),\n        spectrum: [0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000002, 0.000097, 0.000268, 0.000494, 0.000763, 0.001062, 0.001380, 0.001714, 0.002056, 0.002400, 0.002740, 0.003071, 0.003389, 0.003693, 0.003981, 0.004252, 0.004504, 0.004737, 0.004950, 0.005142, 0.005316, 0.005471, 0.005609, 0.005729, 0.005834, 0.005924, 0.006000, 0.006065, 0.006118, 0.006161, 0.006197, 0.006225, 0.006247, 0.006264, 0.006276, 0.006286, 0.006294, 0.006299, 0.006303, 0.006305, 0.006306, 0.006307, 0.006307, 0.006308, 0.006308, 0.006308, 0.006308, 0.006308, 0.006308, 0.006308, 0.006308, 0.006308, 0.006307, 0.006307, 0.006306, 0.006306, 0.006306, 0.006306, 0.006306, 0.006306, 0.006305, 0.006305, 0.006304, 0.006304, 0.006304, 0.006304, 0.006303, 0.006303, 0.006303, 0.006304, 0.006304, 0.006304, 0.006304],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.21679243249157729, 0.11199664401124566),\n        uv: (2.0, 5.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000376, 0.001245, 0.002562, 0.004250, 0.006211, 0.008324, 0.010458, 0.012472, 0.014244, 0.015672, 0.016680, 0.017213, 0.017218, 0.016651, 0.015470, 0.013683, 0.011364, 0.008668, 0.005845, 0.003215, 0.001131, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, 0.11199664401124566),\n        uv: (3.0, 5.999999999999999),\n        spectrum: [0.002087, 0.002087, 0.002087, 0.002087, 0.002087, 0.002089, 0.002090, 0.002093, 0.002098, 0.002107, 0.002122, 0.002150, 0.002199, 0.002288, 0.002437, 0.002662, 0.002974, 0.003375, 0.003856, 0.004407, 0.005015, 0.005662, 0.006322, 0.006976, 0.007600, 0.008169, 0.008662, 0.009068, 0.009367, 0.009536, 0.009553, 0.009399, 0.009060, 0.008533, 0.007829, 0.006976, 0.006000, 0.004942, 0.003850, 0.002781, 0.001790, 0.000948, 0.000328, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000002, 0.000001, 0.000001, 0.000002, 0.000002, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, 0.11199664401124566),\n        uv: (4.0, 5.999999999999999),\n        spectrum: [0.002692, 0.002692, 0.002692, 0.002692, 0.002693, 0.002695, 0.002697, 0.002698, 0.002700, 0.002701, 0.002707, 0.002719, 0.002742, 0.002780, 0.002848, 0.002952, 0.003093, 0.003280, 0.003507, 0.003772, 0.004070, 0.004394, 0.004739, 0.005093, 0.005443, 0.005781, 0.006100, 0.006391, 0.006644, 0.006845, 0.006987, 0.007058, 0.007048, 0.006947, 0.006754, 0.006474, 0.006110, 0.005671, 0.005161, 0.004598, 0.004000, 0.003378, 0.002751, 0.002144, 0.001578, 0.001069, 0.000641, 0.000310, 0.000095, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000002, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000002, 0.000002, 0.000000, 0.000000, 0.000001, 0.000001, 0.000002, 0.000003],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.11199664401124566),\n        uv: (5.0, 5.999999999999999),\n        spectrum: [0.002321, 0.002322, 0.002321, 0.002321, 0.002322, 0.002324, 0.002325, 0.002325, 0.002325, 0.002326, 0.002329, 0.002336, 0.002350, 0.002376, 0.002422, 0.002494, 0.002597, 0.002733, 0.002903, 0.003104, 0.003333, 0.003586, 0.003858, 0.004142, 0.004428, 0.004712, 0.004987, 0.005247, 0.005486, 0.005697, 0.005872, 0.006004, 0.006085, 0.006108, 0.006071, 0.005972, 0.005812, 0.005594, 0.005322, 0.004999, 0.004632, 0.004231, 0.003808, 0.003368, 0.002924, 0.002486, 0.002065, 0.001670, 0.001312, 0.000993, 0.000719, 0.000492, 0.000311, 0.000177, 0.000083, 0.000027, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.11199664401124566),\n        uv: (6.0, 5.999999999999999),\n        spectrum: [0.001610, 0.001610, 0.001610, 0.001610, 0.001610, 0.001610, 0.001611, 0.001612, 0.001614, 0.001617, 0.001624, 0.001635, 0.001653, 0.001685, 0.001736, 0.001813, 0.001919, 0.002057, 0.002227, 0.002425, 0.002651, 0.002900, 0.003170, 0.003453, 0.003744, 0.004036, 0.004325, 0.004603, 0.004866, 0.005106, 0.005316, 0.005490, 0.005621, 0.005705, 0.005738, 0.005717, 0.005642, 0.005516, 0.005342, 0.005125, 0.004868, 0.004576, 0.004257, 0.003920, 0.003573, 0.003223, 0.002883, 0.002557, 0.002252, 0.001973, 0.001727, 0.001512, 0.001329, 0.001177, 0.001053, 0.000955, 0.000876, 0.000815, 0.000769, 0.000735, 0.000710, 0.000692, 0.000678, 0.000668, 0.000660, 0.000654, 0.000650, 0.000647, 0.000645, 0.000643, 0.000642, 0.000640, 0.000639, 0.000639, 0.000639, 0.000638, 0.000638, 0.000638, 0.000638, 0.000638, 0.000637, 0.000636, 0.000636, 0.000635, 0.000634, 0.000634, 0.000633, 0.000633, 0.000632, 0.000632, 0.000631, 0.000631, 0.000631, 0.000631, 0.000631],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, 0.11199664401124566),\n        uv: (7.000000000000001, 5.999999999999999),\n        spectrum: [0.000878, 0.000877, 0.000876, 0.000875, 0.000874, 0.000873, 0.000873, 0.000874, 0.000877, 0.000883, 0.000890, 0.000902, 0.000922, 0.000955, 0.001008, 0.001087, 0.001198, 0.001342, 0.001521, 0.001730, 0.001971, 0.002237, 0.002523, 0.002826, 0.003136, 0.003449, 0.003759, 0.004059, 0.004346, 0.004614, 0.004859, 0.005073, 0.005249, 0.005383, 0.005470, 0.005510, 0.005503, 0.005449, 0.005351, 0.005213, 0.005038, 0.004833, 0.004603, 0.004356, 0.004098, 0.003837, 0.003578, 0.003328, 0.003093, 0.002877, 0.002686, 0.002518, 0.002375, 0.002257, 0.002159, 0.002081, 0.002019, 0.001972, 0.001934, 0.001905, 0.001883, 0.001865, 0.001852, 0.001843, 0.001836, 0.001831, 0.001828, 0.001825, 0.001823, 0.001822, 0.001821, 0.001820, 0.001819, 0.001819, 0.001820, 0.001821, 0.001822, 0.001824, 0.001824, 0.001825, 0.001825, 0.001824, 0.001824, 0.001823, 0.001823, 0.001824, 0.001825, 0.001826, 0.001827, 0.001827, 0.001828, 0.001828, 0.001828, 0.001828, 0.001828],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, 0.11199664401124566),\n        uv: (8.0, 5.999999999999999),\n        spectrum: [0.000179, 0.000179, 0.000179, 0.000178, 0.000179, 0.000180, 0.000180, 0.000180, 0.000182, 0.000185, 0.000191, 0.000201, 0.000219, 0.000249, 0.000302, 0.000382, 0.000495, 0.000644, 0.000823, 0.001036, 0.001281, 0.001555, 0.001852, 0.002166, 0.002491, 0.002823, 0.003155, 0.003482, 0.003800, 0.004101, 0.004382, 0.004636, 0.004858, 0.005044, 0.005190, 0.005295, 0.005360, 0.005384, 0.005370, 0.005319, 0.005235, 0.005121, 0.004981, 0.004822, 0.004648, 0.004466, 0.004282, 0.004102, 0.003931, 0.003773, 0.003631, 0.003507, 0.003400, 0.003312, 0.003238, 0.003179, 0.003133, 0.003096, 0.003067, 0.003045, 0.003030, 0.003018, 0.003010, 0.003004, 0.003000, 0.002996, 0.002993, 0.002992, 0.002990, 0.002988, 0.002987, 0.002986, 0.002986, 0.002985, 0.002986, 0.002985, 0.002985, 0.002985, 0.002985, 0.002985, 0.002985, 0.002984, 0.002984, 0.002984, 0.002984, 0.002984, 0.002985, 0.002985, 0.002984, 0.002984, 0.002984, 0.002984, 0.002985, 0.002985, 0.002985],\n    },\n    SpectrumDataPoint {\n        xystar: (0.162594324368683, 0.11199664401124566),\n        uv: (9.0, 5.999999999999999),\n        spectrum: [0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000052, 0.000168, 0.000346, 0.000577, 0.000862, 0.001185, 0.001538, 0.001913, 0.002298, 0.002686, 0.003071, 0.003447, 0.003805, 0.004142, 0.004450, 0.004722, 0.004957, 0.005149, 0.005300, 0.005409, 0.005482, 0.005514, 0.005511, 0.005477, 0.005414, 0.005330, 0.005227, 0.005113, 0.004993, 0.004871, 0.004750, 0.004636, 0.004532, 0.004439, 0.004359, 0.004292, 0.004236, 0.004190, 0.004153, 0.004126, 0.004106, 0.004089, 0.004076, 0.004068, 0.004062, 0.004058, 0.004054, 0.004051, 0.004049, 0.004048, 0.004047, 0.004046, 0.004045, 0.004045, 0.004045, 0.004044, 0.004044, 0.004044, 0.004044, 0.004044, 0.004044, 0.004043, 0.004043, 0.004043, 0.004043, 0.004043, 0.004043, 0.004043, 0.004042, 0.004042, 0.004042, 0.004042, 0.004042, 0.004042, 0.004042, 0.004041, 0.004042],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.21679243249157729, 0.16799496601686853),\n        uv: (2.0, 6.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000219, 0.002207, 0.005534, 0.009696, 0.014123, 0.018315, 0.021870, 0.024468, 0.025906, 0.026029, 0.024730, 0.021964, 0.017858, 0.012795, 0.007451, 0.002788, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000002, 0.000001, 0.000002, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000002],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, 0.16799496601686853),\n        uv: (3.0, 6.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000065, 0.000263, 0.000629, 0.001181, 0.001925, 0.002844, 0.003912, 0.005094, 0.006351, 0.007633, 0.008886, 0.010062, 0.011113, 0.012001, 0.012696, 0.013162, 0.013362, 0.013258, 0.012817, 0.012028, 0.010910, 0.009506, 0.007889, 0.006154, 0.004406, 0.002769, 0.001384, 0.000405, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, 0.16799496601686853),\n        uv: (4.0, 6.999999999999999),\n        spectrum: [0.001212, 0.001213, 0.001213, 0.001214, 0.001213, 0.001214, 0.001215, 0.001218, 0.001224, 0.001233, 0.001248, 0.001274, 0.001321, 0.001403, 0.001541, 0.001744, 0.002034, 0.002407, 0.002856, 0.003381, 0.003960, 0.004592, 0.005255, 0.005929, 0.006594, 0.007229, 0.007815, 0.008337, 0.008783, 0.009129, 0.009355, 0.009441, 0.009366, 0.009127, 0.008722, 0.008159, 0.007458, 0.006641, 0.005729, 0.004761, 0.003770, 0.002808, 0.001916, 0.001135, 0.000525, 0.000130, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000002, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.16799496601686853),\n        uv: (5.0, 6.999999999999999),\n        spectrum: [0.001278, 0.001277, 0.001277, 0.001277, 0.001278, 0.001279, 0.001279, 0.001281, 0.001285, 0.001291, 0.001300, 0.001318, 0.001350, 0.001405, 0.001496, 0.001634, 0.001826, 0.002076, 0.002382, 0.002740, 0.003144, 0.003588, 0.004061, 0.004552, 0.005049, 0.005540, 0.006011, 0.006454, 0.006857, 0.007207, 0.007493, 0.007699, 0.007813, 0.007825, 0.007730, 0.007530, 0.007226, 0.006826, 0.006341, 0.005782, 0.005164, 0.004504, 0.003821, 0.003139, 0.002477, 0.001857, 0.001301, 0.000825, 0.000449, 0.000184, 0.000033, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.16799496601686853),\n        uv: (6.0, 6.999999999999999),\n        spectrum: [0.000820, 0.000820, 0.000820, 0.000820, 0.000820, 0.000820, 0.000820, 0.000822, 0.000826, 0.000831, 0.000841, 0.000855, 0.000881, 0.000928, 0.001006, 0.001124, 0.001288, 0.001501, 0.001763, 0.002070, 0.002421, 0.002809, 0.003228, 0.003666, 0.004114, 0.004563, 0.005004, 0.005429, 0.005826, 0.006190, 0.006507, 0.006767, 0.006960, 0.007078, 0.007115, 0.007067, 0.006938, 0.006729, 0.006447, 0.006096, 0.005686, 0.005227, 0.004730, 0.004210, 0.003678, 0.003149, 0.002636, 0.002152, 0.001709, 0.001313, 0.000971, 0.000685, 0.000456, 0.000279, 0.000152, 0.000067, 0.000018, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000003, 0.000002, 0.000003, 0.000003, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, 0.16799496601686853),\n        uv: (7.000000000000001, 6.999999999999999),\n        spectrum: [0.000159, 0.000160, 0.000161, 0.000163, 0.000164, 0.000165, 0.000168, 0.000170, 0.000172, 0.000174, 0.000180, 0.000192, 0.000215, 0.000256, 0.000328, 0.000441, 0.000600, 0.000810, 0.001070, 0.001376, 0.001727, 0.002117, 0.002539, 0.002984, 0.003443, 0.003904, 0.004359, 0.004799, 0.005218, 0.005607, 0.005957, 0.006256, 0.006497, 0.006672, 0.006775, 0.006805, 0.006761, 0.006646, 0.006463, 0.006218, 0.005918, 0.005569, 0.005182, 0.004766, 0.004333, 0.003894, 0.003460, 0.003042, 0.002652, 0.002295, 0.001977, 0.001701, 0.001467, 0.001274, 0.001118, 0.000994, 0.000899, 0.000826, 0.000771, 0.000732, 0.000705, 0.000685, 0.000672, 0.000664, 0.000659, 0.000656, 0.000654, 0.000652, 0.000650, 0.000647, 0.000644, 0.000643, 0.000642, 0.000640, 0.000637, 0.000634, 0.000633, 0.000632, 0.000629, 0.000627, 0.000626, 0.000624, 0.000622, 0.000620, 0.000617, 0.000616, 0.000614, 0.000612, 0.000609, 0.000608, 0.000607, 0.000605, 0.000605, 0.000603, 0.000603],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, 0.16799496601686853),\n        uv: (8.0, 6.999999999999999),\n        spectrum: [0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000024, 0.000118, 0.000284, 0.000519, 0.000821, 0.001184, 0.001600, 0.002057, 0.002541, 0.003040, 0.003543, 0.004039, 0.004522, 0.004979, 0.005400, 0.005776, 0.006094, 0.006349, 0.006533, 0.006646, 0.006686, 0.006655, 0.006557, 0.006396, 0.006177, 0.005911, 0.005603, 0.005266, 0.004908, 0.004540, 0.004173, 0.003818, 0.003482, 0.003173, 0.002896, 0.002653, 0.002445, 0.002271, 0.002128, 0.002014, 0.001923, 0.001853, 0.001800, 0.001759, 0.001729, 0.001708, 0.001693, 0.001681, 0.001673, 0.001668, 0.001663, 0.001660, 0.001657, 0.001656, 0.001654, 0.001653, 0.001652, 0.001652, 0.001651, 0.001650, 0.001650, 0.001650, 0.001649, 0.001649, 0.001649, 0.001648, 0.001648, 0.001648, 0.001648, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647],\n    },\n    SpectrumDataPoint {\n        xystar: (0.162594324368683, 0.16799496601686853),\n        uv: (9.0, 6.999999999999999),\n        spectrum: [0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000161, 0.000512, 0.001001, 0.001585, 0.002230, 0.002905, 0.003587, 0.004252, 0.004879, 0.005447, 0.005940, 0.006348, 0.006664, 0.006886, 0.007013, 0.007048, 0.006996, 0.006863, 0.006659, 0.006393, 0.006079, 0.005728, 0.005354, 0.004972, 0.004594, 0.004231, 0.003895, 0.003590, 0.003322, 0.003092, 0.002899, 0.002742, 0.002617, 0.002517, 0.002439, 0.002379, 0.002334, 0.002301, 0.002277, 0.002260, 0.002247, 0.002237, 0.002231, 0.002227, 0.002224, 0.002222, 0.002221, 0.002219, 0.002218, 0.002216, 0.002215, 0.002214, 0.002214, 0.002214, 0.002214, 0.002213, 0.002213, 0.002213, 0.002213, 0.002213, 0.002212, 0.002212, 0.002212, 0.002212, 0.002213, 0.002213, 0.002213, 0.002214, 0.002214, 0.002214, 0.002214, 0.002215],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.21679243249157729, 0.22399328802249138),\n        uv: (2.0, 7.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001663, 0.008895, 0.018646, 0.028404, 0.036236, 0.040926, 0.041705, 0.038211, 0.030549, 0.019783, 0.008306, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, 0.22399328802249138),\n        uv: (3.0, 7.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000620, 0.001807, 0.003472, 0.005503, 0.007763, 0.010094, 0.012348, 0.014397, 0.016134, 0.017473, 0.018333, 0.018638, 0.018310, 0.017304, 0.015627, 0.013359, 0.010661, 0.007758, 0.004916, 0.002440, 0.000674, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, 0.22399328802249138),\n        uv: (4.0, 7.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000092, 0.000326, 0.000716, 0.001264, 0.001969, 0.002808, 0.003762, 0.004814, 0.005914, 0.007033, 0.008130, 0.009166, 0.010113, 0.010936, 0.011610, 0.012100, 0.012371, 0.012388, 0.012135, 0.011595, 0.010787, 0.009742, 0.008499, 0.007113, 0.005650, 0.004182, 0.002795, 0.001581, 0.000640, 0.000078, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.22399328802249138),\n        uv: (5.0, 7.999999999999999),\n        spectrum: [0.000011, 0.000011, 0.000011, 0.000011, 0.000012, 0.000013, 0.000015, 0.000018, 0.000023, 0.000032, 0.000048, 0.000076, 0.000125, 0.000213, 0.000359, 0.000581, 0.000890, 0.001291, 0.001779, 0.002348, 0.002989, 0.003689, 0.004435, 0.005202, 0.005971, 0.006722, 0.007437, 0.008098, 0.008686, 0.009184, 0.009569, 0.009819, 0.009916, 0.009845, 0.009605, 0.009201, 0.008645, 0.007949, 0.007137, 0.006235, 0.005273, 0.004285, 0.003309, 0.002387, 0.001561, 0.000872, 0.000360, 0.000059, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.22399328802249138),\n        uv: (6.0, 7.999999999999999),\n        spectrum: [0.000000, 0.000002, 0.000000, 0.000002, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000016, 0.000064, 0.000159, 0.000318, 0.000546, 0.000851, 0.001228, 0.001674, 0.002190, 0.002757, 0.003370, 0.004011, 0.004665, 0.005316, 0.005950, 0.006556, 0.007119, 0.007621, 0.008052, 0.008390, 0.008624, 0.008739, 0.008733, 0.008600, 0.008347, 0.007976, 0.007502, 0.006933, 0.006286, 0.005581, 0.004833, 0.004074, 0.003320, 0.002599, 0.001931, 0.001339, 0.000845, 0.000459, 0.000189, 0.000037, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000002, 0.000000, 0.000001, 0.000002, 0.000000, 0.000002, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, 0.22399328802249138),\n        uv: (7.000000000000001, 7.999999999999999),\n        spectrum: [0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000079, 0.000245, 0.000501, 0.000843, 0.001264, 0.001760, 0.002317, 0.002918, 0.003549, 0.004191, 0.004831, 0.005454, 0.006048, 0.006598, 0.007090, 0.007510, 0.007841, 0.008073, 0.008200, 0.008218, 0.008127, 0.007934, 0.007642, 0.007260, 0.006800, 0.006273, 0.005692, 0.005078, 0.004445, 0.003811, 0.003193, 0.002606, 0.002067, 0.001585, 0.001166, 0.000818, 0.000538, 0.000324, 0.000170, 0.000071, 0.000016, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001, 0.000001, 0.000002, 0.000001, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, 0.22399328802249138),\n        uv: (8.0, 7.999999999999999),\n        spectrum: [0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000129, 0.000421, 0.000855, 0.001404, 0.002036, 0.002724, 0.003441, 0.004166, 0.004880, 0.005562, 0.006197, 0.006764, 0.007247, 0.007631, 0.007909, 0.008076, 0.008130, 0.008073, 0.007912, 0.007653, 0.007303, 0.006878, 0.006388, 0.005847, 0.005274, 0.004686, 0.004101, 0.003532, 0.002996, 0.002503, 0.002059, 0.001672, 0.001342, 0.001066, 0.000841, 0.000660, 0.000518, 0.000409, 0.000325, 0.000263, 0.000217, 0.000184, 0.000161, 0.000145, 0.000133, 0.000124, 0.000118, 0.000113, 0.000110, 0.000108, 0.000106, 0.000104, 0.000104, 0.000103, 0.000102, 0.000102, 0.000102, 0.000102, 0.000102, 0.000102, 0.000101, 0.000100, 0.000099, 0.000099, 0.000099, 0.000099, 0.000099, 0.000098, 0.000099, 0.000098, 0.000098, 0.000098, 0.000098, 0.000098, 0.000098],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, 0.27999161002811424),\n        uv: (3.0, 8.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000190, 0.002115, 0.005345, 0.009341, 0.013603, 0.017694, 0.021257, 0.024023, 0.025768, 0.026299, 0.025451, 0.023170, 0.019569, 0.014974, 0.009937, 0.005187, 0.001564, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000002, 0.000002, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000003, 0.000003, 0.000003],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, 0.27999161002811424),\n        uv: (4.0, 8.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000403, 0.001222, 0.002407, 0.003898, 0.005611, 0.007442, 0.009292, 0.011069, 0.012696, 0.014107, 0.015237, 0.016023, 0.016395, 0.016298, 0.015698, 0.014601, 0.013057, 0.011152, 0.009001, 0.006737, 0.004517, 0.002524, 0.000959, 0.000038, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.27999161002811424),\n        uv: (5.0, 8.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000040, 0.000290, 0.000749, 0.001404, 0.002233, 0.003211, 0.004304, 0.005468, 0.006658, 0.007830, 0.008948, 0.009977, 0.010885, 0.011635, 0.012188, 0.012510, 0.012570, 0.012351, 0.011858, 0.011105, 0.010118, 0.008939, 0.007617, 0.006206, 0.004771, 0.003386, 0.002132, 0.001092, 0.000355, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.27999161002811424),\n        uv: (6.0, 8.999999999999998),\n        spectrum: [0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000126, 0.000417, 0.000868, 0.001464, 0.002193, 0.003028, 0.003939, 0.004892, 0.005855, 0.006801, 0.007706, 0.008544, 0.009289, 0.009915, 0.010392, 0.010698, 0.010810, 0.010727, 0.010448, 0.009980, 0.009340, 0.008548, 0.007633, 0.006625, 0.005558, 0.004474, 0.003416, 0.002430, 0.001558, 0.000844, 0.000327, 0.000038, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000003, 0.000004, 0.000004, 0.000004, 0.000005, 0.000006, 0.000006, 0.000006, 0.000006, 0.000006, 0.000006, 0.000006],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, 0.27999161002811424),\n        uv: (7.000000000000001, 8.999999999999998),\n        spectrum: [0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000125, 0.000447, 0.000950, 0.001610, 0.002391, 0.003254, 0.004165, 0.005093, 0.006008, 0.006887, 0.007704, 0.008433, 0.009048, 0.009525, 0.009844, 0.009996, 0.009980, 0.009795, 0.009450, 0.008957, 0.008332, 0.007596, 0.006772, 0.005887, 0.004970, 0.004056, 0.003174, 0.002356, 0.001629, 0.001019, 0.000543, 0.000213, 0.000033, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, 0.27999161002811424),\n        uv: (8.0, 8.999999999999998),\n        spectrum: [0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000139, 0.000650, 0.001426, 0.002377, 0.003433, 0.004542, 0.005650, 0.006710, 0.007679, 0.008516, 0.009187, 0.009675, 0.009969, 0.010064, 0.009969, 0.009693, 0.009245, 0.008648, 0.007923, 0.007105, 0.006218, 0.005292, 0.004367, 0.003476, 0.002649, 0.001905, 0.001272, 0.000762, 0.000384, 0.000134, 0.000010, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, 0.3359899320337371),\n        uv: (3.0, 10.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003207, 0.010021, 0.018352, 0.026499, 0.033223, 0.037591, 0.038902, 0.036687, 0.030958, 0.022441, 0.012719, 0.004211, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, 0.3359899320337371),\n        uv: (4.0, 10.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000188, 0.001488, 0.003670, 0.006439, 0.009503, 0.012602, 0.015516, 0.018066, 0.020093, 0.021449, 0.021991, 0.021616, 0.020282, 0.018049, 0.015084, 0.011641, 0.008032, 0.004621, 0.001825, 0.000112, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.3359899320337371),\n        uv: (5.0, 10.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000127, 0.000760, 0.001840, 0.003281, 0.004972, 0.006801, 0.008664, 0.010471, 0.012144, 0.013612, 0.014804, 0.015645, 0.016072, 0.016035, 0.015515, 0.014533, 0.013141, 0.011410, 0.009435, 0.007329, 0.005223, 0.003267, 0.001624, 0.000473, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.3359899320337371),\n        uv: (6.0, 10.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000340, 0.001033, 0.002025, 0.003239, 0.004602, 0.006037, 0.007478, 0.008871, 0.010167, 0.011315, 0.012262, 0.012959, 0.013364, 0.013449, 0.013208, 0.012654, 0.011809, 0.010710, 0.009401, 0.007942, 0.006402, 0.004851, 0.003374, 0.002061, 0.001003, 0.000286, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, 0.3359899320337371),\n        uv: (7.000000000000001, 10.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000362, 0.001116, 0.002160, 0.003399, 0.004749, 0.006137, 0.007504, 0.008795, 0.009954, 0.010929, 0.011673, 0.012150, 0.012345, 0.012257, 0.011892, 0.011270, 0.010417, 0.009370, 0.008171, 0.006871, 0.005527, 0.004200, 0.002953, 0.001853, 0.000959, 0.000324, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, 0.3359899320337371),\n        uv: (8.0, 10.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000781, 0.002423, 0.004576, 0.006912, 0.009148, 0.011074, 0.012569, 0.013567, 0.014036, 0.013966, 0.013384, 0.012350, 0.010939, 0.009246, 0.007389, 0.005493, 0.003691, 0.002113, 0.000897, 0.000161, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, 0.39198825403935994),\n        uv: (3.0, 10.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000774, 0.020476, 0.044616, 0.062955, 0.068902, 0.059372, 0.036659, 0.010641, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, 0.39198825403935994),\n        uv: (4.0, 10.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002385, 0.006713, 0.012032, 0.017536, 0.022603, 0.026720, 0.029451, 0.030420, 0.029389, 0.026340, 0.021555, 0.015636, 0.009438, 0.003969, 0.000388, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.39198825403935994),\n        uv: (5.0, 10.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000985, 0.002940, 0.005535, 0.008470, 0.011484, 0.014360, 0.016916, 0.018981, 0.020393, 0.021014, 0.020758, 0.019611, 0.017654, 0.015041, 0.011974, 0.008702, 0.005513, 0.002746, 0.000771, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.39198825403935994),\n        uv: (6.0, 10.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000283, 0.001378, 0.003057, 0.005104, 0.007334, 0.009591, 0.011744, 0.013674, 0.015264, 0.016407, 0.017017, 0.017045, 0.016494, 0.015405, 0.013845, 0.011904, 0.009699, 0.007369, 0.005073, 0.002989, 0.001312, 0.000246, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, 0.39198825403935994),\n        uv: (7.000000000000001, 10.999999999999998),\n        spectrum: [0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000867, 0.002473, 0.004533, 0.006818, 0.009136, 0.011318, 0.013202, 0.014652, 0.015575, 0.015933, 0.015725, 0.014981, 0.013753, 0.012120, 0.010181, 0.008060, 0.005895, 0.003839, 0.002058, 0.000722, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, 0.4479865760449827),\n        uv: (4.0, 11.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003473, 0.012890, 0.024467, 0.035265, 0.043015, 0.045962, 0.043133, 0.034720, 0.022428, 0.009488, 0.000315, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.4479865760449827),\n        uv: (5.0, 11.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001267, 0.004901, 0.009826, 0.015193, 0.020312, 0.024607, 0.027567, 0.028789, 0.028045, 0.025373, 0.021104, 0.015791, 0.010131, 0.004951, 0.001213, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.4479865760449827),\n        uv: (6.0, 11.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001557, 0.004498, 0.008198, 0.012149, 0.015929, 0.019160, 0.021509, 0.022720, 0.022675, 0.021409, 0.019070, 0.015884, 0.012148, 0.008230, 0.004562, 0.001637, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, 0.4479865760449827),\n        uv: (7.000000000000001, 11.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002626, 0.007837, 0.013870, 0.019250, 0.023008, 0.024733, 0.024362, 0.022048, 0.018145, 0.013206, 0.007968, 0.003318, 0.000286, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.5039848980506056),\n        uv: (5.0, 13.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.007297, 0.018940, 0.031009, 0.040349, 0.044554, 0.042370, 0.034184, 0.022108, 0.009515, 0.000531, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.5039848980506056),\n        uv: (6.0, 13.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003623, 0.012173, 0.022215, 0.030785, 0.035662, 0.035813, 0.031493, 0.023829, 0.014490, 0.005653, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.2709906464701516, -0.22399328802249138),\n        uv: (0.9999980468749987, 0.0),\n        spectrum: [0.023575, 0.023575, 0.023573, 0.023571, 0.023565, 0.023554, 0.023534, 0.023498, 0.023433, 0.023312, 0.023102, 0.022727, 0.022062, 0.020919, 0.019066, 0.016400, 0.013015, 0.009187, 0.005332, 0.002029, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000067, 0.000577, 0.001244, 0.001906, 0.002492, 0.002971, 0.003344, 0.003630, 0.003844, 0.004001, 0.004113, 0.004195, 0.004253, 0.004292, 0.004320, 0.004341, 0.004355, 0.004365, 0.004372, 0.004377, 0.004380, 0.004382, 0.004384, 0.004384, 0.004385, 0.004386, 0.004386, 0.004386, 0.004387, 0.004386, 0.004386, 0.004387, 0.004387, 0.004386, 0.004386, 0.004386, 0.004386, 0.004385, 0.004385, 0.004385],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.2952315971735554, -0.16799496601686853),\n        uv: (0.5527324218750005, 1.0),\n        spectrum: [0.002755, 0.002755, 0.002756, 0.002757, 0.002759, 0.002763, 0.002770, 0.002784, 0.002809, 0.002852, 0.002925, 0.003051, 0.003269, 0.003636, 0.004222, 0.005062, 0.006140, 0.007403, 0.008747, 0.010037, 0.011103, 0.011761, 0.011858, 0.011318, 0.010148, 0.008438, 0.006340, 0.004093, 0.002002, 0.000458, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.29681943237246833, -0.11199664401124569),\n        uv: (0.5234355468750005, 1.9999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000644, 0.003307, 0.007382, 0.012106, 0.016609, 0.020063, 0.021845, 0.021638, 0.019453, 0.015563, 0.010596, 0.005450, 0.001342, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.29242642165547594, -0.05599832200562286),\n        uv: (0.6044902343749996, 2.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.004514, 0.012223, 0.020649, 0.027526, 0.031288, 0.031190, 0.027174, 0.020110, 0.011515, 0.003670, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.28533409110033164, 0.0),\n        uv: (0.7353496093749987, 3.9999999999999996),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002017, 0.013202, 0.026877, 0.037910, 0.043095, 0.040934, 0.032211, 0.019299, 0.006340, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.2743780282278324, 0.05599832200562286),\n        uv: (0.9374980468749996, 5.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.004900, 0.019327, 0.035095, 0.046403, 0.049670, 0.044252, 0.031492, 0.014845, 0.000659, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.27099054061447164, 0.07355259286543629),\n        uv: (0.9999999999999991, 5.3134785156249995),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001600, 0.016345, 0.034217, 0.047985, 0.053062, 0.048335, 0.035048, 0.016928, 0.001058, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.2617282744764928, 0.11199664401124566),\n        uv: (1.1708964843750005, 5.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.009734, 0.028769, 0.046634, 0.056239, 0.054914, 0.042978, 0.023819, 0.004833, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.24738482984631277, 0.16799496601686853),\n        uv: (1.4355449218750005, 6.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000277, 0.019442, 0.042522, 0.058819, 0.063242, 0.054463, 0.034926, 0.011788, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.23113598297743726, 0.22399328802249138),\n        uv: (1.7353496093749996, 7.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.006807, 0.031020, 0.054932, 0.068424, 0.066501, 0.049084, 0.021876, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.21679243249157729, 0.2681795733517758),\n        uv: (2.0, 8.789064453124999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.021923, 0.049140, 0.068549, 0.072642, 0.059125, 0.031591, 0.003161, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.2129288060299024, 0.27999161002811424),\n        uv: (2.0712871093750005, 8.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.017621, 0.045984, 0.068510, 0.075341, 0.062821, 0.034150, 0.003487, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.1925515876438533, 0.3359899320337371),\n        uv: (2.447263671875, 10.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001791, 0.031646, 0.063609, 0.081019, 0.075572, 0.047743, 0.011608, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16899869885997837, 0.39198825403935994),\n        uv: (2.881833984374999, 10.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.009856, 0.045649, 0.077287, 0.086458, 0.066290, 0.026512, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16259432436868296, 0.40730040458449507),\n        uv: (3.0, 11.273439453124999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003203, 0.040349, 0.076519, 0.089963, 0.071555, 0.030200, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.13999424255983572, 0.4479865760449827),\n        uv: (3.4169902343749987, 11.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.020613, 0.058369, 0.084682, 0.082562, 0.050245, 0.007521, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10839621624578864, 0.49922733748630377),\n        uv: (4.0, 12.915041015625),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.023420, 0.071224, 0.096062, 0.075741, 0.024085, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.10331524946494723, 0.5039848980506056),\n        uv: (4.093748046875, 13.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.021532, 0.065142, 0.090615, 0.077320, 0.032759, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.05419810812289431, 0.542483853801194),\n        uv: (5.0, 13.687501953124999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.014494, 0.055320, 0.081441, 0.071778, 0.033095, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.02397641735926503, 0.5039848980506056),\n        uv: (6.442384765625, 13.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005204, 0.020626, 0.035192, 0.042422, 0.040706, 0.031487, 0.018026, 0.005302, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.0, 0.5294686188037934),\n        uv: (6.0, 13.455080078124999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.010722, 0.035142, 0.053289, 0.055233, 0.041528, 0.019537, 0.000631, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.09077135139353519, 0.39198825403935994),\n        uv: (7.674806640625, 10.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000604, 0.004426, 0.009453, 0.014267, 0.018085, 0.020476, 0.021221, 0.020297, 0.017879, 0.014312, 0.010088, 0.005816, 0.002196, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (0.06181982293335631, 0.4479865760449827),\n        uv: (7.140626953125001, 11.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001255, 0.008284, 0.016733, 0.023695, 0.027786, 0.028524, 0.025969, 0.020688, 0.013747, 0.006665, 0.001339, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000002],\n    },\n    SpectrumDataPoint {\n        xystar: (0.05419810812289436, 0.46050974766210345),\n        uv: (7.000000000000001, 12.223634765624999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001972, 0.010154, 0.019515, 0.026750, 0.030390, 0.030062, 0.026050, 0.019258, 0.011200, 0.003936, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.16000096606613853, 0.22399328802249138),\n        uv: (8.952150390625002, 7.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000381, 0.001385, 0.002759, 0.004279, 0.005778, 0.007149, 0.008320, 0.009241, 0.009883, 0.010233, 0.010293, 0.010076, 0.009613, 0.008941, 0.008102, 0.007142, 0.006113, 0.005065, 0.004044, 0.003092, 0.002242, 0.001519, 0.000937, 0.000500, 0.000205, 0.000043, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (0.13845933520088657, 0.27999161002811424),\n        uv: (8.554689453125, 8.999999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001012, 0.002836, 0.005008, 0.007185, 0.009158, 0.010791, 0.011986, 0.012686, 0.012879, 0.012580, 0.011833, 0.010710, 0.009297, 0.007691, 0.006001, 0.004346, 0.002835, 0.001568, 0.000630, 0.000090, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.1157532918564318, 0.3359899320337371),\n        uv: (8.135744140625, 10.0),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000632, 0.003065, 0.006282, 0.009544, 0.012415, 0.014622, 0.015992, 0.016441, 0.015975, 0.014677, 0.012696, 0.010232, 0.007530, 0.004863, 0.002525, 0.000809, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (0.10839621624578867, 0.35316140186421524),\n        uv: (8.0, 10.306642578125),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000571, 0.003318, 0.006986, 0.010670, 0.013840, 0.016178, 0.017488, 0.017686, 0.016806, 0.014979, 0.012419, 0.009407, 0.006277, 0.003401, 0.001175, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.2017081039575845, 0.11199664401124566),\n        uv: (9.721681640625, 5.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000121, 0.000506, 0.001076, 0.001767, 0.002517, 0.003271, 0.003991, 0.004651, 0.005234, 0.005724, 0.006115, 0.006403, 0.006593, 0.006686, 0.006690, 0.006615, 0.006473, 0.006278, 0.006045, 0.005787, 0.005517, 0.005248, 0.004990, 0.004753, 0.004542, 0.004357, 0.004200, 0.004070, 0.003966, 0.003883, 0.003819, 0.003770, 0.003733, 0.003706, 0.003687, 0.003671, 0.003661, 0.003654, 0.003648, 0.003643, 0.003640, 0.003637, 0.003636, 0.003635, 0.003634, 0.003633, 0.003633, 0.003632, 0.003632, 0.003632, 0.003632, 0.003632, 0.003631, 0.003631, 0.003631, 0.003630, 0.003630, 0.003630, 0.003630, 0.003630, 0.003629, 0.003629, 0.003629, 0.003629, 0.003629, 0.003629, 0.003629, 0.003629],\n    },\n    SpectrumDataPoint {\n        xystar: (0.18101331853175281, 0.16799496601686853),\n        uv: (9.339845703125, 6.999999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000152, 0.000746, 0.001631, 0.002685, 0.003791, 0.004856, 0.005829, 0.006671, 0.007357, 0.007869, 0.008199, 0.008348, 0.008324, 0.008146, 0.007828, 0.007396, 0.006874, 0.006289, 0.005673, 0.005048, 0.004438, 0.003864, 0.003339, 0.002873, 0.002472, 0.002136, 0.001856, 0.001631, 0.001454, 0.001317, 0.001213, 0.001133, 0.001074, 0.001032, 0.001003, 0.000983, 0.000966, 0.000952, 0.000946, 0.000941, 0.000937, 0.000934, 0.000931, 0.000928, 0.000928, 0.000926, 0.000925, 0.000926, 0.000925, 0.000925, 0.000924, 0.000924, 0.000924, 0.000924, 0.000924, 0.000924, 0.000923, 0.000923, 0.000924, 0.000923, 0.000924, 0.000924, 0.000923, 0.000923, 0.000923, 0.000923, 0.000923],\n    },\n    SpectrumDataPoint {\n        xystar: (0.162594324368683, 0.21715766472751205),\n        uv: (9.0, 7.877931640624999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000430, 0.001426, 0.002756, 0.004211, 0.005644, 0.006955, 0.008079, 0.008968, 0.009595, 0.009950, 0.010031, 0.009854, 0.009445, 0.008834, 0.008061, 0.007171, 0.006210, 0.005222, 0.004251, 0.003335, 0.002507, 0.001787, 0.001190, 0.000723, 0.000380, 0.000153, 0.000032, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.26342196535533324, -0.05599832200562286),\n        uv: (10.860353515625, 2.999999999999999),\n        spectrum: [0.000196, 0.000197, 0.000197, 0.000197, 0.000198, 0.000199, 0.000200, 0.000200, 0.000199, 0.000200, 0.000200, 0.000199, 0.000196, 0.000190, 0.000180, 0.000166, 0.000147, 0.000123, 0.000096, 0.000068, 0.000041, 0.000019, 0.000004, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000030, 0.000115, 0.000261, 0.000472, 0.000748, 0.001087, 0.001486, 0.001941, 0.002446, 0.002993, 0.003573, 0.004177, 0.004793, 0.005409, 0.006013, 0.006594, 0.007142, 0.007649, 0.008107, 0.008513, 0.008864, 0.009162, 0.009408, 0.009609, 0.009769, 0.009895, 0.009990, 0.010062, 0.010116, 0.010155, 0.010183, 0.010203, 0.010218, 0.010228, 0.010236, 0.010241, 0.010244, 0.010247, 0.010248, 0.010249, 0.010249, 0.010248, 0.010249, 0.010249, 0.010249, 0.010249, 0.010248, 0.010249, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010247, 0.010247],\n    },\n    SpectrumDataPoint {\n        xystar: (0.24288596344939292, 0.0),\n        uv: (10.481447265625, 3.9999999999999996),\n        spectrum: [0.000000, 0.000001, 0.000000, 0.000001, 0.000002, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000018, 0.000056, 0.000116, 0.000195, 0.000285, 0.000387, 0.000513, 0.000652, 0.000808, 0.000985, 0.001176, 0.001396, 0.001642, 0.001910, 0.002206, 0.002525, 0.002863, 0.003221, 0.003595, 0.003977, 0.004370, 0.004765, 0.005153, 0.005536, 0.005903, 0.006250, 0.006572, 0.006867, 0.007133, 0.007368, 0.007568, 0.007736, 0.007876, 0.007990, 0.008079, 0.008151, 0.008206, 0.008245, 0.008276, 0.008297, 0.008312, 0.008325, 0.008333, 0.008338, 0.008341, 0.008346, 0.008346, 0.008346, 0.008348, 0.008348, 0.008349, 0.008350, 0.008350, 0.008350, 0.008350, 0.008349, 0.008348, 0.008348, 0.008348, 0.008349, 0.008349, 0.008349, 0.008348, 0.008347, 0.008348, 0.008349, 0.008349, 0.008348, 0.008348, 0.008347, 0.008348, 0.008348, 0.008348, 0.008348],\n    },\n    SpectrumDataPoint {\n        xystar: (0.22224410586352497, 0.05599832200562286),\n        uv: (10.100587890625, 5.0),\n        spectrum: [0.000005, 0.000005, 0.000004, 0.000003, 0.000002, 0.000002, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000036, 0.000207, 0.000479, 0.000820, 0.001210, 0.001632, 0.002069, 0.002507, 0.002934, 0.003344, 0.003730, 0.004088, 0.004414, 0.004707, 0.004968, 0.005196, 0.005392, 0.005556, 0.005693, 0.005805, 0.005896, 0.005967, 0.006021, 0.006062, 0.006092, 0.006113, 0.006128, 0.006139, 0.006146, 0.006150, 0.006151, 0.006152, 0.006154, 0.006155, 0.006156, 0.006156, 0.006156, 0.006156, 0.006155, 0.006155, 0.006154, 0.006153, 0.006152, 0.006153, 0.006152, 0.006152, 0.006151, 0.006151, 0.006151, 0.006151, 0.006151, 0.006151, 0.006151, 0.006151, 0.006151, 0.006150, 0.006149, 0.006149, 0.006148, 0.006149, 0.006149, 0.006149, 0.006149, 0.006149, 0.006149, 0.006149, 0.006148, 0.006148, 0.006147, 0.006148],\n    },\n    SpectrumDataPoint {\n        xystar: (0.21679243249157734, 0.07092767152142272),\n        uv: (10.0, 5.266603515625),\n        spectrum: [0.000002, 0.000002, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000117, 0.000384, 0.000759, 0.001208, 0.001704, 0.002222, 0.002739, 0.003237, 0.003705, 0.004134, 0.004520, 0.004858, 0.005147, 0.005385, 0.005575, 0.005719, 0.005821, 0.005886, 0.005919, 0.005925, 0.005910, 0.005882, 0.005844, 0.005801, 0.005756, 0.005714, 0.005676, 0.005642, 0.005613, 0.005589, 0.005570, 0.005554, 0.005542, 0.005533, 0.005526, 0.005520, 0.005517, 0.005514, 0.005512, 0.005510, 0.005509, 0.005508, 0.005507, 0.005507, 0.005506, 0.005506, 0.005505, 0.005505, 0.005504, 0.005504, 0.005503, 0.005503, 0.005503, 0.005503, 0.005503, 0.005502, 0.005502, 0.005502, 0.005502, 0.005502, 0.005501, 0.005501, 0.005501, 0.005501, 0.005501, 0.005501, 0.005501, 0.005501, 0.005501],\n    },\n    SpectrumDataPoint {\n        xystar: (0.3045998248471417, -0.16799496601686853),\n        uv: (11.620119140625, 1.0),\n        spectrum: [0.000432, 0.000432, 0.000432, 0.000433, 0.000433, 0.000433, 0.000434, 0.000433, 0.000431, 0.000427, 0.000420, 0.000407, 0.000384, 0.000346, 0.000289, 0.000212, 0.000126, 0.000047, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000137, 0.001087, 0.002640, 0.004596, 0.006770, 0.009006, 0.011176, 0.013187, 0.014983, 0.016531, 0.017831, 0.018895, 0.019746, 0.020411, 0.020918, 0.021298, 0.021578, 0.021782, 0.021929, 0.022035, 0.022112, 0.022165, 0.022203, 0.022229, 0.022248, 0.022260, 0.022269, 0.022275, 0.022279, 0.022282, 0.022283, 0.022285, 0.022286, 0.022286, 0.022287, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022287, 0.022287, 0.022287, 0.022288],\n    },\n    SpectrumDataPoint {\n        xystar: (0.28401089510123756, -0.11199664401124569),\n        uv: (11.240236328125002, 1.9999999999999998),\n        spectrum: [0.000269, 0.000269, 0.000270, 0.000270, 0.000271, 0.000272, 0.000274, 0.000275, 0.000277, 0.000277, 0.000276, 0.000271, 0.000263, 0.000249, 0.000228, 0.000199, 0.000162, 0.000120, 0.000074, 0.000035, 0.000008, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000106, 0.000420, 0.000922, 0.001589, 0.002395, 0.003309, 0.004298, 0.005328, 0.006368, 0.007384, 0.008349, 0.009242, 0.010046, 0.010753, 0.011357, 0.011864, 0.012279, 0.012613, 0.012874, 0.013077, 0.013230, 0.013342, 0.013425, 0.013485, 0.013529, 0.013560, 0.013582, 0.013598, 0.013609, 0.013617, 0.013623, 0.013626, 0.013629, 0.013630, 0.013630, 0.013630, 0.013630, 0.013631, 0.013630, 0.013631, 0.013631, 0.013631, 0.013631, 0.013631, 0.013632, 0.013631, 0.013632, 0.013631, 0.013631, 0.013631, 0.013631, 0.013631, 0.013631, 0.013631, 0.013631, 0.013630, 0.013630],\n    },\n    SpectrumDataPoint {\n        xystar: (0.27099054061447164, -0.07661478235667343),\n        uv: (11.0, 2.6318378906249995),\n        spectrum: [0.000217, 0.000218, 0.000219, 0.000219, 0.000219, 0.000220, 0.000222, 0.000223, 0.000223, 0.000223, 0.000223, 0.000221, 0.000217, 0.000209, 0.000197, 0.000179, 0.000156, 0.000127, 0.000095, 0.000064, 0.000036, 0.000012, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000061, 0.000223, 0.000482, 0.000835, 0.001277, 0.001797, 0.002389, 0.003040, 0.003734, 0.004457, 0.005193, 0.005926, 0.006639, 0.007316, 0.007945, 0.008519, 0.009029, 0.009471, 0.009848, 0.010160, 0.010415, 0.010620, 0.010781, 0.010903, 0.010995, 0.011063, 0.011113, 0.011149, 0.011175, 0.011194, 0.011208, 0.011217, 0.011224, 0.011228, 0.011231, 0.011233, 0.011234, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011236, 0.011236, 0.011236, 0.011236, 0.011236, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011234, 0.011235, 0.011234, 0.011235, 0.011235],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.27705083121816254, -0.19599412701967994),\n        uv: (0.8881826171874998, 0.5000000000000004),\n        spectrum: [0.010364, 0.010365, 0.010364, 0.010364, 0.010364, 0.010363, 0.010362, 0.010360, 0.010355, 0.010347, 0.010332, 0.010304, 0.010254, 0.010166, 0.010018, 0.009790, 0.009470, 0.009054, 0.008545, 0.007947, 0.007269, 0.006521, 0.005719, 0.004886, 0.004046, 0.003221, 0.002435, 0.001712, 0.001075, 0.000554, 0.000183, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000010, 0.000072, 0.000164, 0.000270, 0.000377, 0.000477, 0.000566, 0.000644, 0.000708, 0.000758, 0.000798, 0.000828, 0.000851, 0.000867, 0.000879, 0.000887, 0.000893, 0.000895, 0.000898, 0.000900, 0.000902, 0.000903, 0.000904, 0.000904, 0.000904, 0.000904, 0.000904, 0.000904, 0.000903, 0.000903, 0.000903, 0.000903, 0.000904, 0.000904, 0.000903, 0.000903, 0.000903, 0.000903, 0.000902, 0.000903, 0.000903, 0.000903, 0.000903, 0.000902, 0.000903, 0.000903, 0.000903],\n    },\n    SpectrumDataPoint {\n        xystar: (0.29294238870336275, -0.19599412701967994),\n        uv: (11.405029785156252, 0.5000000000000004),\n        spectrum: [0.001850, 0.001850, 0.001849, 0.001849, 0.001849, 0.001847, 0.001843, 0.001837, 0.001826, 0.001806, 0.001773, 0.001712, 0.001606, 0.001429, 0.001158, 0.000804, 0.000422, 0.000109, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000753, 0.003311, 0.006908, 0.010950, 0.015008, 0.018799, 0.022162, 0.025033, 0.027397, 0.029286, 0.030753, 0.031867, 0.032696, 0.033302, 0.033742, 0.034060, 0.034288, 0.034449, 0.034562, 0.034642, 0.034699, 0.034740, 0.034768, 0.034788, 0.034801, 0.034811, 0.034817, 0.034822, 0.034825, 0.034827, 0.034829, 0.034830, 0.034830, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.2835080276937417, -0.13999580501405712),\n        uv: (0.7690419921874998, 1.5),\n        spectrum: [0.006873, 0.006873, 0.006873, 0.006873, 0.006874, 0.006875, 0.006877, 0.006880, 0.006882, 0.006887, 0.006895, 0.006915, 0.006949, 0.007001, 0.007082, 0.007199, 0.007349, 0.007518, 0.007697, 0.007860, 0.007985, 0.008034, 0.007989, 0.007829, 0.007538, 0.007119, 0.006569, 0.005900, 0.005122, 0.004258, 0.003321, 0.002359, 0.001441, 0.000665, 0.000138, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000002, 0.000001, 0.000002, 0.000001, 0.000000, 0.000003, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.28264795029433065, -0.13999580501405712),\n        uv: (11.2150888671875, 1.5),\n        spectrum: [0.000658, 0.000658, 0.000658, 0.000658, 0.000658, 0.000658, 0.000658, 0.000657, 0.000656, 0.000653, 0.000647, 0.000640, 0.000625, 0.000597, 0.000554, 0.000493, 0.000413, 0.000320, 0.000219, 0.000123, 0.000046, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000219, 0.000791, 0.001656, 0.002753, 0.004015, 0.005375, 0.006768, 0.008138, 0.009438, 0.010631, 0.011696, 0.012617, 0.013393, 0.014035, 0.014554, 0.014963, 0.015280, 0.015522, 0.015702, 0.015834, 0.015929, 0.015997, 0.016047, 0.016082, 0.016107, 0.016124, 0.016136, 0.016145, 0.016151, 0.016155, 0.016157, 0.016159, 0.016160, 0.016160, 0.016161, 0.016161, 0.016162, 0.016162, 0.016162, 0.016162, 0.016162, 0.016162, 0.016161, 0.016162, 0.016162, 0.016162, 0.016162, 0.016162, 0.016161, 0.016161, 0.016161, 0.016161, 0.016161, 0.016161],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.2828067338142219, -0.08399748300843428),\n        uv: (0.7819814453124998, 2.4999999999999996),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000034, 0.000173, 0.000488, 0.001070, 0.001979, 0.003225, 0.004773, 0.006546, 0.008433, 0.010292, 0.011964, 0.013297, 0.014166, 0.014492, 0.014239, 0.013408, 0.012042, 0.010207, 0.007997, 0.005554, 0.003125, 0.001099, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.24779758231348623, -0.08252094287808212),\n        uv: (10.572070703125, 2.5263675781249995),\n        spectrum: [0.000739, 0.000739, 0.000739, 0.000738, 0.000737, 0.000737, 0.000737, 0.000737, 0.000738, 0.000737, 0.000734, 0.000730, 0.000723, 0.000710, 0.000688, 0.000653, 0.000606, 0.000548, 0.000479, 0.000406, 0.000327, 0.000245, 0.000167, 0.000099, 0.000044, 0.000009, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000058, 0.000211, 0.000457, 0.000794, 0.001214, 0.001712, 0.002279, 0.002901, 0.003568, 0.004263, 0.004972, 0.005677, 0.006364, 0.007017, 0.007625, 0.008177, 0.008666, 0.009090, 0.009450, 0.009750, 0.009994, 0.010190, 0.010345, 0.010463, 0.010553, 0.010620, 0.010669, 0.010704, 0.010729, 0.010746, 0.010757, 0.010765, 0.010770, 0.010773, 0.010776, 0.010778, 0.010779, 0.010780, 0.010781, 0.010781, 0.010782, 0.010782, 0.010782, 0.010781, 0.010781, 0.010780, 0.010780, 0.010779, 0.010778, 0.010778, 0.010777, 0.010777, 0.010777, 0.010777, 0.010777, 0.010776, 0.010776, 0.010774, 0.010773, 0.010773, 0.010772],\n    },\n    SpectrumDataPoint {\n        xystar: (0.27533065877672697, -0.10020269012638827),\n        uv: (11.080078776041669, 2.210612630208333),\n        spectrum: [0.000362, 0.000362, 0.000362, 0.000362, 0.000361, 0.000360, 0.000359, 0.000358, 0.000356, 0.000353, 0.000350, 0.000346, 0.000340, 0.000328, 0.000310, 0.000284, 0.000248, 0.000206, 0.000158, 0.000107, 0.000060, 0.000022, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000155, 0.000462, 0.000913, 0.001492, 0.002182, 0.002961, 0.003806, 0.004695, 0.005600, 0.006498, 0.007365, 0.008180, 0.008928, 0.009598, 0.010184, 0.010683, 0.011100, 0.011440, 0.011712, 0.011927, 0.012093, 0.012219, 0.012312, 0.012380, 0.012430, 0.012465, 0.012490, 0.012508, 0.012520, 0.012529, 0.012534, 0.012538, 0.012541, 0.012543, 0.012544, 0.012545, 0.012546, 0.012547, 0.012547, 0.012547, 0.012547, 0.012547, 0.012547, 0.012547, 0.012547, 0.012546, 0.012546, 0.012546, 0.012545, 0.012545, 0.012545, 0.012545, 0.012545, 0.012545, 0.012544, 0.012544, 0.012544, 0.012544],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.2799353984961877, -0.02799916100281143),\n        uv: (0.8349599609374989, 3.4999999999999996),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001439, 0.004357, 0.008262, 0.012568, 0.016679, 0.020035, 0.022223, 0.023000, 0.022257, 0.020070, 0.016618, 0.012202, 0.007311, 0.002797, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.23497319844697023, -0.02799916100281143),\n        uv: (10.3354501953125, 3.4999999999999996),\n        spectrum: [0.000379, 0.000379, 0.000379, 0.000379, 0.000380, 0.000381, 0.000381, 0.000381, 0.000380, 0.000379, 0.000377, 0.000375, 0.000373, 0.000369, 0.000362, 0.000352, 0.000339, 0.000324, 0.000306, 0.000287, 0.000268, 0.000252, 0.000240, 0.000232, 0.000231, 0.000242, 0.000266, 0.000304, 0.000361, 0.000437, 0.000539, 0.000673, 0.000841, 0.001047, 0.001290, 0.001573, 0.001892, 0.002247, 0.002633, 0.003046, 0.003482, 0.003934, 0.004396, 0.004861, 0.005321, 0.005770, 0.006198, 0.006600, 0.006971, 0.007304, 0.007598, 0.007852, 0.008065, 0.008241, 0.008383, 0.008496, 0.008583, 0.008649, 0.008698, 0.008735, 0.008762, 0.008781, 0.008795, 0.008806, 0.008814, 0.008820, 0.008823, 0.008826, 0.008828, 0.008829, 0.008829, 0.008829, 0.008829, 0.008828, 0.008828, 0.008827, 0.008827, 0.008827, 0.008827, 0.008827, 0.008827, 0.008826, 0.008826, 0.008826, 0.008826, 0.008826, 0.008826, 0.008827, 0.008827, 0.008828, 0.008828, 0.008829, 0.008829, 0.008830, 0.008830],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.27542330013927685, 0.02799916100281143),\n        uv: (0.9182119140624989, 4.5),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000232, 0.005811, 0.014294, 0.023164, 0.030417, 0.034696, 0.035224, 0.031977, 0.025412, 0.016514, 0.007021, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.22467873357401813, 0.02799916100281143),\n        uv: (10.1455087890625, 4.5),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000005, 0.000042, 0.000109, 0.000206, 0.000331, 0.000476, 0.000641, 0.000822, 0.001012, 0.001217, 0.001434, 0.001657, 0.001892, 0.002139, 0.002397, 0.002664, 0.002939, 0.003220, 0.003507, 0.003797, 0.004088, 0.004380, 0.004667, 0.004949, 0.005224, 0.005485, 0.005731, 0.005964, 0.006177, 0.006367, 0.006538, 0.006687, 0.006813, 0.006919, 0.007007, 0.007076, 0.007131, 0.007174, 0.007208, 0.007230, 0.007247, 0.007262, 0.007272, 0.007278, 0.007282, 0.007286, 0.007288, 0.007289, 0.007290, 0.007291, 0.007291, 0.007292, 0.007291, 0.007291, 0.007292, 0.007292, 0.007292, 0.007293, 0.007293, 0.007295, 0.007296, 0.007296, 0.007296, 0.007295, 0.007295, 0.007295, 0.007296, 0.007296, 0.007295, 0.007295, 0.007294, 0.007294, 0.007294, 0.007293, 0.007293, 0.007294],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.2721197031522586, 0.06184974562556067),\n        uv: (0.9791660156249993, 5.104492838541667),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005774, 0.019422, 0.033963, 0.044428, 0.047728, 0.043253, 0.032054, 0.016954, 0.003193, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.24745884413771813, 0.08190850497983466),\n        uv: (1.4341792968749996, 5.462695703124999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000077, 0.001801, 0.004814, 0.008664, 0.012863, 0.016896, 0.020328, 0.022837, 0.024195, 0.024286, 0.023057, 0.020511, 0.016733, 0.012028, 0.007001, 0.002584, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.19209632353562106, 0.08138352071103196),\n        uv: (9.544336328125002, 5.453320703125),\n        spectrum: [0.000004, 0.000004, 0.000002, 0.000002, 0.000002, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000009, 0.000083, 0.000221, 0.000415, 0.000658, 0.000937, 0.001241, 0.001564, 0.001897, 0.002233, 0.002573, 0.002906, 0.003230, 0.003540, 0.003830, 0.004099, 0.004342, 0.004559, 0.004750, 0.004914, 0.005052, 0.005166, 0.005257, 0.005327, 0.005378, 0.005412, 0.005431, 0.005439, 0.005438, 0.005432, 0.005423, 0.005410, 0.005398, 0.005385, 0.005375, 0.005366, 0.005360, 0.005357, 0.005354, 0.005354, 0.005354, 0.005352, 0.005354, 0.005354, 0.005355, 0.005355, 0.005354, 0.005353, 0.005350, 0.005349, 0.005348, 0.005346, 0.005346, 0.005345, 0.005344, 0.005344, 0.005344, 0.005343, 0.005343, 0.005343, 0.005344, 0.005344, 0.005344, 0.005344, 0.005343, 0.005342, 0.005342, 0.005341, 0.005339, 0.005339, 0.005338, 0.005337, 0.005336, 0.005337, 0.005337, 0.005337],\n    },\n    SpectrumDataPoint {\n        xystar: (0.21860965694889323, 0.060974771844222814),\n        uv: (10.033529296875003, 5.088867838541667),\n        spectrum: [0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000013, 0.000157, 0.000402, 0.000712, 0.001070, 0.001464, 0.001876, 0.002296, 0.002711, 0.003113, 0.003495, 0.003852, 0.004182, 0.004483, 0.004753, 0.004991, 0.005199, 0.005376, 0.005526, 0.005649, 0.005748, 0.005827, 0.005888, 0.005935, 0.005970, 0.005995, 0.006012, 0.006024, 0.006032, 0.006037, 0.006040, 0.006042, 0.006043, 0.006043, 0.006043, 0.006043, 0.006043, 0.006043, 0.006042, 0.006041, 0.006041, 0.006040, 0.006040, 0.006040, 0.006040, 0.006039, 0.006039, 0.006039, 0.006039, 0.006038, 0.006038, 0.006037, 0.006037, 0.006037, 0.006036, 0.006036, 0.006036, 0.006036, 0.006035, 0.006035, 0.006035, 0.006035, 0.006035, 0.006034, 0.006034, 0.006034, 0.006034, 0.006034, 0.006034, 0.006034, 0.006034],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.23567449232649001, 0.1399958050140571),\n        uv: (1.6516103515625007, 6.499999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000797, 0.004422, 0.009859, 0.015976, 0.021790, 0.026531, 0.029634, 0.030784, 0.029806, 0.026660, 0.021483, 0.014816, 0.007770, 0.002072, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.17697751780667584, 0.1399958050140571),\n        uv: (9.2653818359375, 6.499999999999998),\n        spectrum: [0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000111, 0.000395, 0.000807, 0.001310, 0.001869, 0.002460, 0.003063, 0.003657, 0.004226, 0.004748, 0.005211, 0.005607, 0.005930, 0.006175, 0.006343, 0.006436, 0.006456, 0.006411, 0.006305, 0.006148, 0.005947, 0.005715, 0.005463, 0.005200, 0.004937, 0.004684, 0.004446, 0.004229, 0.004038, 0.003872, 0.003733, 0.003618, 0.003524, 0.003450, 0.003392, 0.003346, 0.003312, 0.003287, 0.003268, 0.003255, 0.003245, 0.003238, 0.003233, 0.003231, 0.003229, 0.003227, 0.003227, 0.003225, 0.003225, 0.003224, 0.003223, 0.003222, 0.003222, 0.003222, 0.003222, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003222, 0.003222, 0.003222, 0.003223, 0.003223, 0.003223],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.22802641945172614, 0.19599412701967997),\n        uv: (1.7927236328124998, 7.499999999999999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002041, 0.010218, 0.020912, 0.031206, 0.038913, 0.042767, 0.042066, 0.036661, 0.027065, 0.015069, 0.004227, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.14039640945901638, 0.2002268345612464),\n        uv: (8.590430078125001, 7.5755863281249995),\n        spectrum: [0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000143, 0.000491, 0.000993, 0.001601, 0.002283, 0.003006, 0.003742, 0.004473, 0.005169, 0.005807, 0.006372, 0.006849, 0.007223, 0.007492, 0.007649, 0.007696, 0.007640, 0.007490, 0.007245, 0.006919, 0.006527, 0.006082, 0.005603, 0.005104, 0.004605, 0.004110, 0.003639, 0.003207, 0.002818, 0.002479, 0.002188, 0.001941, 0.001740, 0.001579, 0.001451, 0.001355, 0.001281, 0.001226, 0.001184, 0.001156, 0.001135, 0.001119, 0.001108, 0.001101, 0.001096, 0.001089, 0.001086, 0.001083, 0.001081, 0.001081, 0.001080, 0.001078, 0.001079, 0.001077, 0.001076, 0.001077, 0.001076, 0.001076, 0.001075, 0.001075, 0.001075, 0.001073, 0.001072, 0.001073, 0.001073, 0.001072, 0.001073, 0.001071, 0.001071, 0.001071, 0.001071, 0.001071, 0.001071],\n    },\n    SpectrumDataPoint {\n        xystar: (0.16873398908970627, 0.1843825322537497),\n        uv: (9.113281901041667, 7.292643880208333),\n        spectrum: [0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000206, 0.000724, 0.001454, 0.002319, 0.003255, 0.004201, 0.005103, 0.005918, 0.006620, 0.007190, 0.007619, 0.007901, 0.008036, 0.008031, 0.007895, 0.007640, 0.007282, 0.006841, 0.006337, 0.005793, 0.005230, 0.004669, 0.004128, 0.003622, 0.003163, 0.002758, 0.002410, 0.002118, 0.001878, 0.001684, 0.001531, 0.001412, 0.001321, 0.001252, 0.001202, 0.001165, 0.001138, 0.001119, 0.001105, 0.001094, 0.001087, 0.001084, 0.001081, 0.001078, 0.001076, 0.001075, 0.001073, 0.001072, 0.001071, 0.001071, 0.001070, 0.001070, 0.001070, 0.001070, 0.001069, 0.001069, 0.001069, 0.001069, 0.001069, 0.001069, 0.001069, 0.001069, 0.001070, 0.001070, 0.001070, 0.001070, 0.001069, 0.001070, 0.001069],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.22157361598686395, 0.23872204979891953),\n        uv: (1.9117832031249993, 8.263021484374999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.012982, 0.030813, 0.046441, 0.055671, 0.056186, 0.047435, 0.030933, 0.011783, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.19434046395008459, 0.2552298738905974),\n        uv: (2.414257421875, 8.557812890625),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003610, 0.009609, 0.016594, 0.023384, 0.029045, 0.032940, 0.034601, 0.033707, 0.030102, 0.024014, 0.016229, 0.008185, 0.001926, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.12881318343965062, 0.2519924490253028),\n        uv: (8.3767099609375, 8.5),\n        spectrum: [0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000223, 0.000771, 0.001548, 0.002474, 0.003482, 0.004523, 0.005548, 0.006511, 0.007370, 0.008094, 0.008660, 0.009056, 0.009280, 0.009329, 0.009211, 0.008936, 0.008519, 0.007977, 0.007332, 0.006610, 0.005837, 0.005042, 0.004250, 0.003487, 0.002776, 0.002133, 0.001574, 0.001103, 0.000725, 0.000436, 0.000227, 0.000092, 0.000019, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001, 0.000001, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.1826672606027804, 0.30799077103092565),\n        uv: (2.6296376953125, 9.499999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002962, 0.011153, 0.021421, 0.031233, 0.038841, 0.042985, 0.042828, 0.037973, 0.028876, 0.017218, 0.006112, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.11775126488722393, 0.30799077103092565),\n        uv: (8.1726083984375, 9.499999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000475, 0.001595, 0.003128, 0.004882, 0.006678, 0.008357, 0.009801, 0.010941, 0.011735, 0.012161, 0.012216, 0.011913, 0.011282, 0.010366, 0.009216, 0.007898, 0.006484, 0.005049, 0.003670, 0.002426, 0.001386, 0.000607, 0.000135, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.17168473381029942, 0.36398909303654853),\n        uv: (2.8322744140624994, 10.5),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.009996, 0.028024, 0.045931, 0.057966, 0.060467, 0.051817, 0.033707, 0.012510, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.08319200002618024, 0.3618235548020819),\n        uv: (7.534961328125, 10.461328515625),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000791, 0.002248, 0.004114, 0.006181, 0.008270, 0.010216, 0.011870, 0.013125, 0.013927, 0.014253, 0.014106, 0.013510, 0.012508, 0.011164, 0.009558, 0.007783, 0.005948, 0.004167, 0.002563, 0.001259, 0.000370, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.1108485747826697, 0.34171375531056314),\n        uv: (8.045248046875, 10.102214192708333),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000363, 0.002276, 0.005030, 0.008008, 0.010757, 0.013016, 0.014633, 0.015515, 0.015626, 0.014996, 0.013707, 0.011880, 0.009672, 0.007274, 0.004894, 0.002759, 0.001090, 0.000105, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.16472911586578143, 0.39709230422107167),\n        uv: (2.9606113281249997, 11.091146484374999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.013110, 0.043196, 0.069841, 0.079880, 0.067008, 0.034892, 0.001225, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.13639506475775579, 0.41745001295063605),\n        uv: (3.4833980468749997, 11.454687890624998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005873, 0.018390, 0.032485, 0.044330, 0.051106, 0.050858, 0.043004, 0.028959, 0.012614, 0.000245, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.06524684764317006, 0.4199874150421713),\n        uv: (7.203858398437501, 11.499999999999998),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001350, 0.004395, 0.008317, 0.012364, 0.015896, 0.018472, 0.019892, 0.020103, 0.019145, 0.017144, 0.014311, 0.010938, 0.007382, 0.004059, 0.001434, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000002, 0.000000, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.11892889168380434, 0.4650668298587564),\n        uv: (3.8056634114583328, 12.305013671874999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002157, 0.021387, 0.044337, 0.061319, 0.065705, 0.054910, 0.032189, 0.008070, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.08570077964046263, 0.480634057135496),\n        uv: (4.418749609375, 12.583008203124997),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005126, 0.016493, 0.029500, 0.040588, 0.046974, 0.046832, 0.039777, 0.027361, 0.013107, 0.002029, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001],\n    },\n    SpectrumDataPoint {\n        xystar: (0.02647452672101075, 0.47289053917065604),\n        uv: (6.488476953125001, 12.444726953124999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003506, 0.009139, 0.015455, 0.021171, 0.025254, 0.027108, 0.026619, 0.024002, 0.019671, 0.014239, 0.008506, 0.003444, 0.000186, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.056738679726381684, 0.45216096658402294),\n        uv: (7.046875651041668, 12.074544921874997),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.004328, 0.011605, 0.018940, 0.024427, 0.027217, 0.027102, 0.024261, 0.019237, 0.012936, 0.006584, 0.001689, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.07057048857024528, 0.5168178833008018),\n        uv: (4.697916015625, 13.229167317708333),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000195, 0.017901, 0.040081, 0.056786, 0.061148, 0.050847, 0.029751, 0.007922, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (-0.027099054061447154, 0.5199805671765496),\n        uv: (5.5, 13.285645507812498),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005899, 0.019086, 0.033309, 0.043457, 0.046146, 0.040597, 0.028764, 0.014526, 0.002900, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],\n    },\n    SpectrumDataPoint {\n        xystar: (0.00799213911975501, 0.5124794716350015),\n        uv: (6.147461588541667, 13.151693359374999),\n        spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003433, 0.016112, 0.030274, 0.039955, 0.042108, 0.036770, 0.026016, 0.013147, 0.002607, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000002, 0.000000],\n    }\n];\n\n// Color matching functions.\npub(crate) const CMF_WAVELENGTH: [f32; 95] = [\n    360.0, 365.0, 370.0, 375.0, 380.0, 385.0, 390.0, 395.0, 400.0, 405.0, 410.0, 415.0, 420.0, 425.0, 430.0, 435.0, 440.0, 445.0, 450.0, 455.0, 460.0, 465.0, 470.0, 475.0, 480.0, 485.0, 490.0, 495.0, 500.0, 505.0, 510.0, 515.0, 520.0, 525.0, 530.0, 535.0, 540.0, 545.0, 550.0, 555.0, 560.0, 565.0, 570.0, 575.0, 580.0, 585.0, 590.0, 595.0, 600.0, 605.0, 610.0, 615.0, 620.0, 625.0, 630.0, 635.0, 640.0, 645.0, 650.0, 655.0, 660.0, 665.0, 670.0, 675.0, 680.0, 685.0, 690.0, 695.0, 700.0, 705.0, 710.0, 715.0, 720.0, 725.0, 730.0, 735.0, 740.0, 745.0, 750.0, 755.0, 760.0, 765.0, 770.0, 775.0, 780.0, 785.0, 790.0, 795.0, 800.0, 805.0, 810.0, 815.0, 820.0, 825.0, 830.0\n];\npub(crate) const CMF_X: [f32; 95] = [\n    0.0001299, 0.0002321, 0.0004149, 0.0007416, 0.001368, 0.002236, 0.004243, 0.00765, 0.01431, 0.02319, 0.04351, 0.07763, 0.13438, 0.21477, 0.2839, 0.3285, 0.34828, 0.34806, 0.3362, 0.3187, 0.2908, 0.2511, 0.19536, 0.1421, 0.09564, 0.05795001, 0.03201, 0.0147, 0.0049, 0.0024, 0.0093, 0.0291, 0.06327, 0.1096, 0.1655, 0.2257499, 0.2904, 0.3597, 0.4334499, 0.5120501, 0.5945, 0.6784, 0.7621, 0.8425, 0.9163, 0.9786, 1.0263, 1.0567, 1.0622, 1.0456, 1.0026, 0.9384, 0.8544499, 0.7514, 0.6424, 0.5419, 0.4479, 0.3608, 0.2835, 0.2187, 0.1649, 0.1212, 0.0874, 0.0636, 0.04677, 0.0329, 0.0227, 0.01584, 0.01135916, 0.008110916, 0.005790346, 0.004109457, 0.002899327, 0.00204919, 0.001439971, 0.0009999493, 0.0006900786, 0.0004760213, 0.0003323011, 0.0002348261, 0.0001661505, 0.000117413, 8.307527e-05, 5.870652e-05, 4.150994e-05, 2.935326e-05, 2.067383e-05, 1.455977e-05, 1.025398e-05, 7.221456e-06, 5.085868e-06, 3.581652e-06, 2.522525e-06, 1.776509e-06, 1.251141e-06\n];\npub(crate) const CMF_Y: [f32; 95] = [\n    3.917e-06, 6.965e-06, 1.239e-05, 2.202e-05, 3.9e-05, 6.4e-05, 0.00012, 0.000217, 0.000396, 0.00064, 0.00121, 0.00218, 0.004, 0.0073, 0.0116, 0.01684, 0.023, 0.0298, 0.038, 0.048, 0.06, 0.0739, 0.09098, 0.1126, 0.13902, 0.1693, 0.20802, 0.2586, 0.323, 0.4073, 0.503, 0.6082, 0.71, 0.7932, 0.862, 0.9148501, 0.954, 0.9803, 0.9949501, 1.0, 0.995, 0.9786, 0.952, 0.9154, 0.87, 0.8163, 0.757, 0.6949, 0.631, 0.5668, 0.503, 0.4412, 0.381, 0.321, 0.265, 0.217, 0.175, 0.1382, 0.107, 0.0816, 0.061, 0.04458, 0.032, 0.0232, 0.017, 0.01192, 0.00821, 0.005723, 0.004102, 0.002929, 0.002091, 0.001484, 0.001047, 0.00074, 0.00052, 0.0003611, 0.0002492, 0.0001719, 0.00012, 8.48e-05, 6e-05, 4.24e-05, 3e-05, 2.12e-05, 1.499e-05, 1.06e-05, 7.4657e-06, 5.2578e-06, 3.7029e-06, 2.6078e-06, 1.8366e-06, 1.2934e-06, 9.1093e-07, 6.4153e-07, 4.5181e-07\n];\npub(crate) const CMF_Z: [f32; 95] = [\n    0.0006061, 0.001086, 0.001946, 0.003486, 0.006450001, 0.01054999, 0.02005001, 0.03621, 0.06785001, 0.1102, 0.2074, 0.3713, 0.6456, 1.0390501, 1.3856, 1.62296, 1.74706, 1.7826, 1.77211, 1.7441, 1.6692, 1.5281, 1.28764, 1.0419, 0.8129501, 0.6162, 0.46518, 0.3533, 0.272, 0.2123, 0.1582, 0.1117, 0.07824999, 0.05725001, 0.04216, 0.02984, 0.0203, 0.0134, 0.008749999, 0.005749999, 0.0039, 0.002749999, 0.0021, 0.0018, 0.001650001, 0.0014, 0.0011, 0.001, 0.0008, 0.0006, 0.00034, 0.00024, 0.00019, 0.0001, 4.999999e-05, 3e-05, 2e-05, 1e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0\n];\n\n"
  },
  {
    "path": "sub_crates/spectral_upsampling/src/meng/xyz_5nm_360_830.csv",
    "content": "360,0.000129900000,0.000003917000,0.000606100000\r\n365,0.000232100000,0.000006965000,0.001086000000\r\n370,0.000414900000,0.000012390000,0.001946000000\r\n375,0.000741600000,0.000022020000,0.003486000000\r\n380,0.001368000000,0.000039000000,0.006450001000\r\n385,0.002236000000,0.000064000000,0.010549990000\r\n390,0.004243000000,0.000120000000,0.020050010000\r\n395,0.007650000000,0.000217000000,0.036210000000\r\n400,0.014310000000,0.000396000000,0.067850010000\r\n405,0.023190000000,0.000640000000,0.110200000000\r\n410,0.043510000000,0.001210000000,0.207400000000\r\n415,0.077630000000,0.002180000000,0.371300000000\r\n420,0.134380000000,0.004000000000,0.645600000000\r\n425,0.214770000000,0.007300000000,1.039050100000\r\n430,0.283900000000,0.011600000000,1.385600000000\r\n435,0.328500000000,0.016840000000,1.622960000000\r\n440,0.348280000000,0.023000000000,1.747060000000\r\n445,0.348060000000,0.029800000000,1.782600000000\r\n450,0.336200000000,0.038000000000,1.772110000000\r\n455,0.318700000000,0.048000000000,1.744100000000\r\n460,0.290800000000,0.060000000000,1.669200000000\r\n465,0.251100000000,0.073900000000,1.528100000000\r\n470,0.195360000000,0.090980000000,1.287640000000\r\n475,0.142100000000,0.112600000000,1.041900000000\r\n480,0.095640000000,0.139020000000,0.812950100000\r\n485,0.057950010000,0.169300000000,0.616200000000\r\n490,0.032010000000,0.208020000000,0.465180000000\r\n495,0.014700000000,0.258600000000,0.353300000000\r\n500,0.004900000000,0.323000000000,0.272000000000\r\n505,0.002400000000,0.407300000000,0.212300000000\r\n510,0.009300000000,0.503000000000,0.158200000000\r\n515,0.029100000000,0.608200000000,0.111700000000\r\n520,0.063270000000,0.710000000000,0.078249990000\r\n525,0.109600000000,0.793200000000,0.057250010000\r\n530,0.165500000000,0.862000000000,0.042160000000\r\n535,0.225749900000,0.914850100000,0.029840000000\r\n540,0.290400000000,0.954000000000,0.020300000000\r\n545,0.359700000000,0.980300000000,0.013400000000\r\n550,0.433449900000,0.994950100000,0.008749999000\r\n555,0.512050100000,1.000000000000,0.005749999000\r\n560,0.594500000000,0.995000000000,0.003900000000\r\n565,0.678400000000,0.978600000000,0.002749999000\r\n570,0.762100000000,0.952000000000,0.002100000000\r\n575,0.842500000000,0.915400000000,0.001800000000\r\n580,0.916300000000,0.870000000000,0.001650001000\r\n585,0.978600000000,0.816300000000,0.001400000000\r\n590,1.026300000000,0.757000000000,0.001100000000\r\n595,1.056700000000,0.694900000000,0.001000000000\r\n600,1.062200000000,0.631000000000,0.000800000000\r\n605,1.045600000000,0.566800000000,0.000600000000\r\n610,1.002600000000,0.503000000000,0.000340000000\r\n615,0.938400000000,0.441200000000,0.000240000000\r\n620,0.854449900000,0.381000000000,0.000190000000\r\n625,0.751400000000,0.321000000000,0.000100000000\r\n630,0.642400000000,0.265000000000,0.000049999990\r\n635,0.541900000000,0.217000000000,0.000030000000\r\n640,0.447900000000,0.175000000000,0.000020000000\r\n645,0.360800000000,0.138200000000,0.000010000000\r\n650,0.283500000000,0.107000000000,0.000000000000\r\n655,0.218700000000,0.081600000000,0.000000000000\r\n660,0.164900000000,0.061000000000,0.000000000000\r\n665,0.121200000000,0.044580000000,0.000000000000\r\n670,0.087400000000,0.032000000000,0.000000000000\r\n675,0.063600000000,0.023200000000,0.000000000000\r\n680,0.046770000000,0.017000000000,0.000000000000\r\n685,0.032900000000,0.011920000000,0.000000000000\r\n690,0.022700000000,0.008210000000,0.000000000000\r\n695,0.015840000000,0.005723000000,0.000000000000\r\n700,0.011359160000,0.004102000000,0.000000000000\r\n705,0.008110916000,0.002929000000,0.000000000000\r\n710,0.005790346000,0.002091000000,0.000000000000\r\n715,0.004109457000,0.001484000000,0.000000000000\r\n720,0.002899327000,0.001047000000,0.000000000000\r\n725,0.002049190000,0.000740000000,0.000000000000\r\n730,0.001439971000,0.000520000000,0.000000000000\r\n735,0.000999949300,0.000361100000,0.000000000000\r\n740,0.000690078600,0.000249200000,0.000000000000\r\n745,0.000476021300,0.000171900000,0.000000000000\r\n750,0.000332301100,0.000120000000,0.000000000000\r\n755,0.000234826100,0.000084800000,0.000000000000\r\n760,0.000166150500,0.000060000000,0.000000000000\r\n765,0.000117413000,0.000042400000,0.000000000000\r\n770,0.000083075270,0.000030000000,0.000000000000\r\n775,0.000058706520,0.000021200000,0.000000000000\r\n780,0.000041509940,0.000014990000,0.000000000000\r\n785,0.000029353260,0.000010600000,0.000000000000\r\n790,0.000020673830,0.000007465700,0.000000000000\r\n795,0.000014559770,0.000005257800,0.000000000000\r\n800,0.000010253980,0.000003702900,0.000000000000\r\n805,0.000007221456,0.000002607800,0.000000000000\r\n810,0.000005085868,0.000001836600,0.000000000000\r\n815,0.000003581652,0.000001293400,0.000000000000\r\n820,0.000002522525,0.000000910930,0.000000000000\r\n825,0.000001776509,0.000000641530,0.000000000000\r\n830,0.000001251141,0.000000451810,0.000000000000"
  },
  {
    "path": "sub_crates/spectral_upsampling/src/meng/xyz_5nm_380_780.csv",
    "content": "380,0.001368,0.000039,0.006450\n385,0.002236,0.000064,0.010550\n390,0.004243,0.000120,0.020050\n395,0.007650,0.000217,0.036210\n400,0.014310,0.000396,0.067850\n405,0.023190,0.000640,0.110200\n410,0.043510,0.001210,0.207400\n415,0.077630,0.002180,0.371300\n420,0.134380,0.004000,0.645600\n425,0.214770,0.007300,1.039050\n430,0.283900,0.011600,1.385600\n435,0.328500,0.016840,1.622960\n440,0.348280,0.023000,1.747060\n445,0.348060,0.029800,1.782600\n450,0.336200,0.038000,1.772110\n455,0.318700,0.048000,1.744100\n460,0.290800,0.060000,1.669200\n465,0.251100,0.073900,1.528100\n470,0.195360,0.090980,1.287640\n475,0.142100,0.112600,1.041900\n480,0.095640,0.139020,0.812950\n485,0.057950,0.169300,0.616200\n490,0.032010,0.208020,0.465180\n495,0.014700,0.258600,0.353300\n500,0.004900,0.323000,0.272000\n505,0.002400,0.407300,0.212300\n510,0.009300,0.503000,0.158200\n515,0.029100,0.608200,0.111700\n520,0.063270,0.710000,0.078250\n525,0.109600,0.793200,0.057250\n530,0.165500,0.862000,0.042160\n535,0.225750,0.914850,0.029840\n540,0.290400,0.954000,0.020300\n545,0.359700,0.980300,0.013400\n550,0.433450,0.994950,0.008750\n555,0.512050,1.000000,0.005750\n560,0.594500,0.995000,0.003900\n565,0.678400,0.978600,0.002750\n570,0.762100,0.952000,0.002100\n575,0.842500,0.915400,0.001800\n580,0.916300,0.870000,0.001650\n585,0.978600,0.816300,0.001400\n590,1.026300,0.757000,0.001100\n595,1.056700,0.694900,0.001000\n600,1.062200,0.631000,0.000800\n605,1.045600,0.566800,0.000600\n610,1.002600,0.503000,0.000340\n615,0.938400,0.441200,0.000240\n620,0.854450,0.381000,0.000190\n625,0.751400,0.321000,0.000100\n630,0.642400,0.265000,0.000050\n635,0.541900,0.217000,0.000030\n640,0.447900,0.175000,0.000020\n645,0.360800,0.138200,0.000010\n650,0.283500,0.107000,0.000000\n655,0.218700,0.081600,0.000000\n660,0.164900,0.061000,0.000000\n665,0.121200,0.044580,0.000000\n670,0.087400,0.032000,0.000000\n675,0.063600,0.023200,0.000000\n680,0.046770,0.017000,0.000000\n685,0.032900,0.011920,0.000000\n690,0.022700,0.008210,0.000000\n695,0.015840,0.005723,0.000000\n700,0.011359,0.004102,0.000000\n705,0.008111,0.002929,0.000000\n710,0.005790,0.002091,0.000000\n715,0.004109,0.001484,0.000000\n720,0.002899,0.001047,0.000000\n725,0.002049,0.000740,0.000000\n730,0.001440,0.000520,0.000000\n735,0.001000,0.000361,0.000000\n740,0.000690,0.000249,0.000000\n745,0.000476,0.000172,0.000000\n750,0.000332,0.000120,0.000000\n755,0.000235,0.000085,0.000000\n760,0.000166,0.000060,0.000000\n765,0.000117,0.000042,0.000000\n770,0.000083,0.000030,0.000000\n775,0.000059,0.000021,0.000000\n780,0.000042,0.000015,0.000000\n"
  },
  {
    "path": "sub_crates/spectral_upsampling/src/meng.rs",
    "content": "// Since this is basicallt translated from C, silence a bunch of\n// clippy warnings that stem from the C code.\n#![allow(clippy::needless_return)]\n#![allow(clippy::useless_let_if_seq)]\n#![allow(clippy::cognitive_complexity)]\n\nuse std::f32;\n\nuse glam::Vec4;\n\nmod meng_spectra_tables;\n\npub use self::meng_spectra_tables::{\n    EQUAL_ENERGY_REFLECTANCE, SPECTRUM_SAMPLE_MAX, SPECTRUM_SAMPLE_MIN,\n};\n\nuse self::meng_spectra_tables::{\n    SPECTRUM_DATA_POINTS,\n    // CMF_X,\n    // CMF_Y,\n    // CMF_Z,\n    // SPECTRUM_MAT_UV_TO_XY,\n    SPECTRUM_GRID,\n    SPECTRUM_GRID_HEIGHT,\n    SPECTRUM_GRID_WIDTH,\n    // SPECTRUM_MAT_XY_TO_XYSTAR,\n    // SPECTRUM_MAT_XYSTAR_TO_XY,\n    SPECTRUM_MAT_XY_TO_UV,\n    // SPECTRUM_BIN_SIZE,\n    SPECTRUM_NUM_SAMPLES,\n};\n\n/// Evaluate the spectrum for xyz at the given wavelength.\n#[inline]\npub fn spectrum_xyz_to_p(lambda: f32, xyz: (f32, f32, f32)) -> f32 {\n    assert!(lambda >= SPECTRUM_SAMPLE_MIN);\n    assert!(lambda <= SPECTRUM_SAMPLE_MAX);\n\n    let inv_norm = xyz.0 + xyz.1 + xyz.2;\n    let norm = {\n        let norm = 1.0 / inv_norm;\n        if norm < f32::MAX {\n            norm\n        } else {\n            return 0.0;\n        }\n    };\n\n    let xyy = (xyz.0 * norm, xyz.1 * norm, xyz.1);\n\n    // Rotate to align with grid\n    let uv = spectrum_xy_to_uv((xyy.0, xyy.1));\n    if uv.0 < 0.0\n        || uv.0 >= SPECTRUM_GRID_WIDTH as f32\n        || uv.1 < 0.0\n        || uv.1 >= SPECTRUM_GRID_HEIGHT as f32\n    {\n        return 0.0;\n    }\n\n    let uvi = (uv.0 as i32, uv.1 as i32);\n    debug_assert!(uvi.0 < SPECTRUM_GRID_WIDTH);\n    debug_assert!(uvi.1 < SPECTRUM_GRID_HEIGHT);\n\n    let cell_idx: i32 = uvi.0 + SPECTRUM_GRID_WIDTH * uvi.1;\n    debug_assert!(cell_idx >= 0);\n\n    let cell = &SPECTRUM_GRID[cell_idx as usize];\n    let inside: bool = cell.inside;\n    let idx = &cell.idx;\n    let num: i32 = cell.num_points;\n\n    // If the cell has no points, nothing we can do, so return 0.0\n    if num == 0 {\n        return 0.0;\n    }\n\n    // Normalize lambda to spectrum table index range.\n    let sb: f32 = (lambda - SPECTRUM_SAMPLE_MIN) / (SPECTRUM_SAMPLE_MAX - SPECTRUM_SAMPLE_MIN)\n        * (SPECTRUM_NUM_SAMPLES as f32 - 1.0);\n    debug_assert!(sb >= 0.0);\n    debug_assert!(sb <= SPECTRUM_NUM_SAMPLES as f32);\n\n    // Get the spectral values for the vertices of the grid cell.\n    let mut p = [0.0f32; 6];\n    let sb0: i32 = sb as i32;\n    let sb1: i32 = if (sb + 1.0) < SPECTRUM_NUM_SAMPLES as f32 {\n        sb as i32 + 1\n    } else {\n        SPECTRUM_NUM_SAMPLES - 1\n    };\n    assert!(sb0 < SPECTRUM_NUM_SAMPLES);\n    let sbf: f32 = sb as f32 - sb0 as f32;\n    for i in 0..(num as usize) {\n        debug_assert!(idx[i] >= 0);\n        let spectrum = &SPECTRUM_DATA_POINTS[idx[i] as usize].spectrum;\n        p[i] = spectrum[sb0 as usize] * (1.0 - sbf) + spectrum[sb1 as usize] * sbf;\n    }\n\n    // Linearly interpolated the spectral power of the cell vertices.\n    let mut interpolated_p: f32 = 0.0;\n    if inside {\n        // Fast path for normal inner quads:\n        let uv2 = (uv.0 - uvi.0 as f32, uv.1 - uvi.1 as f32);\n\n        assert!(uv2.0 >= 0.0 && uv2.0 <= 1.0);\n        assert!(uv2.1 >= 0.0 && uv2.1 <= 1.0);\n\n        // The layout of the vertices in the quad is:\n        //  2  3\n        //  0  1\n        interpolated_p = p[0] * (1.0 - uv2.0) * (1.0 - uv2.1)\n            + p[2] * (1.0 - uv2.0) * uv2.1\n            + p[3] * uv2.0 * uv2.1\n            + p[1] * uv2.0 * (1.0 - uv2.1);\n    } else {\n        // Need to go through triangulation :(\n        // We get the indices in such an order that they form a triangle fan around idx[0].\n        // compute barycentric coordinates of our xy* point for all triangles in the fan:\n        let ex: f32 = uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0;\n        let ey: f32 = uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1;\n        let mut e0x: f32 =\n            SPECTRUM_DATA_POINTS[idx[1] as usize].uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0;\n        let mut e0y: f32 =\n            SPECTRUM_DATA_POINTS[idx[1] as usize].uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1;\n        let mut uu: f32 = e0x * ey - ex * e0y;\n\n        for i in 0..(num as usize - 1) {\n            let (e1x, e1y): (f32, f32) = if i as i32 == (num - 2) {\n                // Close the circle\n                (\n                    SPECTRUM_DATA_POINTS[idx[1] as usize].uv.0\n                        - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0,\n                    SPECTRUM_DATA_POINTS[idx[1] as usize].uv.1\n                        - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1,\n                )\n            } else {\n                (\n                    SPECTRUM_DATA_POINTS[idx[i + 2] as usize].uv.0\n                        - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0,\n                    SPECTRUM_DATA_POINTS[idx[i + 2] as usize].uv.1\n                        - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1,\n                )\n            };\n\n            let vv: f32 = ex * e1y - e1x * ey;\n            let area: f32 = e0x * e1y - e1x * e0y;\n\n            // Normalise\n            let u: f32 = uu / area;\n            let v: f32 = vv / area;\n            let w: f32 = 1.0 - u - v;\n            // Outside spectral locus (quantized version at least) or outside grid\n            if u < 0.0 || v < 0.0 || w < 0.0 {\n                uu = -vv;\n                e0x = e1x;\n                e0y = e1y;\n                continue;\n            }\n\n            // This seems to be the triangle we've been looking for.\n            interpolated_p =\n                p[0] * w + p[i + 1] * v + p[if i as i32 == (num - 2) { 1 } else { i + 2 }] * u;\n            break;\n        }\n    }\n\n    // Now we have a spectrum which corresponds to the xy chromaticities of\n    // the input. need to scale according to the input brightness X+Y+Z now:\n    return interpolated_p * inv_norm;\n}\n\n/// Evaluate the spectrum for xyz at the given wavelengths.\n///\n/// Works on 4 wavelengths at once via SIMD.\n#[inline]\npub fn spectrum_xyz_to_p_4(lambdas: Vec4, xyz: (f32, f32, f32)) -> Vec4 {\n    assert!(lambdas.min_element() >= SPECTRUM_SAMPLE_MIN);\n    assert!(lambdas.max_element() <= SPECTRUM_SAMPLE_MAX);\n\n    let inv_norm = xyz.0 + xyz.1 + xyz.2;\n    let norm = {\n        let norm = 1.0 / inv_norm;\n        if norm < f32::MAX {\n            norm\n        } else {\n            return Vec4::splat(0.0);\n        }\n    };\n\n    let xyy = (xyz.0 * norm, xyz.1 * norm, xyz.1);\n\n    // Rotate to align with grid\n    let uv = spectrum_xy_to_uv((xyy.0, xyy.1));\n    if uv.0 < 0.0\n        || uv.0 >= SPECTRUM_GRID_WIDTH as f32\n        || uv.1 < 0.0\n        || uv.1 >= SPECTRUM_GRID_HEIGHT as f32\n    {\n        return Vec4::splat(0.0);\n    }\n\n    let uvi = (uv.0 as i32, uv.1 as i32);\n    debug_assert!(uvi.0 < SPECTRUM_GRID_WIDTH);\n    debug_assert!(uvi.1 < SPECTRUM_GRID_HEIGHT);\n\n    let cell_idx: i32 = uvi.0 + SPECTRUM_GRID_WIDTH * uvi.1;\n    debug_assert!(cell_idx >= 0);\n\n    let cell = &SPECTRUM_GRID[cell_idx as usize];\n    let inside: bool = cell.inside;\n    let idx = &cell.idx;\n    let num: i32 = cell.num_points;\n\n    // If the cell has no points, nothing we can do, so return 0.0\n    if num == 0 {\n        return Vec4::splat(0.0);\n    }\n\n    // Normalize lambda to spectrum table index range.\n    let sb: Vec4 = (lambdas - Vec4::splat(SPECTRUM_SAMPLE_MIN))\n        / (SPECTRUM_SAMPLE_MAX - SPECTRUM_SAMPLE_MIN)\n        * (SPECTRUM_NUM_SAMPLES as f32 - 1.0);\n    debug_assert!(sb.min_element() >= 0.0);\n    debug_assert!(sb.max_element() <= SPECTRUM_NUM_SAMPLES as f32);\n\n    // Get the spectral values for the vertices of the grid cell.\n    // TODO: use integer SIMD intrinsics to make this part faster.\n    let mut p = [Vec4::splat(0.0); 6];\n    let sb0: [i32; 4] = [sb[0] as i32, sb[1] as i32, sb[2] as i32, sb[3] as i32];\n    assert!(sb0[0].max(sb0[1]).max(sb0[2].max(sb0[3])) < SPECTRUM_NUM_SAMPLES);\n    let sb1: [i32; 4] = [\n        (sb[0] as i32 + 1).min(SPECTRUM_NUM_SAMPLES - 1),\n        (sb[1] as i32 + 1).min(SPECTRUM_NUM_SAMPLES - 1),\n        (sb[2] as i32 + 1).min(SPECTRUM_NUM_SAMPLES - 1),\n        (sb[3] as i32 + 1).min(SPECTRUM_NUM_SAMPLES - 1),\n    ];\n    let sbf = sb - Vec4::new(sb0[0] as f32, sb0[1] as f32, sb0[2] as f32, sb0[3] as f32);\n    for i in 0..(num as usize) {\n        debug_assert!(idx[i] >= 0);\n        let spectrum = &SPECTRUM_DATA_POINTS[idx[i] as usize].spectrum;\n        let p0 = Vec4::new(\n            spectrum[sb0[0] as usize],\n            spectrum[sb0[1] as usize],\n            spectrum[sb0[2] as usize],\n            spectrum[sb0[3] as usize],\n        );\n        let p1 = Vec4::new(\n            spectrum[sb1[0] as usize],\n            spectrum[sb1[1] as usize],\n            spectrum[sb1[2] as usize],\n            spectrum[sb1[3] as usize],\n        );\n        p[i] = p0 * (Vec4::splat(1.0) - sbf) + p1 * sbf;\n    }\n\n    // Linearly interpolate the spectral power of the cell vertices.\n    let mut interpolated_p = Vec4::splat(0.0);\n    if inside {\n        // Fast path for normal inner quads:\n        let uv2 = (uv.0 - uvi.0 as f32, uv.1 - uvi.1 as f32);\n\n        assert!(uv2.0 >= 0.0 && uv2.0 <= 1.0);\n        assert!(uv2.1 >= 0.0 && uv2.1 <= 1.0);\n\n        // The layout of the vertices in the quad is:\n        //  2  3\n        //  0  1\n        interpolated_p = p[0] * ((1.0 - uv2.0) * (1.0 - uv2.1))\n            + p[2] * ((1.0 - uv2.0) * uv2.1)\n            + p[3] * (uv2.0 * uv2.1)\n            + p[1] * (uv2.0 * (1.0 - uv2.1));\n    } else {\n        // Need to go through triangulation :(\n        // We get the indices in such an order that they form a triangle fan around idx[0].\n        // compute barycentric coordinates of our xy* point for all triangles in the fan:\n        let ex: f32 = uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0;\n        let ey: f32 = uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1;\n        let mut e0x: f32 =\n            SPECTRUM_DATA_POINTS[idx[1] as usize].uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0;\n        let mut e0y: f32 =\n            SPECTRUM_DATA_POINTS[idx[1] as usize].uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1;\n        let mut uu: f32 = e0x * ey - ex * e0y;\n\n        for i in 0..(num as usize - 1) {\n            let (e1x, e1y): (f32, f32) = if i as i32 == (num - 2) {\n                // Close the circle\n                (\n                    SPECTRUM_DATA_POINTS[idx[1] as usize].uv.0\n                        - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0,\n                    SPECTRUM_DATA_POINTS[idx[1] as usize].uv.1\n                        - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1,\n                )\n            } else {\n                (\n                    SPECTRUM_DATA_POINTS[idx[i + 2] as usize].uv.0\n                        - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0,\n                    SPECTRUM_DATA_POINTS[idx[i + 2] as usize].uv.1\n                        - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1,\n                )\n            };\n\n            let vv: f32 = ex * e1y - e1x * ey;\n            let area: f32 = e0x * e1y - e1x * e0y;\n\n            // Normalise\n            let u: f32 = uu / area;\n            let v: f32 = vv / area;\n            let w: f32 = 1.0 - u - v;\n            // Outside spectral locus (quantized version at least) or outside grid\n            if u < 0.0 || v < 0.0 || w < 0.0 {\n                uu = -vv;\n                e0x = e1x;\n                e0y = e1y;\n                continue;\n            }\n\n            // This seems to be the triangle we've been looking for.\n            interpolated_p =\n                p[0] * w + p[i + 1] * v + p[if i as i32 == (num - 2) { 1 } else { i + 2 }] * u;\n            break;\n        }\n    }\n\n    // Now we have a spectrum which corresponds to the xy chromaticities of\n    // the input. need to scale according to the input brightness X+Y+Z now:\n    return interpolated_p * inv_norm;\n}\n\n// apply a 3x2 matrix to a 2D color.\n#[inline(always)]\nfn spectrum_apply_3x2(matrix: &[f32; 6], src: (f32, f32)) -> (f32, f32) {\n    (\n        matrix[0] * src.0 + matrix[1] * src.1 + matrix[2],\n        matrix[3] * src.0 + matrix[4] * src.1 + matrix[5],\n    )\n}\n// Concrete conversion routines.\n// #[inline(always)]\n// fn spectrum_xy_to_xystar(xy: (f32, f32)) -> (f32, f32) {\n//     spectrum_apply_3x2(&SPECTRUM_MAT_XY_TO_XYSTAR, xy)\n// }\n// #[inline(always)]\n// fn spectrum_xystar_to_xy(xystar: (f32, f32)) -> (f32, f32) {\n//     spectrum_apply_3x2(&SPECTRUM_MAT_XYSTAR_TO_XY, xystar)\n// }\n#[inline(always)]\nfn spectrum_xy_to_uv(xy: (f32, f32)) -> (f32, f32) {\n    spectrum_apply_3x2(&SPECTRUM_MAT_XY_TO_UV, xy)\n}\n// #[inline(always)]\n// fn spectrum_uv_to_xy(uv: (f32, f32)) -> (f32, f32) {\n//     spectrum_apply_3x2(&SPECTRUM_MAT_UV_TO_XY, uv)\n// }\n\n// #[inline]\n// pub fn xyz_from_spectrum(spectrum: &[f32]) -> (f32, f32, f32) {\n//     let mut xyz = (0.0, 0.0, 0.0);\n//     for i in 0..(SPECTRUM_NUM_SAMPLES as usize) {\n//         xyz.0 += spectrum[i] * CMF_X[i];\n//         xyz.1 += spectrum[i] * CMF_Y[i];\n//         xyz.2 += spectrum[i] * CMF_Z[i];\n//     }\n//     xyz.0 *= SPECTRUM_BIN_SIZE;\n//     xyz.1 *= SPECTRUM_BIN_SIZE;\n//     xyz.2 *= SPECTRUM_BIN_SIZE;\n//     return xyz;\n// }\n"
  }
]