Repository: f3d-app/f3d
Pull request #2172: vtkF3DMetaImporter: Fix coloring for skinning, morphing, and such
Author: login: mwestphal
Created: 2025-04-21T08:04:19Z | Merged: 2025-04-26T12:44:44Z

 - Proper Coloring/PointSprits actor creation by shallow copying Prop3D 
 - Add animation support for coloring and point sprites with a special case for generic importer
 - Add test covering coloring ( and coloring animation ) for user transform, morphing, skinning and input change
 - Add test covering point sprits (and point sprites animation) for user transform and input change
 - Add a test for generic importer animation and point sprites
 
Fix: https://github.com/f3d-app/f3d/issues/2000


Diff:
diff --git a/application/testing/CMakeLists.txt b/application/testing/CMakeLists.txt
index fb19a41ef8..c7eb8964f2 100644
--- a/application/testing/CMakeLists.txt
+++ b/application/testing/CMakeLists.txt
@@ -277,6 +277,15 @@ f3d_test(NAME TestConfigOrder DATA suzanne.ply ARGS CONFIG ${F3D_SOURCE_DIR}/tes
 f3d_test(NAME TestOutputStream DATA suzanne.ply ARGS --verbose=quiet --output=- REGEXP "^.PNG" NO_BASELINE NO_OUTPUT)
 f3d_test(NAME TestOutputStreamInfo DATA suzanne.ply ARGS --verbose=info --output=- REGEXP "redirected to stderr" NO_BASELINE NO_OUTPUT)
 f3d_test(NAME TestInvalidAntiAliasingMode DATA suzanne.ply ARGS -a --anti-aliasing-mode=foo REGEXP "foo is an invalid antialiasing mode" NO_BASELINE)
+f3d_test(NAME TestUserMatrixColoring DATA InterpolationTest.glb ARGS --scalar-coloring --coloring-array=TEXCOORD_0)
+f3d_test(NAME TestSkinColoring DATA SimpleSkin.gltf ARGS --scalar-coloring --coloring-array=WEIGHTS_0)
+f3d_test(NAME TestMorphColoring DATA SimpleMorph.gltf ARGS --scalar-coloring)
+f3d_test(NAME TestAnimationUserMatrixColoring DATA InterpolationTest.glb ARGS --scalar-coloring --coloring-array=TEXCOORD_0 --animation-time=0.5)
+f3d_test(NAME TestAnimationSkinColoring DATA SimpleSkin.gltf ARGS --scalar-coloring --coloring-array=WEIGHTS_0 --animation-time=2)
+f3d_test(NAME TestAnimationMorphColoring DATA SimpleMorph.gltf ARGS --scalar-coloring --animation-time=0.5)
+f3d_test(NAME TestAnimationInputChangeColoring DATA v_rock2.mdl ARGS --scalar-coloring --animation-time=0.01 --animation-index=1)
+f3d_test(NAME TestAnimationUserMatrixPointSprites DATA BoxAnimated.gltf ARGS --point-sprites --point-sprites-size=50 --animation-time=2)
+f3d_test(NAME TestAnimationInputChangePointSprites DATA v_rock2.mdl ARGS --point-sprites --point-sprites-size=50 --animation-time=0.01 --animation-index=1)
 
 # Color map files testing
 f3d_test(NAME TestColorMapFileFullPath DATA dragon.vtu ARGS --colormap-file=${F3D_SOURCE_DIR}/testing/data/viridis8.png --scalar-coloring --coloring-component=1)
@@ -721,14 +730,17 @@ if(F3D_PLUGIN_BUILD_EXODUS)
   # Test animation with generic importer and coloring
   f3d_test(NAME TestAnimationGenericImporter DATA small.ex2 ARGS -sb --load-plugins=exodus --animation-time=0.003 --animation-progress)
 
+  # Test animation with generic importer, coloring and point sprites
+  f3d_test(NAME TestAnimationGenericImporterPointSprites DATA small.ex2 ARGS -sbo --load-plugins=exodus --animation-time=0.003 --animation-progress)
+
   # Test animation with generic importer, coloring and a custom scalar range
   f3d_test(NAME TestAnimationGenericImporterScalarRange DATA small.ex2 ARGS -sb --load-plugins=exodus --animation-time=0.003 --animation-progress --coloring-range=0,1e7)
 
   # Test Generic Importer Verbose animation with a single frame.
   f3d_test(NAME TestVerboseAnimationSingleTimestep DATA single_timestep.e ARGS --load-plugins=exodus --verbose NO_BASELINE REGEXP "0, 0")
 
-  # Test no render animation time. Regex contains a part of the range of the ACCL field.
-  f3d_test(NAME TestNoRenderAnimation DATA small.ex2 ARGS  --load-plugins=exodus --animation-time=0.003 REGEXP "-521950, 6.57485" NO_RENDER)
+  # Test no render animation time. Regex contains a part of the range of the VEL_ field.
+  f3d_test(NAME TestNoRenderAnimation DATA small.ex2 ARGS  --load-plugins=exodus --animation-time=0.003 REGEXP "-994.473, 33.9259" NO_RENDER)
 
   # Test animation time clamping
   f3d_test(NAME TestAnimationTimeLimitsHigh DATA small.ex2 ARGS ARGS --load-plugins=exodus --animation-time=10)
diff --git a/testing/baselines/TestAnimationGenericImporter.png b/testing/baselines/TestAnimationGenericImporter.png
index d0c7811f32..4711d9f436 100644
--- a/testing/baselines/TestAnimationGenericImporter.png
+++ b/testing/baselines/TestAnimationGenericImporter.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:723909832b6e40cf9db909eb75b908cb3604f278e9292a512835879931f8a363
-size 12591
+oid sha256:ba21e16a3c6ab8c1ecb03ed4d0026915bdb805bc539c05d55f05d57cd686d213
+size 12271
diff --git a/testing/baselines/TestAnimationGenericImporterPointSprites.png b/testing/baselines/TestAnimationGenericImporterPointSprites.png
new file mode 100644
index 0000000000..cd207215bf
--- /dev/null
+++ b/testing/baselines/TestAnimationGenericImporterPointSprites.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9dfe6c72e185304b0c76f551ea1eb9f028a1accd2016bb4ff76997490584e471
+size 9082
diff --git a/testing/baselines/TestAnimationGenericImporterScalarRange.png b/testing/baselines/TestAnimationGenericImporterScalarRange.png
index 21f2742442..b60b5bed62 100644
--- a/testing/baselines/TestAnimationGenericImporterScalarRange.png
+++ b/testing/baselines/TestAnimationGenericImporterScalarRange.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e26d029c275cad851aaa2a866c900aa589d8b71fca5059c834e1d17727e1ca54
-size 11518
+oid sha256:8e5d9bc192cf85412ee2b1e922fa031b0c6cf691fd465d7ff167146fc88b9a72
+size 11812
diff --git a/testing/baselines/TestAnimationInputChangeColoring.png b/testing/baselines/TestAnimationInputChangeColoring.png
new file mode 100644
index 0000000000..2750404ace
--- /dev/null
+++ b/testing/baselines/TestAnimationInputChangeColoring.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3e4d3ea2b5b37cb1917d340bcfe1d55ba9bd4836c0e8fddf758605d21bf6aa07
+size 5831
diff --git a/testing/baselines/TestAnimationInputChangePointSprites.png b/testing/baselines/TestAnimationInputChangePointSprites.png
new file mode 100644
index 0000000000..a85b5d70fb
--- /dev/null
+++ b/testing/baselines/TestAnimationInputChangePointSprites.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:31000c5b112f73df0075c93987160d4836b3f7a99e4da953c18422b294796d5c
+size 15364
diff --git a/testing/baselines/TestAnimationMorphColoring.png b/testing/baselines/TestAnimationMorphColoring.png
new file mode 100644
index 0000000000..3db116d60b
--- /dev/null
+++ b/testing/baselines/TestAnimationMorphColoring.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7e9674b5be67d17b7f953a7ffbbe7b1dea25987ba226c5d7cec40ae6ffa3ffd3
+size 15659
diff --git a/testing/baselines/TestAnimationSkinColoring.png b/testing/baselines/TestAnimationSkinColoring.png
new file mode 100644
index 0000000000..d7d08dac72
--- /dev/null
+++ b/testing/baselines/TestAnimationSkinColoring.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f3e703c7154a5913e98dba307f8cb052b3b9d6e0c609bfce77663eb0201eff6a
+size 18463
diff --git a/testing/baselines/TestAnimationTimeLimitsHigh.png b/testing/baselines/TestAnimationTimeLimitsHigh.png
index 7e000255a8..bded68eace 100644
--- a/testing/baselines/TestAnimationTimeLimitsHigh.png
+++ b/testing/baselines/TestAnimationTimeLimitsHigh.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:14d8d520be9559a12f405815cdf078e1f66bc5bfc1c7855bdda98c3d1bacb63e
-size 6269
+oid sha256:117abea6de8640effb2759f0caf320b23ab88d9640065f5013481917b5f946d6
+size 4676
diff --git a/testing/baselines/TestAnimationTimeLimitsLow.png b/testing/baselines/TestAnimationTimeLimitsLow.png
index 7fb6fc70e0..98d28b0e2a 100644
--- a/testing/baselines/TestAnimationTimeLimitsLow.png
+++ b/testing/baselines/TestAnimationTimeLimitsLow.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:3750c6146d17584567873224261bd090952c3ea89a8bc1c81c0c85f840a411a3
-size 3712
+oid sha256:45621f47c5f4c4b17cd33275201e3823fd685f44fd1428aa90261900c7aa8411
+size 3938
diff --git a/testing/baselines/TestAnimationUserMatrixColoring.png b/testing/baselines/TestAnimationUserMatrixColoring.png
new file mode 100644
index 0000000000..891e026768
--- /dev/null
+++ b/testing/baselines/TestAnimationUserMatrixColoring.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:166632b05260071d422fbd1bb2707a067e4b6892cee0ed94889797aa05318193
+size 17128
diff --git a/testing/baselines/TestAnimationUserMatrixPointSprites.png b/testing/baselines/TestAnimationUserMatrixPointSprites.png
new file mode 100644
index 0000000000..10b6609dd3
--- /dev/null
+++ b/testing/baselines/TestAnimationUserMatrixPointSprites.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ae9d868d482379adeec8f295866c413cb66703fada89bbcf691f8a6d87906c41
+size 7440
diff --git a/testing/baselines/TestMorphColoring.png b/testing/baselines/TestMorphColoring.png
new file mode 100644
index 0000000000..60c240f1b1
--- /dev/null
+++ b/testing/baselines/TestMorphColoring.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8d3a1d6a0ab987fd7b57edb83e2c6587c9d14e0511a2d5c061b55e874200bf35
+size 18326
diff --git a/testing/baselines/TestSkinColoring.png b/testing/baselines/TestSkinColoring.png
new file mode 100644
index 0000000000..f78923e73e
--- /dev/null
+++ b/testing/baselines/TestSkinColoring.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7b49520f1893778d345e50dd25fa8daabf9767425c591e0c2570e83359749af4
+size 14526
diff --git a/testing/baselines/TestUserMatrixColoring.png b/testing/baselines/TestUserMatrixColoring.png
new file mode 100644
index 0000000000..4971574e41
--- /dev/null
+++ b/testing/baselines/TestUserMatrixColoring.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bd75b9d8b8c3634717596ab16ea116b7173898d268f853737a44e58d4b9277a8
+size 18177
diff --git a/testing/data/small.ex2 b/testing/data/small.ex2
index 13e77c497f..119f73aad6 100644
--- a/testing/data/small.ex2
+++ b/testing/data/small.ex2
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:9decafe68df07c2a358123a1f4634a620b62b354ac92269da08824b11a4204d4
-size 168796
+oid sha256:bc6b69e439a0a6f05fa6f3384024721ff4ea440c59a6ad57bd6e353220fbbf6a
+size 356592
diff --git a/vtkext/private/module/vtkF3DMetaImporter.cxx b/vtkext/private/module/vtkF3DMetaImporter.cxx
index 78dbc30252..1fbd6ea5db 100644
--- a/vtkext/private/module/vtkF3DMetaImporter.cxx
+++ b/vtkext/private/module/vtkF3DMetaImporter.cxx
@@ -19,6 +19,7 @@
 #include <numeric>
 #include <vector>
 
+//----------------------------------------------------------------------------
 struct vtkF3DMetaImporter::Internals
 {
   // Actors related vectors
@@ -44,6 +45,7 @@ struct vtkF3DMetaImporter::Internals
 #endif
 };
 
+//----------------------------------------------------------------------------
 vtkStandardNewMacro(vtkF3DMetaImporter);
 
 //----------------------------------------------------------------------------
@@ -250,7 +252,7 @@ bool vtkF3DMetaImporter::Update()
 
       // Create and configure point sprites actors
       this->Pimpl->PointSpritesActorsAndMappers.emplace_back(
-        vtkF3DMetaImporter::PointSpritesStruct());
+        vtkF3DMetaImporter::PointSpritesStruct(actor, importer));
       vtkF3DMetaImporter::PointSpritesStruct& pss =
         this->Pimpl->PointSpritesActorsAndMappers.back();
 
@@ -502,6 +504,29 @@ bool vtkF3DMetaImporter::UpdateAtTimeValue(double timeValue)
     importerPair.Importer->UpdateTimeStep(timeValue);
 #endif
   }
+
+  // Update coloring and point sprites
+  for (auto& cs : this->Pimpl->ColoringActorsAndMappers)
+  {
+    cs.Mapper->SetInputData(
+      vtkPolyDataMapper::SafeDownCast(cs.OriginalActor->GetMapper())->GetInput());
+
+    bool visi = cs.Actor->GetVisibility();
+    cs.Actor->vtkProp3D::ShallowCopy(cs.OriginalActor);
+    cs.Actor->SetVisibility(visi);
+  }
+  for (auto& pss : this->Pimpl->PointSpritesActorsAndMappers)
+  {
+    if (!vtkF3DGenericImporter::SafeDownCast(pss.Importer))
+    {
+      pss.Mapper->SetInputData(
+        vtkPolyDataMapper::SafeDownCast(pss.OriginalActor->GetMapper())->GetInput());
+      bool visi = pss.Actor->GetVisibility();
+      pss.Actor->vtkProp3D::ShallowCopy(pss.OriginalActor);
+      pss.Actor->SetVisibility(visi);
+    }
+  }
+
   this->Pimpl->UpdateTime.Modified();
   return ret;
 }
diff --git a/vtkext/private/module/vtkF3DMetaImporter.h b/vtkext/private/module/vtkF3DMetaImporter.h
index 280a8a0078..40ea557f8b 100644
--- a/vtkext/private/module/vtkF3DMetaImporter.h
+++ b/vtkext/private/module/vtkF3DMetaImporter.h
@@ -49,13 +49,18 @@ class vtkF3DMetaImporter : public vtkF3DImporter
 
   struct PointSpritesStruct
   {
-    PointSpritesStruct()
+    explicit PointSpritesStruct(vtkActor* originalActor, vtkImporter* importer)
+      : OriginalActor(originalActor)
+      , Importer(importer)
     {
+      this->Actor->vtkProp3D::ShallowCopy(originalActor);
       this->Actor->SetMapper(this->Mapper);
     }
 
     vtkNew<vtkActor> Actor;
     vtkNew<vtkPointGaussianMapper> Mapper;
+    vtkActor* OriginalActor;
+    vtkImporter* Importer;
   };
 
   struct ColoringStruct
@@ -67,6 +72,7 @@ class vtkF3DMetaImporter : public vtkF3DImporter
       this->Actor->GetProperty()->SetLineWidth(1.0);
       this->Actor->GetProperty()->SetRoughness(0.3);
       this->Actor->GetProperty()->SetInterpolationToPBR();
+      this->Actor->vtkProp3D::ShallowCopy(originalActor);
       this->Actor->SetMapper(this->Mapper);
       this->Mapper->InterpolateScalarsBeforeMappingOn();
     }
diff --git a/vtkext/private/module/vtkF3DRenderer.cxx b/vtkext/private/module/vtkF3DRenderer.cxx
index a42496d59f..9beae5982e 100644
--- a/vtkext/private/module/vtkF3DRenderer.cxx
+++ b/vtkext/private/module/vtkF3DRenderer.cxx
@@ -1993,128 +1993,126 @@ void vtkF3DRenderer::ConfigureActorsProperties()
     }
   }
 
-  for ([[maybe_unused]] const auto& [actor, mapper, originalActor] :
-    this->Importer->GetColoringActorsAndMappers())
+  for (const auto& coloring : this->Importer->GetColoringActorsAndMappers())
   {
     if (this->EdgeVisible.has_value())
     {
-      actor->GetProperty()->SetEdgeVisibility(this->EdgeVisible.value());
-      originalActor->GetProperty()->SetEdgeVisibility(this->EdgeVisible.value());
+      coloring.Actor->GetProperty()->SetEdgeVisibility(this->EdgeVisible.value());
+      coloring.OriginalActor->GetProperty()->SetEdgeVisibility(this->EdgeVisible.value());
     }
 
     if (this->LineWidth.has_value())
     {
-      actor->GetProperty()->SetLineWidth(this->LineWidth.value());
-      originalActor->GetProperty()->SetLineWidth(this->LineWidth.value());
+      coloring.Actor->GetProperty()->SetLineWidth(this->LineWidth.value());
+      coloring.OriginalActor->GetProperty()->SetLineWidth(this->LineWidth.value());
     }
 
     if (this->PointSize.has_value())
     {
-      actor->GetProperty()->SetPointSize(this->PointSize.value());
-      originalActor->GetProperty()->SetPointSize(this->PointSize.value());
+      coloring.Actor->GetProperty()->SetPointSize(this->PointSize.value());
+      coloring.OriginalActor->GetProperty()->SetPointSize(this->PointSize.value());
     }
 
     if (setBackfaceCulling)
     {
-      actor->GetProperty()->SetBackfaceCulling(backfaceCulling);
-      originalActor->GetProperty()->SetBackfaceCulling(backfaceCulling);
+      coloring.Actor->GetProperty()->SetBackfaceCulling(backfaceCulling);
+      coloring.OriginalActor->GetProperty()->SetBackfaceCulling(backfaceCulling);
     }
 
     if (surfaceColor)
     {
-      actor->GetProperty()->SetColor(surfaceColor);
-      originalActor->GetProperty()->SetColor(surfaceColor);
+      coloring.Actor->GetProperty()->SetColor(surfaceColor);
+      coloring.OriginalActor->GetProperty()->SetColor(surfaceColor);
     }
 
     if (this->Opacity.has_value())
     {
-      vtkInformation* info = originalActor->GetPropertyKeys();
+      vtkInformation* info = coloring.OriginalActor->GetPropertyKeys();
       if (!info || !info->Has(vtkF3DImporter::ACTOR_IS_ARMATURE()))
       {
-        actor->GetProperty()->SetOpacity(this->Opacity.value());
-        originalActor->GetProperty()->SetOpacity(this->Opacity.value());
+        coloring.Actor->GetProperty()->SetOpacity(this->Opacity.value());
+        coloring.OriginalActor->GetProperty()->SetOpacity(this->Opacity.value());
       }
     }
 
     if (this->Roughness.has_value())
     {
-      actor->GetProperty()->SetRoughness(this->Roughness.value());
-      originalActor->GetProperty()->SetRoughness(this->Roughness.value());
+      coloring.Actor->GetProperty()->SetRoughness(this->Roughness.value());
+      coloring.OriginalActor->GetProperty()->SetRoughness(this->Roughness.value());
     }
 
     if (this->Metallic.has_value())
     {
-      actor->GetProperty()->SetMetallic(this->Metallic.value());
-      originalActor->GetProperty()->SetMetallic(this->Metallic.value());
+      coloring.Actor->GetProperty()->SetMetallic(this->Metallic.value());
+      coloring.OriginalActor->GetProperty()->SetMetallic(this->Metallic.value());
     }
 
     // Textures
     if (this->TextureBaseColor.has_value())
     {
       auto colorTex = ::GetTexture(this->TextureBaseColor.value(), true);
-      actor->GetProperty()->SetBaseColorTexture(colorTex);
-      originalActor->GetProperty()->SetBaseColorTexture(colorTex);
+      coloring.Actor->GetProperty()->SetBaseColorTexture(colorTex);
+      coloring.OriginalActor->GetProperty()->SetBaseColorTexture(colorTex);
 
-      // If the input texture is RGBA, flag the actor as translucent
+      // If the input texture is RGBA, flag the coloring.Actor as translucent
       if (colorTex && colorTex->GetImageDataInput(0)->GetNumberOfScalarComponents() == 4)
       {
-        actor->ForceTranslucentOn();
-        originalActor->ForceTranslucentOn();
+        coloring.Actor->ForceTranslucentOn();
+        coloring.OriginalActor->ForceTranslucentOn();
       }
     }
 
     if (this->TextureMaterial.has_value())
     {
       auto matTex = ::GetTexture(this->TextureMaterial.value());
-      actor->GetProperty()->SetORMTexture(matTex);
-      originalActor->GetProperty()->SetORMTexture(matTex);
+      coloring.Actor->GetProperty()->SetORMTexture(matTex);
+      coloring.OriginalActor->GetProperty()->SetORMTexture(matTex);
     }
 
     if (this->TextureEmissive.has_value())
     {
       auto emissTex = ::GetTexture(this->TextureEmissive.value(), true);
-      actor->GetProperty()->SetEmissiveTexture(emissTex);
-      originalActor->GetProperty()->SetEmissiveTexture(emissTex);
+      coloring.Actor->GetProperty()->SetEmissiveTexture(emissTex);
+      coloring.OriginalActor->GetProperty()->SetEmissiveTexture(emissTex);
     }
 
     if (emissiveFactor)
     {
-      actor->GetProperty()->SetEmissiveFactor(emissiveFactor);
-      originalActor->GetProperty()->SetEmissiveFactor(emissiveFactor);
+      coloring.Actor->GetProperty()->SetEmissiveFactor(emissiveFactor);
+      coloring.OriginalActor->GetProperty()->SetEmissiveFactor(emissiveFactor);
     }
 
     if (this->TextureNormal.has_value())
     {
       auto normTex = ::GetTexture(this->TextureNormal.value());
-      actor->GetProperty()->SetNormalTexture(normTex);
-      originalActor->GetProperty()->SetNormalTexture(normTex);
+      coloring.Actor->GetProperty()->SetNormalTexture(normTex);
+      coloring.OriginalActor->GetProperty()->SetNormalTexture(normTex);
     }
 
     if (this->NormalScale.has_value())
     {
-      actor->GetProperty()->SetNormalScale(this->NormalScale.value());
-      originalActor->GetProperty()->SetNormalScale(this->NormalScale.value());
+      coloring.Actor->GetProperty()->SetNormalScale(this->NormalScale.value());
+      coloring.OriginalActor->GetProperty()->SetNormalScale(this->NormalScale.value());
     }
 
     if (this->TextureMatCap.has_value())
     {
       auto matCapTex = ::GetTexture(this->TextureMatCap.value());
-      actor->GetProperty()->SetTexture("matcap", matCapTex);
-      originalActor->GetProperty()->SetTexture("matcap", matCapTex);
+      coloring.Actor->GetProperty()->SetTexture("matcap", matCapTex);
+      coloring.OriginalActor->GetProperty()->SetTexture("matcap", matCapTex);
     }
   }
 
-  for ([[maybe_unused]] const auto& [actor, mapper] :
-    this->Importer->GetPointSpritesActorsAndMappers())
+  for (const auto& sprites : this->Importer->GetPointSpritesActorsAndMappers())
   {
     if (surfaceColor)
     {
-      actor->GetProperty()->SetColor(surfaceColor);
+      sprites.Actor->GetProperty()->SetColor(surfaceColor);
     }
 
     if (this->Opacity.has_value())
     {
-      actor->GetProperty()->SetOpacity(this->Opacity.value());
+      sprites.Actor->GetProperty()->SetOpacity(this->Opacity.value());
     }
   }
 
@@ -2146,53 +2144,53 @@ void vtkF3DRenderer::SetPointSpritesProperties(SplatType type, double pointSprit
     scaleFactor = pointSpritesSize * bbox.GetDiagonalLength() * 0.001;
   }
 
-  for (const auto& [actor, mapper] : this->Importer->GetPointSpritesActorsAndMappers())
+  for (const auto& sprites : this->Importer->GetPointSpritesActorsAndMappers())
   {
-
-    mapper->EmissiveOff();
+    sprites.Mapper->EmissiveOff();
     if (type == SplatType::GAUSSIAN)
     {
-      mapper->SetScaleFactor(1.0);
-      mapper->SetSplatShaderCode(nullptr); // gaussian is the default VTK shader
-      mapper->SetScaleArray("scale");
+      sprites.Mapper->SetScaleFactor(1.0);
+      sprites.Mapper->SetSplatShaderCode(nullptr); // gaussian is the default VTK shader
+      sprites.Mapper->SetScaleArray("scale");
 
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20231102)
-      mapper->AnisotropicOn();
-      mapper->SetBoundScale(3.0);
-      mapper->SetRotationArray("rotation");
+      sprites.Mapper->AnisotropicOn();
+      sprites.Mapper->SetBoundScale(3.0);
+      sprites.Mapper->SetRotationArray("rotation");
 
       int* viewport = this->GetSize();
 
       float lowPass[3] = { 0.3f / (viewport[0] * viewport[0]), 0.f,
         0.3f / (viewport[1] * viewport[1]) };
-      mapper->SetLowpassMatrix(lowPass);
+      sprites.Mapper->SetLowpassMatrix(lowPass);
 #else
       F3DLog::Print(F3DLog::Severity::Warning,
         "Gaussian splatting selected but VTK <= 9.3 only supports isotropic gaussians");
 #endif
 
-      actor->ForceTranslucentOn();
+      sprites.Actor->ForceTranslucentOn();
     }
     else
     {
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20231102)
-      mapper->AnisotropicOff();
-      mapper->SetLowpassMatrix(0., 0., 0.);
+      sprites.Mapper->AnisotropicOff();
+      sprites.Mapper->SetLowpassMatrix(0., 0., 0.);
 #endif
 
-      mapper->SetScaleFactor(scaleFactor);
+      sprites.Mapper->SetScaleFactor(scaleFactor);
 
-      mapper->SetSplatShaderCode("//VTK::Color::Impl\n"
-                                 "float dist = dot(offsetVCVSOutput.xy, offsetVCVSOutput.xy);\n"
-                                 "if (dist > 1.0) {\n"
-                                 "  discard;\n"
-                                 "} else {\n"
-                                 "  float scale = (1.0 - dist);\n"
-                                 "  ambientColor *= scale;\n"
-                                 "  diffuseColor *= scale;\n"
-                                 "}\n");
+      sprites.Mapper->SetSplatShaderCode(
+        "//VTK::Color::Impl\n"
+        "float dist = dot(offsetVCVSOutput.xy, offsetVCVSOutput.xy);\n"
+        "if (dist > 1.0) {\n"
+        "  discard;\n"
+        "} else {\n"
+        "  float scale = (1.0 - dist);\n"
+        "  ambientColor *= scale;\n"
+        "  diffuseColor *= scale;\n"
+        "}\n");
 
-      actor->ForceTranslucentOff();
+      sprites.Actor->ForceTranslucentOff();
     }
   }
 }
@@ -2238,11 +2236,11 @@ void vtkF3DRenderer::SetUseInverseOpacityFunction(bool use)
   if (this->UseInverseOpacityFunction != use)
   {
     this->UseInverseOpacityFunction = use;
-    for ([[maybe_unused]] const auto& [prop, mapper] : this->Importer->GetVolumePropsAndMappers())
+    for (const auto& volume : this->Importer->GetVolumePropsAndMappers())
     {
-      if (prop)
+      if (volume.Prop)
       {
-        vtkPiecewiseFunction* pwf = prop->GetProperty()->GetScalarOpacity();
+        vtkPiecewiseFunction* pwf = volume.Prop->GetProperty()->GetScalarOpacity();
         if (pwf->GetSize() == 2)
         {
           double range[2];
@@ -2379,30 +2377,30 @@ void vtkF3DRenderer::ConfigureColoring()
 
   // Handle surface geometry
   bool geometriesVisible = this->UseRaytracing || (!this->UseVolume && !this->UsePointSprites);
-  for (const auto& [actor, mapper, originalActor] : this->Importer->GetColoringActorsAndMappers())
+  for (const auto& coloring : this->Importer->GetColoringActorsAndMappers())
   {
     if (geometriesVisible)
     {
       bool visible = false;
       if (hasColoring)
       {
-        // Rely on the previous state of scalar visibility to know if we should show the actor by
-        // default
-        visible = mapper->GetScalarVisibility();
+        // Rely on the previous state of scalar visibility to know if we should show the
+        // coloring.Actor by default
+        visible = coloring.Mapper->GetScalarVisibility();
         if (!this->ColoringMappersConfigured)
         {
-          visible = vtkF3DRenderer::ConfigureMapperForColoring(mapper, info.value().Name,
+          visible = vtkF3DRenderer::ConfigureMapperForColoring(coloring.Mapper, info.value().Name,
             this->ComponentForColoring, this->ColorTransferFunction, this->ColorRange,
             this->UseCellColoring);
         }
       }
-      actor->SetVisibility(visible);
-      originalActor->SetVisibility(!visible);
+      coloring.Actor->SetVisibility(visible);
+      coloring.OriginalActor->SetVisibility(!visible);
     }
     else
     {
-      actor->SetVisibility(false);
-      originalActor->SetVisibility(false);
+      coloring.Actor->SetVisibility(false);
+      coloring.OriginalActor->SetVisibility(false);
     }
   }
   if (geometriesVisible)
@@ -2412,21 +2410,21 @@ void vtkF3DRenderer::ConfigureColoring()
 
   // Handle point sprites
   bool pointSpritesVisible = !this->UseRaytracing && !this->UseVolume && this->UsePointSprites;
-  for (const auto& [actor, mapper] : this->Importer->GetPointSpritesActorsAndMappers())
+  for (const auto& sprites : this->Importer->GetPointSpritesActorsAndMappers())
   {
-    actor->SetVisibility(pointSpritesVisible);
+    sprites.Actor->SetVisibility(pointSpritesVisible);
     if (pointSpritesVisible)
     {
       if (hasColoring)
       {
         if (!this->PointSpritesMappersConfigured)
         {
-          vtkF3DRenderer::ConfigureMapperForColoring(mapper, info.value().Name,
+          vtkF3DRenderer::ConfigureMapperForColoring(sprites.Mapper, info.value().Name,
             this->ComponentForColoring, this->ColorTransferFunction, this->ColorRange,
             this->UseCellColoring);
         }
       }
-      mapper->SetScalarVisibility(hasColoring);
+      sprites.Mapper->SetScalarVisibility(hasColoring);
     }
   }
   if (pointSpritesVisible)
@@ -2437,11 +2435,11 @@ void vtkF3DRenderer::ConfigureColoring()
   // Handle Volume prop
   bool volumeVisible = !this->UseRaytracing && this->UseVolume;
   const auto& volPropsAndMappers = this->Importer->GetVolumePropsAndMappers();
-  for (const auto& [prop, mapper] : volPropsAndMappers)
+  for (const auto& volume : volPropsAndMappers)
   {
     if (!volumeVisible)
     {
-      prop->VisibilityOff();
+      volume.Prop->VisibilityOff();
     }
     else
     {
@@ -2449,12 +2447,12 @@ void vtkF3DRenderer::ConfigureColoring()
       if (hasColoring)
       {
         // Initialize the visibility based on the mapper configuration
-        visible = !std::string(mapper->GetArrayName()).empty();
+        visible = !std::string(volume.Mapper->GetArrayName()).empty();
         if (!this->VolumePropsAndMappersConfigured)
         {
-          visible = vtkF3DRenderer::ConfigureVolumeForColoring(mapper, prop, info.value().Name,
-            this->ComponentForColoring, this->ColorTransferFunction, this->ColorRange,
-            this->UseCellColoring, this->UseInverseOpacityFunction);
+          visible = vtkF3DRenderer::ConfigureVolumeForColoring(volume.Mapper, volume.Prop,
+            info.value().Name, this->ComponentForColoring, this->ColorTransferFunction,
+            this->ColorRange, this->UseCellColoring, this->UseInverseOpacityFunction);
           if (!visible)
           {
             F3DLog::Print(F3DLog::Severity::Warning,
@@ -2462,7 +2460,7 @@ void vtkF3DRenderer::ConfigureColoring()
           }
         }
       }
-      prop->SetVisibility(visible);
+      volume.Prop->SetVisibility(visible);
     }
   }
   if (volumeVisible)


Reviews:
user: login: github-actions[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-21T08:04:52Z
user: login: github-actions[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:24:32Z
user: login: github-actions[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T07:32:42Z
user: login: exbluesbreaker
body: Seems good to me. I also checked already how it works and behavior is as expected
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:00:31Z
user: login: Meakk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T13:06:10Z
user: login: mwestphal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T13:19:05Z
user: login: mwestphal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T13:19:25Z
user: login: Meakk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T13:39:13Z
user: login: Meakk
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-26T12:39:23Z

Review comments:
diff_hunk: @@ -501,6 +501,17 @@ bool vtkF3DMetaImporter::UpdateAtTimeValue(double timeValue)
 #else
     importerPair.Importer->UpdateTimeStep(timeValue);
 #endif
+
+    // Update coloring and point sprites actors
+    for (auto& cs : this->Pimpl->ColoringActorsAndMappers)
+    {
+      cs.Actor->vtkProp3D::ShallowCopy(cs.OriginalActor);
+    }
+    for (auto& pss : this->Pimpl->PointSpritesActorsAndMappers)
+    {
+      pss.Actor->vtkProp3D::ShallowCopy(pss.OriginalActor);
+    }
+
path: vtkext/private/module/vtkF3DMetaImporter.cxx
user: login: github-actions[bot]
body: ```suggestion
    }
```
created_at: 2025-04-21T08:04:51Z
author_association: NONE
diff_hunk: @@ -2146,7 +2146,7 @@ void vtkF3DRenderer::SetPointSpritesProperties(SplatType type, double pointSprit
     scaleFactor = pointSpritesSize * bbox.GetDiagonalLength() * 0.001;
   }
 
-  for (const auto& [actor, mapper] : this->Importer->GetPointSpritesActorsAndMappers())
+  for (const auto& [actor, mapper, originalActor] : this->Importer->GetPointSpritesActorsAndMappers())
path: vtkext/private/module/vtkF3DRenderer.cxx
user: login: github-actions[bot]
body: ```suggestion
  for (const auto& [actor, mapper, originalActor] :
    this->Importer->GetPointSpritesActorsAndMappers())
```
created_at: 2025-04-21T08:04:51Z
author_association: NONE
diff_hunk: @@ -2412,7 +2412,7 @@ void vtkF3DRenderer::ConfigureColoring()
 
   // Handle point sprites
   bool pointSpritesVisible = !this->UseRaytracing && !this->UseVolume && this->UsePointSprites;
-  for (const auto& [actor, mapper] : this->Importer->GetPointSpritesActorsAndMappers())
+  for (const auto& [actor, mapper, originalActor] : this->Importer->GetPointSpritesActorsAndMappers())
path: vtkext/private/module/vtkF3DRenderer.cxx
user: login: github-actions[bot]
body: ```suggestion
  for (const auto& [actor, mapper, originalActor] :
    this->Importer->GetPointSpritesActorsAndMappers())
```
created_at: 2025-04-21T08:04:52Z
author_association: NONE
diff_hunk: @@ -44,6 +45,25 @@ struct vtkF3DMetaImporter::Internals
 #endif
 };
 
+//----------------------------------------------------------------------------
+namespace
+{
+  vtkPolyData* GetPointSpritesPoints(vtkPolyData* surface, vtkF3DGenericImporter* genericImporter)
+  {
+    if (genericImporter)
+    {
+      // For generic importer, use the single imported points
+      // TODO when supporting composite, handle with an actor based index
+      return genericImporter->GetImportedPoints();
+    }
+    else
+    {
+      return surface;
path: vtkext/private/module/vtkF3DMetaImporter.cxx
user: login: github-actions[bot]
body: ```suggestion
vtkPolyData* GetPointSpritesPoints(vtkPolyData* surface, vtkF3DGenericImporter* genericImporter)
{
  if (genericImporter)
    // For generic importer, use the single imported points
    // TODO when supporting composite, handle with an actor based index
    return genericImporter->GetImportedPoints();
  }
  else
  {
    return surface;
}
```
created_at: 2025-04-25T06:24:32Z
author_association: NONE
diff_hunk: @@ -502,6 +516,23 @@ bool vtkF3DMetaImporter::UpdateAtTimeValue(double timeValue)
     importerPair.Importer->UpdateTimeStep(timeValue);
 #endif
   }
+
+  // Update coloring and point sprites
+  for (auto& cs : this->Pimpl->ColoringActorsAndMappers)
+  {
+    cs.Actor->vtkProp3D::ShallowCopy(cs.OriginalActor);
+    cs.Mapper->SetInputData(vtkPolyDataMapper::SafeDownCast(cs.OriginalActor->GetMapper())->GetInput());
path: vtkext/private/module/vtkF3DMetaImporter.cxx
user: login: github-actions[bot]
body: ```suggestion
    cs.Mapper->SetInputData(
      vtkPolyDataMapper::SafeDownCast(cs.OriginalActor->GetMapper())->GetInput());
```
created_at: 2025-04-25T06:24:32Z
author_association: NONE
diff_hunk: @@ -502,6 +516,23 @@ bool vtkF3DMetaImporter::UpdateAtTimeValue(double timeValue)
     importerPair.Importer->UpdateTimeStep(timeValue);
 #endif
   }
+
+  // Update coloring and point sprites
+  for (auto& cs : this->Pimpl->ColoringActorsAndMappers)
+  {
+    cs.Actor->vtkProp3D::ShallowCopy(cs.OriginalActor);
+    cs.Mapper->SetInputData(vtkPolyDataMapper::SafeDownCast(cs.OriginalActor->GetMapper())->GetInput());
+  }
+  for (auto& pss : this->Pimpl->PointSpritesActorsAndMappers)
+  {
+    pss.Actor->vtkProp3D::ShallowCopy(pss.OriginalActor);
+
+    if (!vtkF3DGenericImporter::SafeDownCast(pss.Importer))
+    {
+      pss.Mapper->SetInputData(vtkPolyDataMapper::SafeDownCast(pss.OriginalActor->GetMapper())->GetInput());
+    }
path: vtkext/private/module/vtkF3DMetaImporter.cxx
user: login: github-actions[bot]
body: ```suggestion
      pss.Mapper->SetInputData(
        vtkPolyDataMapper::SafeDownCast(pss.OriginalActor->GetMapper())->GetInput());
```
created_at: 2025-04-25T06:24:32Z
author_association: NONE
diff_hunk: @@ -44,6 +45,25 @@ struct vtkF3DMetaImporter::Internals
 #endif
 };
 
+//----------------------------------------------------------------------------
+namespace
+{
+  vtkPolyData* GetPointSpritesPoints(vtkPolyData* surface, vtkF3DGenericImporter* genericImporter)
+  {
+    if (genericImporter)
+    {
+      // For generic importer, use the single imported points
+      // TODO when supporting composite, handle with an actor based index
+      return genericImporter->GetImportedPoints();
+    }
+    else
+    {
+      return surface;
path: vtkext/private/module/vtkF3DMetaImporter.cxx
user: login: github-actions[bot]
body: ```suggestion
vtkPolyData* GetPointSpritesPoints(vtkPolyData* surface, vtkF3DGenericImporter* genericImporter)
{
  if (genericImporter)
    // For generic importer, use the single imported points
    // TODO when supporting composite, handle with an actor based index
    return genericImporter->GetImportedPoints();
  }
  else
  {
    return surface;
}
```
created_at: 2025-04-25T07:32:42Z
author_association: NONE
diff_hunk: @@ -502,6 +516,27 @@ bool vtkF3DMetaImporter::UpdateAtTimeValue(double timeValue)
     importerPair.Importer->UpdateTimeStep(timeValue);
 #endif
   }
+
+  // Update coloring and point sprites
+  for (auto& cs : this->Pimpl->ColoringActorsAndMappers)
+  {
+    cs.Mapper->SetInputData(vtkPolyDataMapper::SafeDownCast(cs.OriginalActor->GetMapper())->GetInput());
path: vtkext/private/module/vtkF3DMetaImporter.cxx
user: login: github-actions[bot]
body: ```suggestion
    cs.Mapper->SetInputData(
      vtkPolyDataMapper::SafeDownCast(cs.OriginalActor->GetMapper())->GetInput());
```
created_at: 2025-04-25T07:32:42Z
author_association: NONE
diff_hunk: @@ -502,6 +516,27 @@ bool vtkF3DMetaImporter::UpdateAtTimeValue(double timeValue)
     importerPair.Importer->UpdateTimeStep(timeValue);
 #endif
   }
+
+  // Update coloring and point sprites
+  for (auto& cs : this->Pimpl->ColoringActorsAndMappers)
+  {
+    cs.Mapper->SetInputData(vtkPolyDataMapper::SafeDownCast(cs.OriginalActor->GetMapper())->GetInput());
+
+    bool visi = cs.Actor->GetVisibility();
+    cs.Actor->vtkProp3D::ShallowCopy(cs.OriginalActor);
+    cs.Actor->SetVisibility(visi);
+  }
+  for (auto& pss : this->Pimpl->PointSpritesActorsAndMappers)
+  {
+    if (!vtkF3DGenericImporter::SafeDownCast(pss.Importer))
+    {
+      pss.Mapper->SetInputData(vtkPolyDataMapper::SafeDownCast(pss.OriginalActor->GetMapper())->GetInput());
path: vtkext/private/module/vtkF3DMetaImporter.cxx
user: login: github-actions[bot]
body: ```suggestion
      pss.Mapper->SetInputData(
        vtkPolyDataMapper::SafeDownCast(pss.OriginalActor->GetMapper())->GetInput());
```
created_at: 2025-04-25T07:32:42Z
author_association: NONE
diff_hunk: @@ -2146,7 +2146,8 @@ void vtkF3DRenderer::SetPointSpritesProperties(SplatType type, double pointSprit
     scaleFactor = pointSpritesSize * bbox.GetDiagonalLength() * 0.001;
   }
 
-  for (const auto& [actor, mapper] : this->Importer->GetPointSpritesActorsAndMappers())
+  for (const auto& [actor, mapper, originalActor, importer] :
path: vtkext/private/module/vtkF3DRenderer.cxx
user: login: Meakk
body: That's an abusive way of using structured binding in my opinion
```suggestion
  for (const auto& sprites :
```
and use `sprites.Actor`, `sprites.Mapper`...
created_at: 2025-04-25T13:04:16Z
author_association: MEMBER
diff_hunk: @@ -502,6 +504,29 @@ bool vtkF3DMetaImporter::UpdateAtTimeValue(double timeValue)
     importerPair.Importer->UpdateTimeStep(timeValue);
 #endif
   }
+
+  // Update coloring and point sprites
+  for (auto& cs : this->Pimpl->ColoringActorsAndMappers)
+  {
+    cs.Mapper->SetInputData(
+      vtkPolyDataMapper::SafeDownCast(cs.OriginalActor->GetMapper())->GetInput());
+
+    bool visi = cs.Actor->GetVisibility();
+    cs.Actor->vtkProp3D::ShallowCopy(cs.OriginalActor);
path: vtkext/private/module/vtkF3DMetaImporter.cxx
user: login: Meakk
body: why cannot we use `vtkActor` shallow copy directly?
created_at: 2025-04-25T13:06:02Z
author_association: MEMBER
diff_hunk: @@ -2146,7 +2146,8 @@ void vtkF3DRenderer::SetPointSpritesProperties(SplatType type, double pointSprit
     scaleFactor = pointSpritesSize * bbox.GetDiagonalLength() * 0.001;
   }
 
-  for (const auto& [actor, mapper] : this->Importer->GetPointSpritesActorsAndMappers())
+  for (const auto& [actor, mapper, originalActor, importer] :
path: vtkext/private/module/vtkF3DRenderer.cxx
user: login: mwestphal
body: TIL
created_at: 2025-04-25T13:19:05Z
author_association: MEMBER
diff_hunk: @@ -502,6 +504,29 @@ bool vtkF3DMetaImporter::UpdateAtTimeValue(double timeValue)
     importerPair.Importer->UpdateTimeStep(timeValue);
 #endif
   }
+
+  // Update coloring and point sprites
+  for (auto& cs : this->Pimpl->ColoringActorsAndMappers)
+  {
+    cs.Mapper->SetInputData(
+      vtkPolyDataMapper::SafeDownCast(cs.OriginalActor->GetMapper())->GetInput());
+
+    bool visi = cs.Actor->GetVisibility();
+    cs.Actor->vtkProp3D::ShallowCopy(cs.OriginalActor);
path: vtkext/private/module/vtkF3DMetaImporter.cxx
user: login: mwestphal
body: Because it would copy textures too, which we do not want.
created_at: 2025-04-25T13:19:25Z
author_association: MEMBER
diff_hunk: @@ -502,6 +504,29 @@ bool vtkF3DMetaImporter::UpdateAtTimeValue(double timeValue)
     importerPair.Importer->UpdateTimeStep(timeValue);
 #endif
   }
+
+  // Update coloring and point sprites
+  for (auto& cs : this->Pimpl->ColoringActorsAndMappers)
+  {
+    cs.Mapper->SetInputData(
+      vtkPolyDataMapper::SafeDownCast(cs.OriginalActor->GetMapper())->GetInput());
+
+    bool visi = cs.Actor->GetVisibility();
+    cs.Actor->vtkProp3D::ShallowCopy(cs.OriginalActor);
path: vtkext/private/module/vtkF3DMetaImporter.cxx
user: login: Meakk
body: ok
created_at: 2025-04-25T13:39:13Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-25T08:11:24Z
body: ## [Codecov](https://app.codecov.io/gh/f3d-app/f3d/pull/2172?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=f3d-app) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 95.88%. Comparing base [(`bbb0a63`)](https://app.codecov.io/gh/f3d-app/f3d/commit/bbb0a63d0480885a0526e0230a9a09bd9a07f433?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=f3d-app) to head [(`7c16caf`)](https://app.codecov.io/gh/f3d-app/f3d/commit/7c16caf566ed23df92f9ffccd494e33823953200?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=f3d-app).
> Report is 10 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2172      +/-   ##
==========================================
+ Coverage   95.86%   95.88%   +0.01%     
==========================================
  Files         127      127              
  Lines       11060    11104      +44     
==========================================
+ Hits        10603    10647      +44     
  Misses        457      457              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/f3d-app/f3d/pull/2172?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=f3d-app).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=f3d-app).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: mwestphal
created_at: 2025-04-25T11:55:48Z
body: @exbluesbreaker @Meakk please review
author_association: MEMBER