44#include < ipc/collision_mesh.hpp>
55#include < ipc/collisions/normal/normal_collisions.hpp>
66
7+ #include < igl/edges.h>
8+
79#include < Eigen/Dense>
810
911using namespace ipc ;
@@ -588,3 +590,126 @@ TEST_CASE(
588590 CHECK (final_y > 0.0 );
589591 CHECK (std::abs (dx (2 , 1 )) < 0.6 );
590592}
593+
594+ // Helper: (N+1)×(N+1) cloth grid in the xz-plane at y = y_offset.
595+ // Vertices are on a regular grid; edges are extracted from a triangulation.
596+ static std::tuple<Eigen::MatrixXd, Eigen::MatrixXi, Eigen::MatrixXi>
597+ make_grid_layer (int N, double spacing, double y_offset)
598+ {
599+ const int nv = (N + 1 ) * (N + 1 );
600+ Eigen::MatrixXd V (nv, 3 );
601+ for (int i = 0 ; i <= N; ++i) {
602+ for (int j = 0 ; j <= N; ++j) {
603+ V.row (i * (N + 1 ) + j) << j * spacing, y_offset, i * spacing;
604+ }
605+ }
606+
607+ Eigen::MatrixXi F (2 * N * N, 3 );
608+ int f = 0 ;
609+ for (int i = 0 ; i < N; ++i) {
610+ for (int j = 0 ; j < N; ++j) {
611+ const int a = i * (N + 1 ) + j, b = a + 1 ;
612+ const int c = (i + 1 ) * (N + 1 ) + j, d = c + 1 ;
613+ F.row (f++) << a, b, d;
614+ F.row (f++) << a, d, c;
615+ }
616+ }
617+
618+ Eigen::MatrixXi E;
619+ igl::edges (F, E);
620+ return { V, E, F };
621+ }
622+
623+ TEST_CASE (
624+ " planar_filter_step: stacked parallel cloth layers" , " [ogc][planar_dat]" )
625+ {
626+ // Flat cloth grids stacked at y = gap * i (layer 0 at bottom).
627+ // Even-indexed layers (0, 2, ...) move downward (−y);
628+ // odd-indexed layers (1, 3, ...) move upward (+y).
629+ // Adjacent pairs where an odd layer is directly below an even layer
630+ // (e.g. layers 1 and 2) approach each other and must be truncated.
631+ // Adjacent pairs where an even layer is below an odd layer (e.g. 0 and 1)
632+ // move apart and must not be spuriously truncated.
633+
634+ const int N_LAYERS = GENERATE (range (1 , 5 )); // test 1 to 4 layers
635+
636+ #ifdef NDEBUG
637+ const int N = GENERATE (2 , 10 );
638+ #else
639+ const int N = 2 ; // smaller N for debug mode to keep test fast
640+ #endif
641+ const int NV = (N + 1 ) * (N + 1 );
642+ constexpr double spacing = 0.1 ;
643+ constexpr double gap = 0.3 ; // y-separation between adjacent layers
644+ constexpr double dhat = 0.35 ; // > gap so adjacent layers are active
645+ constexpr double step = 0.2 ; // approach speed; combined 0.4 > gap
646+
647+ Eigen::MatrixXd V;
648+ Eigen::MatrixXi E, F;
649+ for (int i = 0 ; i < N_LAYERS; ++i) {
650+ auto [Vi, Ei, Fi] = make_grid_layer (N, spacing, gap * i);
651+ const int prev_nv = static_cast <int >(V.rows ());
652+ V.conservativeResize (V.rows () + Vi.rows (), 3 );
653+ E.conservativeResize (E.rows () + Ei.rows (), 2 );
654+ F.conservativeResize (F.rows () + Fi.rows (), 3 );
655+ V.bottomRows (Vi.rows ()) = Vi;
656+ E.bottomRows (Ei.rows ()) = Ei.array () + prev_nv;
657+ F.bottomRows (Fi.rows ()) = Fi.array () + prev_nv;
658+ }
659+
660+ CollisionMesh mesh (V, E, F);
661+
662+ ipc::ogc::TrustRegion tr (dhat);
663+ ipc::NormalCollisions collisions;
664+ collisions.set_collision_set_type (
665+ ipc::NormalCollisions::CollisionSetType::OGC);
666+ tr.update (mesh, V, collisions);
667+
668+ if (N_LAYERS > 1 ) {
669+ REQUIRE (!collisions.empty ());
670+ }
671+
672+ Eigen::MatrixXd x = V;
673+ Eigen::MatrixXd dx = Eigen::MatrixXd::Zero (V.rows (), 3 );
674+ for (int i = 0 ; i < N_LAYERS; ++i) {
675+ const double dir = (i % 2 == 0 ) ? -1.0 : 1.0 ; // even down, odd up
676+ dx.middleRows (i * NV, NV).col (1 ).array () = dir * step;
677+ }
678+
679+ tr.planar_filter_step (mesh, x, dx);
680+
681+ // Every layer must still move in its original direction — intra-layer
682+ // parallel-edge pairs must not produce spurious truncations.
683+ for (int i = 0 ; i < N_LAYERS; ++i) {
684+ for (int j = 0 ; j < NV; ++j) {
685+ const int k = NV * i + j;
686+ CHECK (dx (k, 0 ) == Catch::Approx (0.0 ));
687+ CHECK (dx (k, 2 ) == Catch::Approx (0.0 ));
688+ if (i % 2 == 0 ) {
689+ CHECK (dx (k, 1 ) < 0.0 ); // even: downward
690+ } else {
691+ CHECK (dx (k, 1 ) > 0.0 ); // odd: upward
692+ }
693+ }
694+ }
695+
696+ // Single layer: no collision pairs, so the step must be completely
697+ // unmodified.
698+ if (N_LAYERS == 1 ) {
699+ for (int j = 0 ; j < NV; ++j) {
700+ CHECK (dx (j, 1 ) == Catch::Approx (-step));
701+ }
702+ }
703+
704+ // Approaching pairs (odd layer i below even layer i+1): step must be
705+ // truncated and the result must be penetration-free.
706+ const Eigen::MatrixXd x_new = x + dx;
707+ for (int i = 1 ; i + 1 < N_LAYERS; i += 2 ) {
708+ CHECK (dx (i * NV, 1 ) < step - 1e-10 );
709+
710+ const double max_yi = x_new.middleRows (i * NV, NV).col (1 ).maxCoeff ();
711+ const double min_yi1 =
712+ x_new.middleRows ((i + 1 ) * NV, NV).col (1 ).minCoeff ();
713+ CHECK (min_yi1 > max_yi);
714+ }
715+ }
0 commit comments