diff --git a/Cargo.lock b/Cargo.lock
index a0fa4571a992c3cd325a54e15a7889d11bf97b58..a3cfd778b0a943b1f7078235249c6d5a45d08aff 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -207,9 +207,9 @@ dependencies = [
 
 [[package]]
 name = "anstream"
-version = "0.6.15"
+version = "0.6.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
+checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -222,43 +222,43 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.8"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.5"
+version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.1.1"
+version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
 dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.4"
+version = "3.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
 dependencies = [
  "anstyle",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "anyhow"
-version = "1.0.89"
+version = "1.0.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
+checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
 
 [[package]]
 name = "approx"
@@ -283,7 +283,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -421,9 +421,9 @@ dependencies = [
 
 [[package]]
 name = "avif-serialize"
-version = "0.8.1"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2"
+checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62"
 dependencies = [
  "arrayvec",
 ]
@@ -571,7 +571,7 @@ dependencies = [
  "bevy_macro_utils",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -655,7 +655,7 @@ checksum = "3fbfc33a4c6b80760bb8bf850a2cc65a1e031da62fd3ca8b552189104dc98514"
 dependencies = [
  "bevy_macro_utils",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -704,7 +704,7 @@ dependencies = [
  "bevy_macro_utils",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -773,7 +773,7 @@ dependencies = [
  "bevy_macro_utils",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -900,7 +900,7 @@ checksum = "bfc65e570012e64a21f3546df68591aaede8349e6174fb500071677f54f06630"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
  "toml_edit",
 ]
 
@@ -989,7 +989,7 @@ dependencies = [
  "bevy_macro_utils",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
  "uuid",
 ]
 
@@ -1025,7 +1025,7 @@ dependencies = [
  "encase",
  "futures-lite",
  "hexasphere",
- "image 0.25.2",
+ "image 0.25.4",
  "js-sys",
  "ktx2",
  "naga",
@@ -1050,7 +1050,7 @@ dependencies = [
  "bevy_macro_utils",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -1122,7 +1122,7 @@ dependencies = [
  "bevy_macro_utils",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -1242,7 +1242,7 @@ checksum = "38f1ab8f2f6f58439d260081d89a42b02690e5fdd64f814edc9417d33fcf2857"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -1304,7 +1304,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -1392,9 +1392,9 @@ dependencies = [
 
 [[package]]
 name = "built"
-version = "0.7.4"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4"
+checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b"
 
 [[package]]
 name = "bumpalo"
@@ -1404,9 +1404,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
 
 [[package]]
 name = "bytemuck"
-version = "1.18.0"
+version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae"
+checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
 dependencies = [
  "bytemuck_derive",
 ]
@@ -1419,7 +1419,7 @@ checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -1436,9 +1436,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
 
 [[package]]
 name = "bytes"
-version = "1.7.2"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
 
 [[package]]
 name = "calloop"
@@ -1456,9 +1456,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.1.28"
+version = "1.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1"
+checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
 dependencies = [
  "jobserver",
  "libc",
@@ -1551,9 +1551,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.19"
+version = "4.5.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615"
+checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -1561,9 +1561,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.19"
+version = "4.5.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b"
+checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1580,7 +1580,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -1616,9 +1616,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
 
 [[package]]
 name = "colorchoice"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 
 [[package]]
 name = "colorous"
@@ -1959,7 +1959,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "strsim 0.11.1",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -1981,7 +1981,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
 dependencies = [
  "darling_core 0.20.10",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2025,11 +2025,11 @@ dependencies = [
 
 [[package]]
 name = "derive_builder"
-version = "0.20.1"
+version = "0.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b"
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
 dependencies = [
- "derive_builder_macro 0.20.1",
+ "derive_builder_macro 0.20.2",
 ]
 
 [[package]]
@@ -2046,14 +2046,14 @@ dependencies = [
 
 [[package]]
 name = "derive_builder_core"
-version = "0.20.1"
+version = "0.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38"
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
 dependencies = [
  "darling 0.20.10",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2068,12 +2068,12 @@ dependencies = [
 
 [[package]]
 name = "derive_builder_macro"
-version = "0.20.1"
+version = "0.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc"
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
 dependencies = [
- "derive_builder_core 0.20.1",
- "syn 2.0.79",
+ "derive_builder_core 0.20.2",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2159,9 +2159,9 @@ dependencies = [
 
 [[package]]
 name = "dwrote"
-version = "0.11.1"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2da3498378ed373237bdef1eddcc64e7be2d3ba4841f4c22a998e81cadeea83c"
+checksum = "70182709525a3632b2ba96b6569225467b18ecb4a77f46d255f713a6bebf05fd"
 dependencies = [
  "lazy_static",
  "libc",
@@ -2216,7 +2216,7 @@ checksum = "fd31dbbd9743684d339f907a87fe212cb7b51d75b9e8e74181fe363199ee9b47"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2334,9 +2334,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
 
 [[package]]
 name = "fdeflate"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab"
+checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb"
 dependencies = [
  "simd-adler32",
 ]
@@ -2371,9 +2371,9 @@ checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
 
 [[package]]
 name = "flume"
-version = "0.11.0"
+version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
 dependencies = [
  "spin",
 ]
@@ -2427,7 +2427,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2644,7 +2644,7 @@ dependencies = [
  "inflections",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -2893,9 +2893,9 @@ dependencies = [
 
 [[package]]
 name = "image"
-version = "0.25.2"
+version = "0.25.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
+checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae"
 dependencies = [
  "bytemuck",
  "byteorder-lite",
@@ -2916,9 +2916,9 @@ dependencies = [
 
 [[package]]
 name = "image-webp"
-version = "0.1.3"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904"
+checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
 dependencies = [
  "byteorder-lite",
  "quick-error",
@@ -2926,15 +2926,15 @@ dependencies = [
 
 [[package]]
 name = "imgref"
-version = "1.10.1"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
+checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
 
 [[package]]
 name = "immutable-chunkmap"
-version = "2.0.5"
+version = "2.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4419f022e55cc63d5bbd6b44b71e1d226b9c9480a47824c706e9d54e5c40c5eb"
+checksum = "12f97096f508d54f8f8ab8957862eee2ccd628847b6217af1a335e1c44dee578"
 dependencies = [
  "arrayvec",
 ]
@@ -2993,7 +2993,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -3075,9 +3075,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
 
 [[package]]
 name = "js-sys"
-version = "0.3.70"
+version = "0.3.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
+checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -3142,9 +3142,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.159"
+version = "0.2.161"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
+checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
 
 [[package]]
 name = "libflate"
@@ -3209,17 +3209,6 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
-[[package]]
-name = "libredox"
-version = "0.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607"
-dependencies = [
- "bitflags 2.6.0",
- "libc",
- "redox_syscall 0.4.1",
-]
-
 [[package]]
 name = "libredox"
 version = "0.1.3"
@@ -3228,6 +3217,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
 dependencies = [
  "bitflags 2.6.0",
  "libc",
+ "redox_syscall 0.5.7",
 ]
 
 [[package]]
@@ -3434,9 +3424,9 @@ dependencies = [
 
 [[package]]
 name = "nalgebra"
-version = "0.33.0"
+version = "0.33.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c4b5f057b303842cf3262c27e465f4c303572e7f6b0648f60e16248ac3397f4"
+checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b"
 dependencies = [
  "approx",
  "matrixmultiply",
@@ -3457,7 +3447,7 @@ checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -3621,7 +3611,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -3683,7 +3673,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -3963,13 +3953,13 @@ dependencies = [
  "bevy",
  "bevy_flycam",
  "chrono",
- "clap 4.5.19",
+ "clap 4.5.20",
  "colorous",
  "csv",
  "delaunator",
  "embed-doc-image",
  "env_logger",
- "image 0.25.2",
+ "image 0.25.4",
  "itertools 0.13.0",
  "kahan",
  "log",
@@ -4006,11 +3996,11 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
 [[package]]
 name = "orbclient"
-version = "0.3.47"
+version = "0.3.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166"
+checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43"
 dependencies = [
- "libredox 0.0.2",
+ "libredox",
 ]
 
 [[package]]
@@ -4127,29 +4117,29 @@ dependencies = [
 
 [[package]]
 name = "pin-project"
-version = "1.1.6"
+version = "1.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec"
+checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.1.6"
+version = "1.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8"
+checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.14"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
 
 [[package]]
 name = "piper"
@@ -4283,30 +4273,30 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.87"
+version = "1.0.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
+checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "profiling"
-version = "1.0.15"
+version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
+checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
 dependencies = [
  "profiling-procmacros",
 ]
 
 [[package]]
 name = "profiling-procmacros"
-version = "1.0.15"
+version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
+checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
 dependencies = [
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -4412,9 +4402,9 @@ dependencies = [
 
 [[package]]
 name = "ravif"
-version = "0.11.10"
+version = "0.11.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd"
+checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6"
 dependencies = [
  "avif-serialize",
  "imgref",
@@ -4487,15 +4477,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
 dependencies = [
  "getrandom",
- "libredox 0.1.3",
+ "libredox",
  "thiserror",
 ]
 
 [[package]]
 name = "regex"
-version = "1.11.0"
+version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -4546,9 +4536,6 @@ name = "rgb"
 version = "0.8.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
-dependencies = [
- "bytemuck",
-]
 
 [[package]]
 name = "rle-decode-fast"
@@ -4640,9 +4627,9 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "0.38.37"
+version = "0.38.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
+checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
 dependencies = [
  "bitflags 2.6.0",
  "errno",
@@ -4653,9 +4640,9 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
+checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
 
 [[package]]
 name = "ruzstd"
@@ -4710,29 +4697,29 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
 
 [[package]]
 name = "serde"
-version = "1.0.210"
+version = "1.0.214"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
+checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.210"
+version = "1.0.214"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
+checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.128"
+version = "1.0.132"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
+checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
 dependencies = [
  "itoa",
  "memchr",
@@ -4931,7 +4918,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -4953,9 +4940,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.79"
+version = "2.0.85"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
+checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5047,22 +5034,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
 
 [[package]]
 name = "thiserror"
-version = "1.0.64"
+version = "1.0.65"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
+checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.64"
+version = "1.0.65"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
+checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -5197,7 +5184,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -5376,9 +5363,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "uuid"
-version = "1.10.0"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
+checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
 dependencies = [
  "getrandom",
  "rand",
@@ -5421,7 +5408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f"
 dependencies = [
  "anyhow",
- "derive_builder 0.20.1",
+ "derive_builder 0.20.2",
  "rustversion",
  "vergen-lib",
 ]
@@ -5433,7 +5420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e771aff771c0d7c2f42e434e2766d304d917e29b40f0424e8faaaa936bbc3f29"
 dependencies = [
  "anyhow",
- "derive_builder 0.20.1",
+ "derive_builder 0.20.2",
  "git2",
  "rustversion",
  "time",
@@ -5448,7 +5435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0"
 dependencies = [
  "anyhow",
- "derive_builder 0.20.1",
+ "derive_builder 0.20.2",
  "rustversion",
 ]
 
@@ -5491,9 +5478,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.93"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
+checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
 dependencies = [
  "cfg-if",
  "once_cell",
@@ -5502,24 +5489,24 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.93"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
+checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
 dependencies = [
  "bumpalo",
  "log",
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.43"
+version = "0.4.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
+checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -5529,9 +5516,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.93"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
+checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -5539,28 +5526,28 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.93"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
+checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.93"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
+checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
 
 [[package]]
 name = "web-sys"
-version = "0.3.70"
+version = "0.3.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
+checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -5810,7 +5797,7 @@ checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -5821,7 +5808,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -5832,7 +5819,7 @@ checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -5843,7 +5830,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
@@ -6265,7 +6252,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.79",
+ "syn 2.0.85",
 ]
 
 [[package]]
diff --git a/opossum/examples/parabolic_mirror.rs b/opossum/examples/parabolic_mirror.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b60b2646a914a35feb3fa8ebd2fd70a63075551a
--- /dev/null
+++ b/opossum/examples/parabolic_mirror.rs
@@ -0,0 +1,49 @@
+use opossum::{
+    analyzers::{AnalyzerType, RayTraceConfig},
+    coatings::CoatingType,
+    degree,
+    error::OpmResult,
+    joule, millimeter,
+    nodes::{
+        round_collimated_ray_source, EnergyMeter, NodeGroup, ParabolicMirror,
+        RayPropagationVisualizer, SpotDiagram, ThinMirror, WaveFront,
+    },
+    optic_node::{Alignable, OpticNode},
+    optic_ports::PortType,
+    OpmDocument,
+};
+use std::path::Path;
+
+fn main() -> OpmResult<()> {
+    let mut scenery = NodeGroup::default();
+    let i_src = scenery.add_node(&round_collimated_ray_source(
+        millimeter!(20.0),
+        joule!(1.0),
+        3,
+    )?)?;
+    let mut mirror1 = ThinMirror::new("mirror 1").with_tilt(degree!(22.5, 0.0, 0.0))?;
+    mirror1.set_coating(
+        &PortType::Input,
+        "input",
+        &CoatingType::ConstantR { reflectivity: 0.5 },
+    )?;
+    let i_m1 = scenery.add_node(&mirror1)?;
+    let i_m2 = scenery.add_node(
+        &ParabolicMirror::new("parabola", millimeter!(-50.0))?
+            .with_oap_angles(degree!(45.0, 0.0))?,
+    )?;
+    let i_prop_vis = scenery.add_node(&RayPropagationVisualizer::default())?;
+    let i_sd = scenery.add_node(&SpotDiagram::default())?;
+    let i_wf = scenery.add_node(&WaveFront::default())?;
+    let i_pm = scenery.add_node(&EnergyMeter::default())?;
+    scenery.connect_nodes(i_src, "out1", i_m1, "input", millimeter!(100.0))?;
+    scenery.connect_nodes(i_m1, "reflected", i_m2, "input", millimeter!(100.0))?;
+    scenery.connect_nodes(i_m2, "reflected", i_prop_vis, "in1", millimeter!(80.0))?;
+    scenery.connect_nodes(i_prop_vis, "out1", i_sd, "in1", millimeter!(0.1))?;
+    scenery.connect_nodes(i_sd, "out1", i_wf, "in1", millimeter!(0.1))?;
+    scenery.connect_nodes(i_wf, "out1", i_pm, "in1", millimeter!(0.1))?;
+
+    let mut doc = OpmDocument::new(scenery);
+    doc.add_analyzer(AnalyzerType::RayTrace(RayTraceConfig::default()));
+    doc.save_to_file(Path::new("./opossum/playground/parabolic_mirror.opm"))
+}
diff --git a/opossum/src/main.rs b/opossum/src/main.rs
index d92ab1d5f08d5de2bbbb970a74ad06d564f9130b..f05a81d471264fd1b00538e79b643afb61cc872d 100644
--- a/opossum/src/main.rs
+++ b/opossum/src/main.rs
@@ -110,11 +110,11 @@ fn opossum() -> OpmResult<()> {
     let analyzers = document.analyzers();
     let scenery = document.scenery_mut();
     //create the dot file of the scenery
+    create_data_dir(&opossum_args.report_directory)?;
     create_dot_file(&opossum_args.report_directory, scenery)?;
     if analyzers.is_empty() {
         info!("No analyzer defined in document. Stopping here.");
     } else {
-        create_data_dir(&opossum_args.report_directory)?;
         for ana in analyzers.iter().enumerate() {
             let analyzer: &dyn Analyzer = match ana.1 {
                 AnalyzerType::Energy => &EnergyAnalyzer::default(),
diff --git a/opossum/src/nodes/mod.rs b/opossum/src/nodes/mod.rs
index f58118605c847667357df1c4fddf15074aa41f6e..f463f05b5f6c7aa3ad75472c220ad3f9e1007347 100644
--- a/opossum/src/nodes/mod.rs
+++ b/opossum/src/nodes/mod.rs
@@ -10,6 +10,7 @@ mod ideal_filter;
 mod lens;
 mod node_attr;
 mod node_group;
+mod parabolic_mirror;
 mod paraxial_surface;
 mod reference;
 mod source;
@@ -34,6 +35,7 @@ pub use ideal_filter::{FilterType, IdealFilter};
 pub use lens::Lens;
 pub use node_attr::NodeAttr;
 pub use node_group::{NodeGroup, OpticGraph};
+pub use parabolic_mirror::ParabolicMirror;
 pub use paraxial_surface::ParaxialSurface;
 pub use ray_propagation_visualizer::RayPropagationVisualizer;
 pub use reference::NodeReference;
@@ -62,6 +64,7 @@ use uuid::Uuid;
 /// # Errors
 ///
 /// This function will return an [`OpossumError`] if there is no node with the given type.
+#[allow(clippy::too_many_lines)]
 pub fn create_node_ref(node_type: &str, uuid: Option<Uuid>) -> OpmResult<OpticRef> {
     match node_type {
         "dummy" => Ok(OpticRef::new(
@@ -159,6 +162,11 @@ pub fn create_node_ref(node_type: &str, uuid: Option<Uuid>) -> OpmResult<OpticRe
             uuid,
             None,
         )),
+        "parabolic mirror" => Ok(OpticRef::new(
+            Rc::new(RefCell::new(ParabolicMirror::default())),
+            uuid,
+            None,
+        )),
         _ => Err(OpossumError::Other(format!(
             "cannot create node type <{node_type}>"
         ))),
diff --git a/opossum/src/nodes/node_group/mod.rs b/opossum/src/nodes/node_group/mod.rs
index e159827a2b9ed9b4f0c52a7c8666a67ac35d20ee..0ba27cc703b8f058ef7d52530424b10a9de93cec 100644
--- a/opossum/src/nodes/node_group/mod.rs
+++ b/opossum/src/nodes/node_group/mod.rs
@@ -29,9 +29,9 @@ use std::{
     cell::RefCell,
     collections::{BTreeMap, HashMap},
     io::Write,
+    process::Stdio,
     rc::Rc,
 };
-use tempfile::NamedTempFile;
 use uom::si::f64::Length;
 use uuid::Uuid;
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -396,17 +396,29 @@ impl NodeGroup {
     /// This function will return an error if the image generation fails (e.g. program not found, no memory left etc.).
     pub fn toplevel_dot_svg(&self) -> OpmResult<String> {
         let dot_string = self.toplevel_dot("")?;
-        let mut f = NamedTempFile::new()
-            .map_err(|e| OpossumError::Other(format!("conversion to image failed: {e}")))?;
-        f.write_all(dot_string.as_bytes())
-            .map_err(|e| OpossumError::Other(format!("conversion to image failed: {e}")))?;
-        let r = std::process::Command::new("dot")
-            .arg(f.path())
+        let mut child = std::process::Command::new("dot")
             .arg("-Tsvg:cairo")
             .arg("-Kdot")
-            .output()
+            .stdin(Stdio::piped())
+            .stdout(Stdio::piped())
+            .stderr(Stdio::null())
+            .spawn()
+            .map_err(|e| OpossumError::Other(format!("conversion to image failed: {e}")))?;
+
+        let Some(child_stdin) = child.stdin.as_mut() else {
+            return Err(OpossumError::Other(
+                "conversion to image failed: could not set stdin for graphviz command".into(),
+            ));
+        };
+        child_stdin
+            .write_all(dot_string.as_bytes())
             .map_err(|e| OpossumError::Other(format!("conversion to image failed: {e}")))?;
-        let svg_string = String::from_utf8(r.stdout)
+
+        let output = child
+            .wait_with_output()
+            .map_err(|e| OpossumError::Other(format!("conversion to image failed: {e}")))?;
+
+        let svg_string = String::from_utf8(output.stdout)
             .map_err(|e| OpossumError::Other(format!("conversion to image failed: {e}")))?;
         Ok(svg_string)
     }
diff --git a/opossum/src/nodes/parabolic_mirror.rs b/opossum/src/nodes/parabolic_mirror.rs
new file mode 100644
index 0000000000000000000000000000000000000000..915b2679d4f858baf7a8d0e221b1a4c2a9f3ebf5
--- /dev/null
+++ b/opossum/src/nodes/parabolic_mirror.rs
@@ -0,0 +1,231 @@
+use nalgebra::Point2;
+use uom::si::f64::{Angle, Length};
+
+use crate::{
+    analyzers::{
+        energy::AnalysisEnergy, ghostfocus::AnalysisGhostFocus, raytrace::AnalysisRayTrace,
+        Analyzable, RayTraceConfig,
+    },
+    coatings::CoatingType,
+    degree,
+    dottable::Dottable,
+    error::{OpmResult, OpossumError},
+    light_result::LightResult,
+    lightdata::LightData,
+    meter,
+    optic_node::{Alignable, OpticNode, LIDT},
+    optic_ports::{OpticPorts, PortType},
+    properties::Proptype,
+    surface::{OpticalSurface, Parabola},
+    utils::geom_transformation::Isometry,
+};
+
+use super::NodeAttr;
+
+#[derive(Debug, Clone)]
+/// An infinitely thin mirror with a spherical (or flat) surface.
+///
+///
+/// ## Optical Ports
+///   - Inputs
+///     - `input`
+///   - Outputs
+///     - `reflected`
+///
+/// ## Properties
+///   - `name`
+///   - `inverted`
+///   - `curvature`
+pub struct ParabolicMirror {
+    node_attr: NodeAttr,
+    surface: OpticalSurface,
+}
+impl Default for ParabolicMirror {
+    /// Create a parabolic mirror with a focal length of 1 meter.
+    fn default() -> Self {
+        let mut node_attr = NodeAttr::new("parabolic mirror");
+        node_attr
+            .create_property("focal length", "focal length", None, meter!(-1.0).into())
+            .unwrap();
+        node_attr
+            .create_property(
+                "oap angle x",
+                "off axis angle around local x axis",
+                None,
+                degree!(0.0).into(),
+            )
+            .unwrap();
+        node_attr
+            .create_property(
+                "oap angle y",
+                "off axis angle around local y axis",
+                None,
+                degree!(0.0).into(),
+            )
+            .unwrap();
+        let mut ports = OpticPorts::new();
+        ports.add(&PortType::Input, "input").unwrap();
+        ports
+            .set_coating(
+                &PortType::Input,
+                "input",
+                &CoatingType::ConstantR { reflectivity: 1.0 },
+            )
+            .unwrap();
+        ports.add(&PortType::Output, "reflected").unwrap();
+        node_attr.set_ports(ports);
+
+        Self {
+            node_attr,
+            surface: OpticalSurface::new(Box::new(
+                Parabola::new(meter!(-1.0), &Isometry::identity()).unwrap(),
+            )),
+        }
+    }
+}
+impl ParabolicMirror {
+    /// Creates a new [`ParabolicMirror`] node.
+    ///
+    /// This function creates a infinitely thin parabolic mirror with a given focal length.
+    ///
+    /// # Errors
+    ///
+    /// This function returns an error if the given focal length is zero or not finite.
+    pub fn new(name: &str, focal_length: Length) -> OpmResult<Self> {
+        if !focal_length.is_normal() {
+            return Err(OpossumError::Other(
+                "focal length must not be 0.0 and finite".into(),
+            ));
+        }
+        let mut parabola = Self::default();
+        parabola.node_attr.set_name(name);
+        parabola
+            .node_attr
+            .set_property("focal length", focal_length.into())?;
+        parabola.update_surfaces()?;
+        Ok(parabola)
+    }
+    /// Returns / modifies a [`ParabolicMirror`] with given off-axis angles.
+    ///
+    /// The angles define the off axis angles around the local x and y axis of the node. The given angles denote the full
+    /// angle between an incoming and a reflected beam. Effectively this introduces a decentering
+    /// of the node during positioning in 3D space such that the desired angles are met.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the node properties cannot be set.
+    pub fn with_oap_angles(mut self, angles: Point2<Angle>) -> OpmResult<Self> {
+        self.set_property("oap angle x", angles[0].into())?;
+        self.set_property("oap angle y", angles[1].into())?;
+        self.update_surfaces()?;
+        Ok(self)
+    }
+}
+impl OpticNode for ParabolicMirror {
+    fn node_attr(&self) -> &NodeAttr {
+        &self.node_attr
+    }
+    fn node_attr_mut(&mut self) -> &mut NodeAttr {
+        &mut self.node_attr
+    }
+    fn update_surfaces(&mut self) -> OpmResult<()> {
+        let Ok(Proptype::Length(focal_length)) = self.node_attr.get_property("focal length") else {
+            return Err(OpossumError::Analysis("cannot read focal length".into()));
+        };
+        let Ok(Proptype::Angle(oap_angle_x)) = self.node_attr.get_property("oap angle x") else {
+            return Err(OpossumError::Analysis(
+                "cannot read off axis angle x".into(),
+            ));
+        };
+        let Ok(Proptype::Angle(oap_angle_y)) = self.node_attr.get_property("oap angle y") else {
+            return Err(OpossumError::Analysis(
+                "cannot read off axis angle y".into(),
+            ));
+        };
+        let mut parabola = Parabola::new(*focal_length, &Isometry::identity())?;
+        parabola.set_off_axis_angles((*oap_angle_x, *oap_angle_y));
+        self.surface = OpticalSurface::new(Box::new(parabola));
+        Ok(())
+    }
+    fn get_surface_mut(&mut self, _surf_name: &str) -> &mut OpticalSurface {
+        &mut self.surface
+    }
+}
+impl Alignable for ParabolicMirror {}
+impl Dottable for ParabolicMirror {
+    fn node_color(&self) -> &str {
+        "chocolate2"
+    }
+}
+impl LIDT for ParabolicMirror {}
+impl Analyzable for ParabolicMirror {}
+impl AnalysisGhostFocus for ParabolicMirror {}
+impl AnalysisEnergy for ParabolicMirror {
+    fn analyze(&mut self, incoming_data: LightResult) -> OpmResult<LightResult> {
+        let (inport, outport) = if self.inverted() {
+            ("reflected", "input")
+        } else {
+            ("input", "reflected")
+        };
+        let Some(data) = incoming_data.get(inport) else {
+            return Ok(LightResult::default());
+        };
+        Ok(LightResult::from([(outport.into(), data.clone())]))
+    }
+}
+impl AnalysisRayTrace for ParabolicMirror {
+    fn analyze(
+        &mut self,
+        incoming_data: LightResult,
+        config: &RayTraceConfig,
+    ) -> OpmResult<LightResult> {
+        let (inport, outport) = if self.inverted() {
+            ("reflected", "input")
+        } else {
+            ("input", "reflected")
+        };
+        let Some(data) = incoming_data.get(inport) else {
+            return Ok(LightResult::default());
+        };
+        if let LightData::Geometric(mut rays) = data.clone() {
+            let reflected = if let Some(iso) = self.effective_iso() {
+                let coating = self
+                    .node_attr()
+                    .ports()
+                    .coating(&PortType::Input, "input")
+                    .unwrap()
+                    .clone();
+                let surface = self.get_surface_mut("");
+                surface.set_isometry(&iso);
+                surface.set_coating(coating);
+                let mut reflected_rays = rays.refract_on_surface(surface, None)?;
+                if let Some(aperture) = self.ports().aperture(&PortType::Input, inport) {
+                    reflected_rays.apodize(aperture, &iso)?;
+                    reflected_rays.invalidate_by_threshold_energy(config.min_energy_per_ray())?;
+                    reflected_rays
+                } else {
+                    return Err(OpossumError::OpticPort("input aperture not found".into()));
+                }
+            } else {
+                return Err(OpossumError::Analysis(
+                    "no location for surface defined. Aborting".into(),
+                ));
+            };
+            let light_data = LightData::Geometric(reflected);
+            let light_result = LightResult::from([(outport.into(), light_data)]);
+            Ok(light_result)
+        } else {
+            Err(OpossumError::Analysis(
+                "expected ray data at input port".into(),
+            ))
+        }
+    }
+
+    fn calc_node_position(
+        &mut self,
+        incoming_data: LightResult,
+        config: &RayTraceConfig,
+    ) -> OpmResult<LightResult> {
+        AnalysisRayTrace::analyze(self, incoming_data, config)
+    }
+}
diff --git a/opossum/src/surface/mod.rs b/opossum/src/surface/mod.rs
index 5377bd20449254cd0de52ec11b53f3cecc4c735e..5caf321a38b82013bcd5a9de7b98bca6247189a5 100644
--- a/opossum/src/surface/mod.rs
+++ b/opossum/src/surface/mod.rs
@@ -4,6 +4,7 @@
 //! This module handles only the geometric aspect of an optical surface. So a [`GeoSurface`] has no [`Coating`](crate::coatings::Coating) or
 //! [`Aperture`](crate::aperture::Aperture)s.
 mod cylinder;
+mod parabola;
 mod plane;
 mod sphere;
 
@@ -11,6 +12,7 @@ pub mod hit_map;
 mod optical_surface;
 pub use cylinder::Cylinder;
 pub use optical_surface::OpticalSurface;
+pub use parabola::Parabola;
 pub use plane::Plane;
 pub use sphere::Sphere;
 
diff --git a/opossum/src/surface/parabola.rs b/opossum/src/surface/parabola.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1df78a7c1c33ebafec13d10ac855ae09b9353f7d
--- /dev/null
+++ b/opossum/src/surface/parabola.rs
@@ -0,0 +1,209 @@
+//! Parabolic surface
+//!
+//! This module implements a parabolic surface with a given focal length and a given z position on the optical axis.
+
+use crate::{
+    degree,
+    error::{OpmResult, OpossumError},
+    meter, radian,
+    utils::geom_transformation::Isometry,
+};
+use nalgebra::{vector, Point3, Vector3};
+use num::Zero;
+use roots::{find_roots_quadratic, Roots};
+use uom::si::{
+    f64::{Angle, Length, Ratio},
+    ratio::basis_point,
+};
+
+use super::GeoSurface;
+
+#[derive(Debug, Clone)]
+/// A spherical surface with its anchor point on the optical axis.
+pub struct Parabola {
+    focal_length: Length,
+    isometry: Isometry,
+    off_axis_angles: (Angle, Angle),
+}
+
+impl Parabola {
+    /// Create a new [`Parabola`] located and oriented by the given [`Isometry`].
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the focal length is not normal.
+    pub fn new(focal_length: Length, isometry: &Isometry) -> OpmResult<Self> {
+        if !focal_length.is_normal() {
+            return Err(OpossumError::Other(
+                "focal length must be != 0.0 and finite".into(),
+            ));
+        }
+        let anchor_isometry = Isometry::new(
+            Point3::new(Length::zero(), Length::zero(), focal_length),
+            radian!(0., 0., 0.),
+        )?;
+        let isometry = isometry.append(&anchor_isometry);
+        Ok(Self {
+            focal_length,
+            isometry,
+            off_axis_angles: (Angle::zero(), Angle::zero()),
+        })
+    }
+    /// Sets the off-axis angles (full refelction) of this [`Parabola`].
+    pub fn set_off_axis_angles(&mut self, off_axis_angles: (Angle, Angle)) {
+        //  factor 2. because of full reflection angle <-> angle of incidence
+        self.off_axis_angles = (off_axis_angles.0 / 2., off_axis_angles.1 / 2.);
+    }
+    /// Returns the off axis angles of this [`Parabola`].
+    #[must_use]
+    pub fn off_axis_angles(&self) -> (Angle, Angle) {
+        self.off_axis_angles
+    }
+    fn calc_oap_decenter(&self) -> (Length, Length) {
+        let f_x =
+            2. * self.focal_length / (Ratio::new::<basis_point>(1.) + self.off_axis_angles.0.cos());
+        let f_y =
+            2. * self.focal_length / (Ratio::new::<basis_point>(1.) + self.off_axis_angles.0.cos());
+        let oad_x = f_y * (self.off_axis_angles.1.sin());
+        let oad_y = f_x * (self.off_axis_angles.0.sin());
+        (oad_x, oad_y)
+    }
+}
+
+impl GeoSurface for Parabola {
+    fn calc_intersect_and_normal_do(
+        &self,
+        ray: &crate::ray::Ray,
+    ) -> Option<(Point3<Length>, Vector3<f64>)> {
+        let dir = ray.direction();
+        let pos = vector![
+            ray.position().x.value,
+            ray.position().y.value,
+            ray.position().z.value
+        ];
+        let f_length = self.focal_length.value;
+        let is_back_propagating = dir.z.is_sign_negative();
+        // parabola formula (at origin)
+        // x^2 + y^2 - 4fz = 0
+        //
+        // insert ray (p: position, d: direction):
+        // (p_x+t*d_x)^2 + (p_y+t*d_y)^2 - 4f*(p_z+t*d_z) = 0
+        // This translates into the qudratic equation
+        // at^2 + bt + c = 0 with
+        // a = d_x^2+d_y^2
+        // b = 2* (p_x*d_x + p_y*d_y - 2*f*d_z)
+        // c = p_x^2 + p_y^2 - 4f*p_z
+        let a = dir.x.mul_add(dir.x, dir.y * dir.y);
+        let b = 2. * (2. * f_length).mul_add(-dir.z, pos.x.mul_add(dir.x, pos.y * dir.y));
+        let c = (4. * f_length).mul_add(-pos.z, pos.x.mul_add(pos.x, pos.y * pos.y));
+        // Solve t of qudaratic equation
+        let roots = find_roots_quadratic(a, b, c);
+        let intersection_point = match roots {
+            // no intersection
+            Roots::No(_) => return None,
+            // "just touching" intersection
+            Roots::One(t) => {
+                if t[0] >= 0.0 {
+                    pos + t[0] * dir
+                } else {
+                    return None;
+                }
+            }
+            // "regular" intersection
+            Roots::Two(t) => {
+                let real_t = if self.focal_length.is_sign_positive() {
+                    // convex surface => use min t
+                    if is_back_propagating {
+                        f64::max(t[0], t[1])
+                    } else {
+                        f64::min(t[0], t[1])
+                    }
+                } else {
+                    // concave surface => use max t
+                    if is_back_propagating {
+                        f64::min(t[0], t[1])
+                    } else {
+                        f64::max(t[0], t[1])
+                    }
+                };
+                if real_t.is_sign_negative() {
+                    // surface behind beam
+                    return None;
+                }
+                pos + real_t * dir
+            }
+            _ => unreachable!(),
+        };
+        // calc surface normal
+        // calculate grad F(x,y,z) =(2* p_x, 2* p_y, -4 * f)
+        let normal_vector = vector![
+            2. * intersection_point.x,
+            2. * intersection_point.y,
+            -4. * f_length
+        ];
+        Some((
+            meter!(
+                intersection_point.x,
+                intersection_point.y,
+                intersection_point.z
+            ),
+            -1.0 * normal_vector,
+        ))
+    }
+    fn isometry(&self) -> &Isometry {
+        &self.isometry
+    }
+    fn set_isometry(&mut self, isometry: &Isometry) {
+        let oap_decenter = self.calc_oap_decenter();
+        let oap_iso = Isometry::new(
+            Point3::new(oap_decenter.0, oap_decenter.1, Length::zero()),
+            degree!(0.0, 0.0, 0.0),
+        )
+        .unwrap();
+        let total_iso = isometry.append(&oap_iso);
+        self.isometry = total_iso;
+    }
+    fn box_clone(&self) -> Box<dyn GeoSurface> {
+        Box::new(self.clone())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use nalgebra::vector;
+
+    use super::Parabola;
+    use crate::{
+        degree, joule, meter, millimeter, nanometer, ray::Ray, surface::GeoSurface,
+        utils::geom_transformation::Isometry,
+    };
+
+    #[test]
+    fn intersect() {
+        let parabola = Parabola::new(meter!(-1.0), &Isometry::identity()).unwrap();
+        let ray = Ray::new_collimated(meter!(-1.0, -1.0, -10.0), nanometer!(1000.0), joule!(1.0))
+            .unwrap();
+        let intersection = parabola.calc_intersect_and_normal_do(&ray).unwrap();
+        assert_eq!(intersection.0, meter!(-1.0, -1.0, -0.5));
+        assert_eq!(intersection.1, vector![2.0, 2.0, -4.]);
+    }
+    #[test]
+    fn intersect_ray_through_focus() {
+        let parabola = Parabola::new(meter!(-1.0), &Isometry::identity()).unwrap();
+        let direction = vector![0.0, 1.0, 1. - 0.25];
+        let ray = Ray::new(
+            meter!(0.0, 0.0, -1.0),
+            direction,
+            nanometer!(1000.0),
+            joule!(1.0),
+        )
+        .unwrap();
+        dbg!(parabola.calc_intersect_and_normal_do(&ray));
+    }
+    #[test]
+    fn off_axis_decenter() {
+        let mut parabola = Parabola::new(millimeter!(-50.0), &Isometry::identity()).unwrap();
+        parabola.set_off_axis_angles((degree!(22.5), degree!(0.0)));
+        dbg!(parabola.calc_oap_decenter());
+    }
+}
diff --git a/opossum/src/surface/sphere.rs b/opossum/src/surface/sphere.rs
index eb148c59ecf07e2152c2fbab0fbcd891f1ff2017..0895d0ecdd0195662ceae6eef58b2317c9d9369b 100644
--- a/opossum/src/surface/sphere.rs
+++ b/opossum/src/surface/sphere.rs
@@ -62,11 +62,6 @@ impl Sphere {
         let isometry = isometry.append(&anchor_isometry);
         Ok(Self { radius, isometry })
     }
-    /// Returns the center position of this [`Sphere`]
-    #[must_use]
-    pub fn get_pos(&self) -> Point3<Length> {
-        self.isometry.transform_point(&Point3::origin())
-    }
 }
 impl GeoSurface for Sphere {
     fn calc_intersect_and_normal_do(&self, ray: &Ray) -> Option<(Point3<Length>, Vector3<f64>)> {
@@ -194,11 +189,9 @@ mod test {
 
         let s = Sphere::new(millimeter!(2.0), &iso).unwrap();
         assert_eq!(s.radius, millimeter!(2.0));
-        assert_eq!(s.get_pos(), millimeter!(0.0, 0.0, 3.0));
 
         let s = Sphere::new(millimeter!(-2.0), &iso).unwrap();
         assert_eq!(s.radius, millimeter!(-2.0));
-        assert_eq!(s.get_pos(), millimeter!(0.0, 0.0, -1.0));
     }
     #[test]
     fn new_at_position() {
@@ -229,7 +222,6 @@ mod test {
 
         let s = Sphere::new_at_position(millimeter!(2.0), millimeter!(1.0, 2.0, 3.0)).unwrap();
         assert_eq!(s.radius, millimeter!(2.0));
-        assert_eq!(s.get_pos(), millimeter!(1.0, 2.0, 5.0));
     }
     #[test]
     fn intersect_positive_on_axis_forward() {
diff --git a/opossum/test b/opossum/test
new file mode 100644
index 0000000000000000000000000000000000000000..da1b18e37b125fd2530d4db80b5c2f25bf1a1a15
Binary files /dev/null and b/opossum/test differ