@@ -448,37 +448,97 @@ TEST_F(MeshToSdfTest, Step3_SignDetermination_InsideOutsideAccuracy) {
448448 EXPECT_GT (clearlyOutsideVoxels, 0 ) << " Should have some clearly outside voxels" ;
449449}
450450
451- TEST_F (MeshToSdfTest, Step3_SignDetermination_WindingNumbers) {
452- // Test sign determination using winding numbers
451+ TEST_F (MeshToSdfTest, Step3_WindingNumber_OutwardNormals) {
452+ // Test WindingNumber (signed, wn > 0.5) with a cube that has outward-facing normals.
453+ // Flip the test cube's winding order so normals point outward.
454+ std::vector<Eigen::Vector3i> outwardFaces;
455+ outwardFaces.reserve (cubeFaces.size ());
456+ for (const auto & f : cubeFaces) {
457+ outwardFaces.emplace_back (f.x (), f.z (), f.y ());
458+ }
459+
453460 const BoundingBoxf bounds (
454461 Eigen::Vector3f (-1 .0f , -1 .0f , -1 .0f ), Eigen::Vector3f (1 .0f , 1 .0f , 1 .0f ));
455462 const Eigen::Vector3<Index> resolution (10 , 10 , 10 );
456463
457464 MeshToSdfConfigf config;
458465 config.narrowBandWidth = 2 .0f ;
466+ config.signMethod = SignMethod::WindingNumber;
459467
460- // Generate complete SDF
468+ const auto sdf = meshToSdf<float >(
469+ std::span<const Eigen::Vector3f>(cubeVertices),
470+ std::span<const Eigen::Vector3i>(outwardFaces),
471+ bounds,
472+ resolution,
473+ config);
474+
475+ const float centerValue = sdf.sample (Eigen::Vector3f (0 .0f , 0 .0f , 0 .0f ));
476+ EXPECT_LT (centerValue, 0 .0f ) << " Center should be inside with outward normals" ;
477+
478+ const float outsideValue = sdf.sample (Eigen::Vector3f (0 .9f , 0 .9f , 0 .9f ));
479+ EXPECT_GT (outsideValue, 0 .0f ) << " Outside point should be positive" ;
480+ }
481+
482+ TEST_F (MeshToSdfTest, Step3_WindingNumber_RejectsFlippedNormals) {
483+ // WindingNumber (signed) should classify inside as outside when normals point inward,
484+ // since winding number will be -1 (not > 0.5). This is the desired behavior —
485+ // it catches orientation errors rather than silently accepting them.
486+ const BoundingBoxf bounds (
487+ Eigen::Vector3f (-1 .0f , -1 .0f , -1 .0f ), Eigen::Vector3f (1 .0f , 1 .0f , 1 .0f ));
488+ const Eigen::Vector3<Index> resolution (10 , 10 , 10 );
489+
490+ MeshToSdfConfigf config;
491+ config.narrowBandWidth = 2 .0f ;
492+ config.signMethod = SignMethod::WindingNumber;
493+
494+ // The test cube has inward-pointing normals
461495 const auto sdf = meshToSdf<float >(
462496 std::span<const Eigen::Vector3f>(cubeVertices),
463497 std::span<const Eigen::Vector3i>(cubeFaces),
464498 bounds,
465499 resolution,
466500 config);
467501
502+ // Center should be classified as OUTSIDE (positive) because normals are flipped
503+ const float centerValue = sdf.sample (Eigen::Vector3f (0 .0f , 0 .0f , 0 .0f ));
504+ EXPECT_GT (centerValue, 0 .0f ) << " Signed winding number should reject inward normals" ;
505+ }
506+
507+ TEST_F (MeshToSdfTest, Step3_WindingNumberPermissive_AcceptsBothOrientations) {
508+ // WindingNumberPermissive should work regardless of normal orientation.
509+ const BoundingBoxf bounds (
510+ Eigen::Vector3f (-1 .0f , -1 .0f , -1 .0f ), Eigen::Vector3f (1 .0f , 1 .0f , 1 .0f ));
511+ const Eigen::Vector3<Index> resolution (10 , 10 , 10 );
512+
513+ MeshToSdfConfigf config;
514+ config.narrowBandWidth = 2 .0f ;
515+ config.signMethod = SignMethod::WindingNumberPermissive;
516+
517+ const float boundaryThreshold = 0 .05f ;
468518 int correctSigns = 0 ;
469519 int totalVoxels = 0 ;
470520
521+ // Test with the inward-normal cube — Permissive should handle it
522+ const auto sdf = meshToSdf<float >(
523+ std::span<const Eigen::Vector3f>(cubeVertices),
524+ std::span<const Eigen::Vector3i>(cubeFaces),
525+ bounds,
526+ resolution,
527+ config);
528+
471529 for (Index i = 0 ; i < resolution.x (); ++i) {
472530 for (Index j = 0 ; j < resolution.y (); ++j) {
473531 for (Index k = 0 ; k < resolution.z (); ++k) {
474- const float sdfValue = sdf.at (i, j, k);
475-
476532 const Eigen::Vector3f gridPos (
477533 static_cast <float >(i), static_cast <float >(j), static_cast <float >(k));
478534 const Eigen::Vector3f worldPos = sdf.gridToWorld (gridPos);
479535
536+ if (exactDistanceToUnitCube (worldPos) < boundaryThreshold) {
537+ continue ;
538+ }
539+
480540 const bool shouldBeInside = isInsideUnitCube (worldPos);
481- const bool sdfSaysInside = sdfValue < 0 .0f ;
541+ const bool sdfSaysInside = sdf. at (i, j, k) < 0 .0f ;
482542
483543 if (shouldBeInside == sdfSaysInside) {
484544 correctSigns++;
@@ -489,10 +549,91 @@ TEST_F(MeshToSdfTest, Step3_SignDetermination_WindingNumbers) {
489549 }
490550
491551 const float accuracy = static_cast <float >(correctSigns) / totalVoxels;
492- std::cout << " Step 3 (Winding): Sign accuracy: " << accuracy * 100 .0f << " % (" << correctSigns
493- << " /" << totalVoxels << " )" << std::endl;
552+ EXPECT_GT (accuracy, 0 .95f ) << " Permissive winding number should handle either orientation" ;
553+ }
554+
555+ TEST_F (MeshToSdfTest, Step3_WindingNumberPermissive_NonWatertightMesh) {
556+ // Test that permissive winding numbers handle a non-watertight mesh.
557+ std::vector<Eigen::Vector3i> openCubeFaces (cubeFaces.begin (), cubeFaces.end ());
558+ openCubeFaces.erase (openCubeFaces.begin () + 2 , openCubeFaces.begin () + 4 );
559+
560+ const BoundingBoxf bounds (
561+ Eigen::Vector3f (-1 .0f , -1 .0f , -1 .0f ), Eigen::Vector3f (1 .0f , 1 .0f , 1 .0f ));
562+ const Eigen::Vector3<Index> resolution (10 , 10 , 10 );
563+
564+ MeshToSdfConfigf config;
565+ config.narrowBandWidth = 2 .0f ;
566+ config.signMethod = SignMethod::WindingNumberPermissive;
567+
568+ const auto sdf = meshToSdf<float >(
569+ std::span<const Eigen::Vector3f>(cubeVertices),
570+ std::span<const Eigen::Vector3i>(openCubeFaces),
571+ bounds,
572+ resolution,
573+ config);
574+
575+ const float centerValue = sdf.sample (Eigen::Vector3f (0 .0f , 0 .0f , 0 .0f ));
576+ EXPECT_LT (centerValue, 0 .0f ) << " Center should be inside even with missing face" ;
577+
578+ const float outsideValue = sdf.sample (Eigen::Vector3f (0 .9f , 0 .9f , 0 .9f ));
579+ EXPECT_GT (outsideValue, 0 .0f ) << " Clearly outside point should be positive" ;
580+ }
581+
582+ TEST_F (MeshToSdfTest, Step3_WindingNumberPermissive_MatchesRayCastingOnWatertight) {
583+ // On a watertight mesh, both methods should produce the same sign classification.
584+ const BoundingBoxf bounds (
585+ Eigen::Vector3f (-1 .0f , -1 .0f , -1 .0f ), Eigen::Vector3f (1 .0f , 1 .0f , 1 .0f ));
586+ const Eigen::Vector3<Index> resolution (10 , 10 , 10 );
587+
588+ MeshToSdfConfigf configRay;
589+ configRay.narrowBandWidth = 2 .0f ;
590+ configRay.signMethod = SignMethod::RayCasting;
591+
592+ MeshToSdfConfigf configWind;
593+ configWind.narrowBandWidth = 2 .0f ;
594+ configWind.signMethod = SignMethod::WindingNumberPermissive;
595+
596+ const auto sdfRay = meshToSdf<float >(
597+ std::span<const Eigen::Vector3f>(cubeVertices),
598+ std::span<const Eigen::Vector3i>(cubeFaces),
599+ bounds,
600+ resolution,
601+ configRay);
602+
603+ const auto sdfWind = meshToSdf<float >(
604+ std::span<const Eigen::Vector3f>(cubeVertices),
605+ std::span<const Eigen::Vector3i>(cubeFaces),
606+ bounds,
607+ resolution,
608+ configWind);
609+
610+ const float boundaryThreshold = 0 .05f ;
611+ int agreements = 0 ;
612+ int total = 0 ;
613+
614+ for (Index i = 0 ; i < resolution.x (); ++i) {
615+ for (Index j = 0 ; j < resolution.y (); ++j) {
616+ for (Index k = 0 ; k < resolution.z (); ++k) {
617+ const Eigen::Vector3f gridPos (
618+ static_cast <float >(i), static_cast <float >(j), static_cast <float >(k));
619+ const Eigen::Vector3f worldPos = sdfRay.gridToWorld (gridPos);
620+
621+ if (exactDistanceToUnitCube (worldPos) < boundaryThreshold) {
622+ continue ;
623+ }
624+
625+ const bool rayInside = sdfRay.at (i, j, k) < 0 .0f ;
626+ const bool windInside = sdfWind.at (i, j, k) < 0 .0f ;
627+ if (rayInside == windInside) {
628+ agreements++;
629+ }
630+ total++;
631+ }
632+ }
633+ }
494634
495- EXPECT_GT (accuracy, 0 .95f ) << " Winding number sign determination should be very accurate" ;
635+ const float agreement = static_cast <float >(agreements) / total;
636+ EXPECT_GT (agreement, 0 .99f ) << " Both methods should agree on watertight mesh" ;
496637}
497638
498639TEST_F (MeshToSdfTest, IntegratedTest_CubeSDFProperties) {
0 commit comments